dsh-hooks 0.11.0 → 0.13.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/README.md +58 -9
- package/README.zh.md +60 -9
- package/bin/dsh-hooks.mjs +110 -10
- package/lib/client.js +286 -56
- package/lib/config.d.ts +5 -3
- package/lib/config.js +3 -2
- package/lib/context.d.ts +10 -1
- package/lib/context.js +6 -0
- package/lib/dry-run.d.ts +29 -0
- package/lib/dry-run.js +109 -15
- package/lib/events.d.ts +12 -8
- package/lib/events.js +28 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +34 -3
- package/lib/notify.d.ts +32 -6
- package/lib/notify.js +55 -19
- package/lib/server.d.ts +1 -1
- package/lib/server.js +21 -4
- package/lib/tail.d.ts +44 -0
- package/lib/tail.js +154 -0
- package/lib/usage.d.ts +79 -0
- package/lib/usage.js +110 -0
- package/package.json +5 -5
package/lib/client.js
CHANGED
|
@@ -31,6 +31,44 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
async function fetchHistory(n = 50, fetchFn = fetch) {
|
|
32
32
|
return getJson(`/dsh-hooks/history?n=${Math.max(1, Math.min(500, Math.floor(n)))}`, fetchFn);
|
|
33
33
|
}
|
|
34
|
+
/** Outcomes a hook run can end in (kept in sync with the host record type). */
|
|
35
|
+
const HISTORY_OUTCOMES = [
|
|
36
|
+
"spawned",
|
|
37
|
+
"exit-0",
|
|
38
|
+
"exit-nonzero",
|
|
39
|
+
"timeout",
|
|
40
|
+
"spawn-failed",
|
|
41
|
+
"skipped",
|
|
42
|
+
"sent",
|
|
43
|
+
"send-failed"
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Apply the panel's filters to a batch of records. The three conditions are
|
|
47
|
+
* AND-ed; an empty filter keeps everything (the timeline's default view).
|
|
48
|
+
*/
|
|
49
|
+
function filterHistory(records, filter) {
|
|
50
|
+
const session = filter.session?.trim().toLowerCase();
|
|
51
|
+
return records.filter((record) => {
|
|
52
|
+
if (filter.event !== void 0 && filter.event !== "" && record.event !== filter.event) return false;
|
|
53
|
+
if (filter.outcome !== void 0 && filter.outcome !== "" && record.outcome !== filter.outcome) return false;
|
|
54
|
+
if (session !== void 0 && session !== "") {
|
|
55
|
+
const id = record.sessionId?.toLowerCase() ?? "";
|
|
56
|
+
const name = record.sessionName?.toLowerCase() ?? "";
|
|
57
|
+
if (!id.includes(session) && !name.includes(session)) return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Serialize records as JSONL — byte-compatible with the on-disk history log. */
|
|
63
|
+
function historyToJsonl(records) {
|
|
64
|
+
if (records.length === 0) return "";
|
|
65
|
+
return `${records.map((record) => JSON.stringify(record)).join("\n")}\n`;
|
|
66
|
+
}
|
|
67
|
+
/** Download file name for a history export, stamped with local time. */
|
|
68
|
+
function historyExportName(now = /* @__PURE__ */ new Date()) {
|
|
69
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
70
|
+
return `dsh-hooks-history-${`${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`}.jsonl`;
|
|
71
|
+
}
|
|
34
72
|
async function postTest(body, fetchFn = fetch) {
|
|
35
73
|
try {
|
|
36
74
|
const response = await fetchFn("/dsh-hooks/test", {
|
|
@@ -200,18 +238,22 @@ window.__ModuleLoader__.load({
|
|
|
200
238
|
const EVENTS = [
|
|
201
239
|
"turn/start",
|
|
202
240
|
"turn/end",
|
|
241
|
+
"tree/settled",
|
|
203
242
|
"step/end",
|
|
204
243
|
"tool/call",
|
|
205
244
|
"tool/result",
|
|
206
245
|
"user/message",
|
|
207
246
|
"approval/asked",
|
|
247
|
+
"approval/decided",
|
|
208
248
|
"session/title",
|
|
209
249
|
"session/created",
|
|
210
250
|
"session/disposed",
|
|
211
251
|
"agent/created",
|
|
212
252
|
"agent/disposed",
|
|
213
253
|
"agent/error",
|
|
214
|
-
"agent/status"
|
|
254
|
+
"agent/status",
|
|
255
|
+
"hook/failed",
|
|
256
|
+
"usage/daily"
|
|
215
257
|
];
|
|
216
258
|
const TURN_END_REASONS = [
|
|
217
259
|
"completed",
|
|
@@ -313,10 +355,35 @@ window.__ModuleLoader__.load({
|
|
|
313
355
|
}
|
|
314
356
|
});
|
|
315
357
|
const [loadError, setLoadError] = (0, react.useState)(false);
|
|
358
|
+
const [historyFilter, setHistoryFilterState] = (0, react.useState)(() => ({
|
|
359
|
+
event: loadStored("dsh-hooks.historyEvent", ""),
|
|
360
|
+
outcome: loadStored("dsh-hooks.historyOutcome", ""),
|
|
361
|
+
session: loadStored("dsh-hooks.historySession", "")
|
|
362
|
+
}));
|
|
363
|
+
const setHistoryFilter = (patch) => {
|
|
364
|
+
setHistoryFilterState((current) => {
|
|
365
|
+
const next = {
|
|
366
|
+
...current,
|
|
367
|
+
...patch
|
|
368
|
+
};
|
|
369
|
+
storeValue("dsh-hooks.historyEvent", next.event ?? "");
|
|
370
|
+
storeValue("dsh-hooks.historyOutcome", next.outcome ?? "");
|
|
371
|
+
storeValue("dsh-hooks.historySession", next.session ?? "");
|
|
372
|
+
return next;
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
const visibleHistory = history === null ? null : filterHistory(history, historyFilter);
|
|
376
|
+
const historyFiltered = (historyFilter.event ?? "") !== "" || (historyFilter.outcome ?? "") !== "" || (historyFilter.session ?? "") !== "";
|
|
316
377
|
const [event, setEvent] = (0, react.useState)("turn/end");
|
|
317
378
|
const [reason, setReason] = (0, react.useState)("completed");
|
|
318
379
|
const [tool, setTool] = (0, react.useState)("");
|
|
319
380
|
const [testResult, setTestResult] = (0, react.useState)(null);
|
|
381
|
+
const [mockFields, setMockFields] = (0, react.useState)({
|
|
382
|
+
runningSubagents: "",
|
|
383
|
+
durationMs: "",
|
|
384
|
+
usageInputTokens: "",
|
|
385
|
+
usageOutputTokens: ""
|
|
386
|
+
});
|
|
320
387
|
const [notifyChannel, setNotifyChannel] = (0, react.useState)("webhook");
|
|
321
388
|
const [notifyUrl, setNotifyUrl] = (0, react.useState)("");
|
|
322
389
|
const [notifySlack, setNotifySlack] = (0, react.useState)(false);
|
|
@@ -341,7 +408,7 @@ window.__ModuleLoader__.load({
|
|
|
341
408
|
const refresh = (0, react.useCallback)(async () => {
|
|
342
409
|
const [statusInfo, records, feishuInfo] = await Promise.all([
|
|
343
410
|
fetchStatus(),
|
|
344
|
-
fetchHistory(
|
|
411
|
+
fetchHistory(200),
|
|
345
412
|
fetchFeishuStatus()
|
|
346
413
|
]);
|
|
347
414
|
setStatus(statusInfo);
|
|
@@ -375,18 +442,47 @@ window.__ModuleLoader__.load({
|
|
|
375
442
|
}, [
|
|
376
443
|
event,
|
|
377
444
|
reason,
|
|
378
|
-
tool
|
|
445
|
+
tool,
|
|
446
|
+
mockFields
|
|
379
447
|
]);
|
|
448
|
+
/** Numeric overrides the user actually filled in (empty inputs are skipped). */
|
|
449
|
+
const testFields = () => {
|
|
450
|
+
const fields = {};
|
|
451
|
+
for (const [key, raw] of Object.entries(mockFields)) {
|
|
452
|
+
const text = raw.trim();
|
|
453
|
+
if (text === "") continue;
|
|
454
|
+
const value = Number(text);
|
|
455
|
+
if (Number.isFinite(value)) fields[key] = value;
|
|
456
|
+
}
|
|
457
|
+
return Object.keys(fields).length > 0 ? fields : void 0;
|
|
458
|
+
};
|
|
380
459
|
const runTest = async (execute) => {
|
|
381
460
|
const result = await postTest({
|
|
382
461
|
event,
|
|
383
462
|
reason: event === "turn/end" && reason !== "" ? reason : void 0,
|
|
384
463
|
tool: tool !== "" ? tool : void 0,
|
|
464
|
+
fields: testFields(),
|
|
385
465
|
execute
|
|
386
466
|
});
|
|
387
467
|
setTestResult(result);
|
|
388
468
|
if (execute) refresh();
|
|
389
469
|
};
|
|
470
|
+
/** Download the filtered timeline as JSONL (same shape as the on-disk log). */
|
|
471
|
+
const exportHistory = () => {
|
|
472
|
+
const records = visibleHistory ?? [];
|
|
473
|
+
if (records.length === 0) return;
|
|
474
|
+
const name = historyExportName();
|
|
475
|
+
const blob = new Blob([historyToJsonl(records)], { type: "application/x-ndjson" });
|
|
476
|
+
const url = URL.createObjectURL(blob);
|
|
477
|
+
try {
|
|
478
|
+
const anchor = document.createElement("a");
|
|
479
|
+
anchor.href = url;
|
|
480
|
+
anchor.download = name;
|
|
481
|
+
anchor.click();
|
|
482
|
+
} finally {
|
|
483
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
390
486
|
const sendNotifyTest = async () => {
|
|
391
487
|
setNotifyResult(null);
|
|
392
488
|
const result = await postNotifyTest(notifyChannel, notifyUrl.trim() || void 0, notifySlack);
|
|
@@ -699,6 +795,46 @@ window.__ModuleLoader__.load({
|
|
|
699
795
|
})
|
|
700
796
|
]
|
|
701
797
|
}),
|
|
798
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
799
|
+
className: "dh-test-row",
|
|
800
|
+
children: [
|
|
801
|
+
[
|
|
802
|
+
"runningSubagents",
|
|
803
|
+
"runningSubagents",
|
|
804
|
+
"0"
|
|
805
|
+
],
|
|
806
|
+
[
|
|
807
|
+
"durationMs",
|
|
808
|
+
"durationMs",
|
|
809
|
+
"1200"
|
|
810
|
+
],
|
|
811
|
+
[
|
|
812
|
+
"usageInputTokens",
|
|
813
|
+
"usage 输入",
|
|
814
|
+
"120000"
|
|
815
|
+
],
|
|
816
|
+
[
|
|
817
|
+
"usageOutputTokens",
|
|
818
|
+
"usage 输出",
|
|
819
|
+
"45000"
|
|
820
|
+
]
|
|
821
|
+
].map(([key, label, placeholder]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
822
|
+
className: "dh-field",
|
|
823
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
824
|
+
className: "dh-field-label",
|
|
825
|
+
children: [label, "(可选)"]
|
|
826
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
827
|
+
className: "dh-input",
|
|
828
|
+
value: mockFields[key],
|
|
829
|
+
inputMode: "numeric",
|
|
830
|
+
onChange: (e) => setMockFields((current) => ({
|
|
831
|
+
...current,
|
|
832
|
+
[key]: e.target.value
|
|
833
|
+
})),
|
|
834
|
+
placeholder
|
|
835
|
+
})]
|
|
836
|
+
}, key))
|
|
837
|
+
}),
|
|
702
838
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
703
839
|
className: "dh-buttons",
|
|
704
840
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -1343,58 +1479,152 @@ window.__ModuleLoader__.load({
|
|
|
1343
1479
|
]
|
|
1344
1480
|
}, hook.index))
|
|
1345
1481
|
})] }),
|
|
1346
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
1369
|
-
className: "dh-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1482
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [
|
|
1483
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1484
|
+
className: "dh-section-head",
|
|
1485
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
1486
|
+
className: "dh-section-title",
|
|
1487
|
+
children: ["执行历史(最近 30 条)", status !== null && status.historyCount > 0 ? ` · ${status.historyCount} 条` : ""]
|
|
1488
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1489
|
+
type: "button",
|
|
1490
|
+
className: "dh-button dh-toggle",
|
|
1491
|
+
onClick: () => setHistoryOpen(!historyOpen),
|
|
1492
|
+
"aria-expanded": historyOpen,
|
|
1493
|
+
children: historyOpen ? "收起 ▲" : "展开 ▼"
|
|
1494
|
+
})]
|
|
1495
|
+
}),
|
|
1496
|
+
historyOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1497
|
+
className: "dh-test-row dh-history-filters",
|
|
1498
|
+
children: [
|
|
1499
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1500
|
+
className: "dh-field",
|
|
1501
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1502
|
+
className: "dh-field-label",
|
|
1503
|
+
children: "事件"
|
|
1504
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1505
|
+
className: "dh-select",
|
|
1506
|
+
value: historyFilter.event ?? "",
|
|
1507
|
+
onChange: (e) => setHistoryFilter({ event: e.target.value }),
|
|
1508
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1509
|
+
value: "",
|
|
1510
|
+
children: "全部"
|
|
1511
|
+
}), EVENTS.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1512
|
+
value: name,
|
|
1513
|
+
children: name
|
|
1514
|
+
}, name))]
|
|
1515
|
+
})]
|
|
1516
|
+
}),
|
|
1517
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1518
|
+
className: "dh-field",
|
|
1519
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1520
|
+
className: "dh-field-label",
|
|
1521
|
+
children: "结果"
|
|
1522
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1523
|
+
className: "dh-select",
|
|
1524
|
+
value: historyFilter.outcome ?? "",
|
|
1525
|
+
onChange: (e) => setHistoryFilter({ outcome: e.target.value }),
|
|
1526
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1527
|
+
value: "",
|
|
1528
|
+
children: "全部"
|
|
1529
|
+
}), HISTORY_OUTCOMES.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1530
|
+
value: name,
|
|
1531
|
+
children: outcomeLabel(name)
|
|
1532
|
+
}, name))]
|
|
1533
|
+
})]
|
|
1534
|
+
}),
|
|
1535
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1536
|
+
className: "dh-field",
|
|
1537
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1538
|
+
className: "dh-field-label",
|
|
1539
|
+
children: "会话(可选)"
|
|
1540
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1541
|
+
className: "dh-input",
|
|
1542
|
+
value: historyFilter.session ?? "",
|
|
1543
|
+
onChange: (e) => setHistoryFilter({ session: e.target.value }),
|
|
1544
|
+
placeholder: "id 或名称片段"
|
|
1545
|
+
})]
|
|
1546
|
+
}),
|
|
1547
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1548
|
+
className: "dh-buttons",
|
|
1549
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1550
|
+
type: "button",
|
|
1551
|
+
className: "dh-button",
|
|
1552
|
+
disabled: !historyFiltered,
|
|
1553
|
+
onClick: () => setHistoryFilter({
|
|
1554
|
+
event: "",
|
|
1555
|
+
outcome: "",
|
|
1556
|
+
session: ""
|
|
1557
|
+
}),
|
|
1558
|
+
children: "清空过滤"
|
|
1559
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1560
|
+
type: "button",
|
|
1561
|
+
className: "dh-button",
|
|
1562
|
+
disabled: (visibleHistory ?? []).length === 0,
|
|
1563
|
+
onClick: exportHistory,
|
|
1564
|
+
children: "导出 JSONL"
|
|
1565
|
+
})]
|
|
1566
|
+
})
|
|
1567
|
+
]
|
|
1568
|
+
}),
|
|
1569
|
+
historyOpen && history !== null && history.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1570
|
+
className: "dh-history-count",
|
|
1571
|
+
children: [
|
|
1572
|
+
"显示 ",
|
|
1573
|
+
visibleHistory?.length ?? 0,
|
|
1574
|
+
" / 共 ",
|
|
1575
|
+
history.length,
|
|
1576
|
+
" 条(最近 200 条内)",
|
|
1577
|
+
historyFiltered ? " · 已过滤" : ""
|
|
1578
|
+
]
|
|
1579
|
+
}),
|
|
1580
|
+
historyOpen && (history === null || history.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1581
|
+
className: "dh-empty",
|
|
1582
|
+
children: history === null ? "加载中…" : "暂无记录"
|
|
1583
|
+
}) : (visibleHistory ?? []).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1584
|
+
className: "dh-empty",
|
|
1585
|
+
children: "没有符合过滤条件的记录"
|
|
1586
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1587
|
+
className: "dh-timeline",
|
|
1588
|
+
children: [...visibleHistory ?? []].reverse().map((record, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1589
|
+
className: "dh-record",
|
|
1590
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1591
|
+
className: "dh-record-main",
|
|
1592
|
+
children: [
|
|
1593
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1594
|
+
className: "dh-record-top",
|
|
1595
|
+
children: [
|
|
1596
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1597
|
+
className: "dh-record-time",
|
|
1598
|
+
children: formatTime(record.ts)
|
|
1599
|
+
}),
|
|
1600
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1601
|
+
className: "dh-record-event",
|
|
1602
|
+
children: record.event
|
|
1603
|
+
}),
|
|
1604
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1605
|
+
className: `dh-outcome ${outcomeClass(record.outcome)}`,
|
|
1606
|
+
children: outcomeLabel(record.outcome)
|
|
1607
|
+
})
|
|
1608
|
+
]
|
|
1609
|
+
}),
|
|
1610
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1611
|
+
className: "dh-record-command",
|
|
1612
|
+
title: record.command,
|
|
1613
|
+
children: record.command
|
|
1614
|
+
}),
|
|
1615
|
+
record.sessionName !== void 0 && record.sessionName !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1616
|
+
className: "dh-record-session",
|
|
1617
|
+
children: record.sessionName
|
|
1618
|
+
}),
|
|
1619
|
+
record.error !== void 0 && record.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1620
|
+
className: "dh-record-error",
|
|
1621
|
+
children: record.error.slice(0, 200)
|
|
1622
|
+
})
|
|
1623
|
+
]
|
|
1624
|
+
})
|
|
1625
|
+
}, `${record.ts}-${index}`))
|
|
1626
|
+
}))
|
|
1627
|
+
] })
|
|
1398
1628
|
]
|
|
1399
1629
|
});
|
|
1400
1630
|
}
|
|
@@ -1413,7 +1643,7 @@ window.__ModuleLoader__.load({
|
|
|
1413
1643
|
}
|
|
1414
1644
|
//#endregion
|
|
1415
1645
|
//#region src/client/settings-card.module.css?inline
|
|
1416
|
-
var settings_card_module_default = ":root {\n --dsh-hooks-border: #80849038;\n --dsh-hooks-muted: #767c85;\n --dsh-hooks-accent: #4d8df7;\n --dsh-hooks-ok: #3fb56b;\n --dsh-hooks-bad: #e5534b;\n --dsh-hooks-warn: #d9a13c;\n}\n\n.dh-card {\n flex-direction: column;\n gap: 14px;\n padding: 12px 4px;\n font-size: 13px;\n line-height: 1.5;\n display: flex;\n}\n\n.dh-card-head {\n align-items: center;\n gap: 10px;\n display: flex;\n}\n\n.dh-card-title {\n font-size: 14px;\n font-weight: 600;\n}\n\n.dh-badges {\n gap: 6px;\n display: flex;\n}\n\n.dh-badge {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n background: #80849029;\n border-radius: 9px;\n padding: 1px 7px;\n font-size: 11px;\n}\n\n.dh-section-title {\n color: var(--dsh-hooks-muted);\n text-transform: uppercase;\n letter-spacing: .04em;\n margin: 0 0 8px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.dh-section-head {\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n margin: 0 0 8px;\n display: flex;\n}\n\n.dh-section-head .dh-section-title {\n margin: 0;\n}\n\n.dh-toggle {\n padding: 2px 10px;\n font-size: 11px;\n}\n\n.dh-field-narrow {\n flex: 0 0 150px;\n}\n\n.dh-feishu-row {\n align-items: flex-end;\n gap: 8px;\n display: flex;\n}\n\n.dh-timeline {\n flex-direction: column;\n gap: 6px;\n display: flex;\n}\n\n.dh-record {\n border: 1px solid var(--dsh-hooks-border);\n border-radius: 6px;\n gap: 8px;\n padding: 7px 9px;\n display: flex;\n}\n\n.dh-record-main {\n flex: 1;\n min-width: 0;\n}\n\n.dh-record-top {\n align-items: baseline;\n gap: 6px;\n display: flex;\n}\n\n.dh-record-time {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n font-size: 11px;\n}\n\n.dh-record-event {\n white-space: nowrap;\n text-overflow: ellipsis;\n font-weight: 600;\n overflow: hidden;\n}\n\n.dh-record-command {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: left;\n direction: rtl;\n font-size: 12px;\n overflow: hidden;\n}\n\n.dh-outcome {\n white-space: nowrap;\n border-radius: 9px;\n align-self: flex-start;\n padding: 1px 7px;\n font-size: 11px;\n}\n\n.dh-outcome-ok {\n color: var(--dsh-hooks-ok);\n background: #3fb56b29;\n}\n\n.dh-outcome-bad {\n color: var(--dsh-hooks-bad);\n background: #e5534b29;\n}\n\n.dh-outcome-warn {\n color: var(--dsh-hooks-warn);\n background: #d9a13c29;\n}\n\n.dh-outcome-neutral {\n color: var(--dsh-hooks-muted);\n background: #80849029;\n}\n\n.dh-record-error {\n color: var(--dsh-hooks-bad);\n white-space: pre-wrap;\n word-break: break-all;\n margin-top: 4px;\n font-size: 11px;\n}\n\n.dh-empty {\n color: var(--dsh-hooks-muted);\n padding: 6px 2px;\n font-size: 12px;\n}\n\n.dh-test-form {\n flex-direction: column;\n gap: 8px;\n display: flex;\n}\n\n.dh-test-row {\n gap: 8px;\n display: flex;\n}\n\n.dh-field {\n flex-direction: column;\n flex: 1;\n gap: 3px;\n min-width: 0;\n display: flex;\n}\n\n.dh-field-label {\n color: var(--dsh-hooks-muted);\n font-size: 11px;\n}\n\n.dh-input, .dh-select {\n border: 1px solid var(--dsh-hooks-border);\n color: inherit;\n box-sizing: border-box;\n background: #8084901f;\n border-radius: 5px;\n outline: none;\n width: 100%;\n padding: 5px 8px;\n font-size: 12px;\n}\n\nbody[data-ds-dark-theme] .dh-select {\n color-scheme: dark;\n}\n\n.dh-select option {\n background-color: var(--dsw-specific-menu, transparent);\n color: inherit;\n}\n\n.dh-input:focus, .dh-select:focus {\n border-color: var(--dsh-hooks-accent);\n}\n\n.dh-buttons {\n gap: 8px;\n display: flex;\n}\n\n.dh-button {\n border: 1px solid var(--dsh-hooks-border);\n color: inherit;\n cursor: pointer;\n background: #8084901f;\n border-radius: 5px;\n padding: 5px 12px;\n font-size: 12px;\n}\n\n.dh-button:hover {\n background: #80849038;\n}\n\n.dh-button-primary {\n background: var(--dsh-hooks-accent);\n border-color: var(--dsh-hooks-accent);\n color: #fff;\n}\n\n.dh-button-primary:hover {\n background: #3c7de8;\n}\n\n.dh-test-results {\n flex-direction: column;\n gap: 4px;\n display: flex;\n}\n\n.dh-test-line {\n word-break: break-all;\n border-radius: 5px;\n padding: 4px 8px;\n font-size: 12px;\n}\n\n.dh-test-line-match {\n color: var(--dsh-hooks-ok);\n background: #3fb56b24;\n}\n\n.dh-test-line-skip {\n color: var(--dsh-hooks-muted);\n background: #8084901a;\n}\n\n.dh-error-banner {\n color: var(--dsh-hooks-bad);\n background: #e5534b1f;\n border: 1px solid #e5534b66;\n border-radius: 6px;\n padding: 8px 10px;\n font-size: 12px;\n}\n\n.dh-feishu {\n flex-direction: column;\n gap: 8px;\n display: flex;\n}\n\n.dh-feishu-form, .dh-feishu-status, .dh-feishu-qr {\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n display: flex;\n}\n\n.dh-feishu-qr-img {\n border: 1px solid var(--dsh-hooks-border);\n box-sizing: border-box;\n background: #fff;\n border-radius: 8px;\n width: 220px;\n height: 220px;\n padding: 6px;\n}\n\n.dh-feishu-line {\n font-size: 12px;\n}\n\n.dh-feishu-ok {\n color: var(--dsh-hooks-ok);\n}\n\n.dh-feishu-error {\n color: var(--dsh-hooks-bad);\n word-break: break-all;\n font-size: 12px;\n}\n\n.dh-feishu-hint {\n color: var(--dsh-hooks-muted);\n font-size: 11px;\n}\n\n.dh-feishu-link {\n color: var(--dsh-hooks-accent);\n font-size: 12px;\n}\n\n.dh-badge-bad {\n color: var(--dsh-hooks-bad);\n background: #e5534b29;\n}\n\n.dh-error-retry {\n flex-shrink: 0;\n margin-left: 8px;\n padding: 2px 10px;\n font-size: 11px;\n}\n\n.dh-check {\n color: var(--dsh-hooks-muted);\n align-items: center;\n gap: 6px;\n font-size: 12px;\n display: flex;\n}\n\n.dh-button-small {\n padding: 2px 8px;\n font-size: 11px;\n}\n\n.dh-button-danger {\n color: var(--dsh-hooks-bad);\n border-color: #e5534b80;\n}\n\n.dh-button-danger:hover {\n background: #e5534b24;\n}\n\n.dh-hook-list, .dh-hook-editor {\n flex-direction: column;\n gap: 6px;\n display: flex;\n}\n\n.dh-hook {\n border: 1px solid var(--dsh-hooks-border);\n border-radius: 6px;\n flex-direction: column;\n gap: 4px;\n padding: 7px 9px;\n display: flex;\n}\n\n.dh-hook-editing {\n border-color: #4d8df766;\n gap: 8px;\n padding: 9px;\n}\n\n.dh-hook-head {\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n display: flex;\n}\n\n.dh-hook-event {\n white-space: nowrap;\n font-size: 12px;\n font-weight: 600;\n}\n\n.dh-hook-action {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: left;\n direction: rtl;\n flex: 1;\n min-width: 0;\n font-size: 12px;\n overflow: hidden;\n}\n\n.dh-hook-match {\n color: var(--dsh-hooks-accent);\n word-break: break-all;\n font-size: 11px;\n}\n\n.dh-hook-meta {\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.dh-match-key {\n flex: 0 0 130px;\n}\n\n.dh-feishu-preview {\n color: var(--dsh-hooks-muted);\n border-left: 2px solid var(--dsh-hooks-border);\n word-break: break-all;\n max-height: 120px;\n padding: 4px 8px;\n font-size: 11px;\n overflow: hidden;\n}\n";
|
|
1646
|
+
var settings_card_module_default = ":root {\n --dsh-hooks-border: #80849038;\n --dsh-hooks-muted: #767c85;\n --dsh-hooks-accent: #4d8df7;\n --dsh-hooks-ok: #3fb56b;\n --dsh-hooks-bad: #e5534b;\n --dsh-hooks-warn: #d9a13c;\n}\n\n.dh-card {\n flex-direction: column;\n gap: 14px;\n padding: 12px 4px;\n font-size: 13px;\n line-height: 1.5;\n display: flex;\n}\n\n.dh-card-head {\n align-items: center;\n gap: 10px;\n display: flex;\n}\n\n.dh-card-title {\n font-size: 14px;\n font-weight: 600;\n}\n\n.dh-badges {\n gap: 6px;\n display: flex;\n}\n\n.dh-badge {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n background: #80849029;\n border-radius: 9px;\n padding: 1px 7px;\n font-size: 11px;\n}\n\n.dh-section-title {\n color: var(--dsh-hooks-muted);\n text-transform: uppercase;\n letter-spacing: .04em;\n margin: 0 0 8px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.dh-section-head {\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n margin: 0 0 8px;\n display: flex;\n}\n\n.dh-section-head .dh-section-title {\n margin: 0;\n}\n\n.dh-toggle {\n padding: 2px 10px;\n font-size: 11px;\n}\n\n.dh-field-narrow {\n flex: 0 0 150px;\n}\n\n.dh-feishu-row {\n align-items: flex-end;\n gap: 8px;\n display: flex;\n}\n\n.dh-timeline {\n flex-direction: column;\n gap: 6px;\n display: flex;\n}\n\n.dh-record {\n border: 1px solid var(--dsh-hooks-border);\n border-radius: 6px;\n gap: 8px;\n padding: 7px 9px;\n display: flex;\n}\n\n.dh-record-main {\n flex: 1;\n min-width: 0;\n}\n\n.dh-record-top {\n align-items: baseline;\n gap: 6px;\n display: flex;\n}\n\n.dh-record-time {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n font-size: 11px;\n}\n\n.dh-record-event {\n white-space: nowrap;\n text-overflow: ellipsis;\n font-weight: 600;\n overflow: hidden;\n}\n\n.dh-record-command {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: left;\n direction: rtl;\n font-size: 12px;\n overflow: hidden;\n}\n\n.dh-outcome {\n white-space: nowrap;\n border-radius: 9px;\n align-self: flex-start;\n padding: 1px 7px;\n font-size: 11px;\n}\n\n.dh-outcome-ok {\n color: var(--dsh-hooks-ok);\n background: #3fb56b29;\n}\n\n.dh-outcome-bad {\n color: var(--dsh-hooks-bad);\n background: #e5534b29;\n}\n\n.dh-outcome-warn {\n color: var(--dsh-hooks-warn);\n background: #d9a13c29;\n}\n\n.dh-outcome-neutral {\n color: var(--dsh-hooks-muted);\n background: #80849029;\n}\n\n.dh-record-error {\n color: var(--dsh-hooks-bad);\n white-space: pre-wrap;\n word-break: break-all;\n margin-top: 4px;\n font-size: 11px;\n}\n\n.dh-empty {\n color: var(--dsh-hooks-muted);\n padding: 6px 2px;\n font-size: 12px;\n}\n\n.dh-history-filters {\n flex-wrap: wrap;\n align-items: flex-end;\n margin-bottom: 6px;\n}\n\n.dh-history-filters .dh-buttons {\n flex: none;\n}\n\n.dh-history-count {\n color: var(--dsh-hooks-muted);\n padding: 0 2px 6px;\n font-size: 11px;\n}\n\n.dh-record-session {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n text-overflow: ellipsis;\n font-size: 11px;\n overflow: hidden;\n}\n\n.dh-test-form {\n flex-direction: column;\n gap: 8px;\n display: flex;\n}\n\n.dh-test-row {\n gap: 8px;\n display: flex;\n}\n\n.dh-field {\n flex-direction: column;\n flex: 1;\n gap: 3px;\n min-width: 0;\n display: flex;\n}\n\n.dh-field-label {\n color: var(--dsh-hooks-muted);\n font-size: 11px;\n}\n\n.dh-input, .dh-select {\n border: 1px solid var(--dsh-hooks-border);\n color: inherit;\n box-sizing: border-box;\n background: #8084901f;\n border-radius: 5px;\n outline: none;\n width: 100%;\n padding: 5px 8px;\n font-size: 12px;\n}\n\nbody[data-ds-dark-theme] .dh-select {\n color-scheme: dark;\n}\n\n.dh-select option {\n background-color: var(--dsw-specific-menu, transparent);\n color: inherit;\n}\n\n.dh-input:focus, .dh-select:focus {\n border-color: var(--dsh-hooks-accent);\n}\n\n.dh-buttons {\n gap: 8px;\n display: flex;\n}\n\n.dh-button {\n border: 1px solid var(--dsh-hooks-border);\n color: inherit;\n cursor: pointer;\n background: #8084901f;\n border-radius: 5px;\n padding: 5px 12px;\n font-size: 12px;\n}\n\n.dh-button:hover {\n background: #80849038;\n}\n\n.dh-button-primary {\n background: var(--dsh-hooks-accent);\n border-color: var(--dsh-hooks-accent);\n color: #fff;\n}\n\n.dh-button-primary:hover {\n background: #3c7de8;\n}\n\n.dh-test-results {\n flex-direction: column;\n gap: 4px;\n display: flex;\n}\n\n.dh-test-line {\n word-break: break-all;\n border-radius: 5px;\n padding: 4px 8px;\n font-size: 12px;\n}\n\n.dh-test-line-match {\n color: var(--dsh-hooks-ok);\n background: #3fb56b24;\n}\n\n.dh-test-line-skip {\n color: var(--dsh-hooks-muted);\n background: #8084901a;\n}\n\n.dh-error-banner {\n color: var(--dsh-hooks-bad);\n background: #e5534b1f;\n border: 1px solid #e5534b66;\n border-radius: 6px;\n padding: 8px 10px;\n font-size: 12px;\n}\n\n.dh-feishu {\n flex-direction: column;\n gap: 8px;\n display: flex;\n}\n\n.dh-feishu-form, .dh-feishu-status, .dh-feishu-qr {\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n display: flex;\n}\n\n.dh-feishu-qr-img {\n border: 1px solid var(--dsh-hooks-border);\n box-sizing: border-box;\n background: #fff;\n border-radius: 8px;\n width: 220px;\n height: 220px;\n padding: 6px;\n}\n\n.dh-feishu-line {\n font-size: 12px;\n}\n\n.dh-feishu-ok {\n color: var(--dsh-hooks-ok);\n}\n\n.dh-feishu-error {\n color: var(--dsh-hooks-bad);\n word-break: break-all;\n font-size: 12px;\n}\n\n.dh-feishu-hint {\n color: var(--dsh-hooks-muted);\n font-size: 11px;\n}\n\n.dh-feishu-link {\n color: var(--dsh-hooks-accent);\n font-size: 12px;\n}\n\n.dh-badge-bad {\n color: var(--dsh-hooks-bad);\n background: #e5534b29;\n}\n\n.dh-error-retry {\n flex-shrink: 0;\n margin-left: 8px;\n padding: 2px 10px;\n font-size: 11px;\n}\n\n.dh-check {\n color: var(--dsh-hooks-muted);\n align-items: center;\n gap: 6px;\n font-size: 12px;\n display: flex;\n}\n\n.dh-button-small {\n padding: 2px 8px;\n font-size: 11px;\n}\n\n.dh-button-danger {\n color: var(--dsh-hooks-bad);\n border-color: #e5534b80;\n}\n\n.dh-button-danger:hover {\n background: #e5534b24;\n}\n\n.dh-hook-list, .dh-hook-editor {\n flex-direction: column;\n gap: 6px;\n display: flex;\n}\n\n.dh-hook {\n border: 1px solid var(--dsh-hooks-border);\n border-radius: 6px;\n flex-direction: column;\n gap: 4px;\n padding: 7px 9px;\n display: flex;\n}\n\n.dh-hook-editing {\n border-color: #4d8df766;\n gap: 8px;\n padding: 9px;\n}\n\n.dh-hook-head {\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n display: flex;\n}\n\n.dh-hook-event {\n white-space: nowrap;\n font-size: 12px;\n font-weight: 600;\n}\n\n.dh-hook-action {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: left;\n direction: rtl;\n flex: 1;\n min-width: 0;\n font-size: 12px;\n overflow: hidden;\n}\n\n.dh-hook-match {\n color: var(--dsh-hooks-accent);\n word-break: break-all;\n font-size: 11px;\n}\n\n.dh-hook-meta {\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.dh-match-key {\n flex: 0 0 130px;\n}\n\n.dh-feishu-preview {\n color: var(--dsh-hooks-muted);\n border-left: 2px solid var(--dsh-hooks-border);\n word-break: break-all;\n max-height: 120px;\n padding: 4px 8px;\n font-size: 11px;\n overflow: hidden;\n}\n";
|
|
1417
1647
|
//#endregion
|
|
1418
1648
|
//#region src/client/index.ts
|
|
1419
1649
|
const name = "dsh-hooks";
|
package/lib/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Hookable event kinds. v1 is emit-only: no waterfall/interception events. */
|
|
2
|
-
export declare const HOOK_EVENTS: readonly ['turn/start', 'turn/end', 'tree/settled', 'step/end', 'tool/call', 'tool/result', 'user/message', 'approval/asked', 'approval/decided', 'session/title', 'session/created', 'session/disposed', 'agent/created', 'agent/disposed', 'agent/error', 'agent/status', 'hook/failed'];
|
|
2
|
+
export declare const HOOK_EVENTS: readonly ['turn/start', 'turn/end', 'tree/settled', 'step/end', 'tool/call', 'tool/result', 'user/message', 'approval/asked', 'approval/decided', 'session/title', 'session/created', 'session/disposed', 'agent/created', 'agent/disposed', 'agent/error', 'agent/status', 'hook/failed', 'usage/daily'];
|
|
3
3
|
export type HookEvent = (typeof HOOK_EVENTS)[number];
|
|
4
4
|
/** `turn/end` reason kinds (from @deepseek-ai/dsh-session TurnEndReasonMap). */
|
|
5
5
|
export declare const TURN_END_REASONS: readonly ['completed', 'error', 'aborted', 'blocked', 'max-tokens', 'interrupted'];
|
|
@@ -65,8 +65,10 @@ export interface HookSpec {
|
|
|
65
65
|
/** Per-hook timeout in milliseconds. Defaults to 10000. */
|
|
66
66
|
timeoutMs?: number;
|
|
67
67
|
/**
|
|
68
|
-
* Retry count for
|
|
69
|
-
*
|
|
68
|
+
* Retry count for retryable failures (default 0: fire-and-forget, never
|
|
69
|
+
* retried). `run`: non-zero exit codes only — spawn failures and timeouts
|
|
70
|
+
* are never retried. `notify` (webhook): transport failures and HTTP
|
|
71
|
+
* 408/429/5xx; the desktop channel never retries.
|
|
70
72
|
*/
|
|
71
73
|
retries?: number;
|
|
72
74
|
/** Base delay between retries in milliseconds; doubles per attempt. Defaults to 500. */
|
package/lib/config.js
CHANGED
|
@@ -18,6 +18,7 @@ export const HOOK_EVENTS = [
|
|
|
18
18
|
'agent/error',
|
|
19
19
|
'agent/status',
|
|
20
20
|
'hook/failed',
|
|
21
|
+
'usage/daily',
|
|
21
22
|
];
|
|
22
23
|
/** `turn/end` reason kinds (from @deepseek-ai/dsh-session TurnEndReasonMap). */
|
|
23
24
|
export const TURN_END_REASONS = [
|
|
@@ -34,7 +35,7 @@ export const TURN_END_REASONS = [
|
|
|
34
35
|
// declaration self-contained.
|
|
35
36
|
export const Config = Schema.object({
|
|
36
37
|
hooks: Schema.array(Schema.object({
|
|
37
|
-
on: Schema.union([...HOOK_EVENTS]).description('触发事件:turn/start | turn/end | tree/settled | step/end | tool/call | tool/result | user/message | approval/asked | approval/decided | session/title | session/created | session/disposed | agent/created | agent/disposed | agent/error | agent/status | hook/failed'),
|
|
38
|
+
on: Schema.union([...HOOK_EVENTS]).description('触发事件:turn/start | turn/end | tree/settled | step/end | tool/call | tool/result | user/message | approval/asked | approval/decided | session/title | session/created | session/disposed | agent/created | agent/disposed | agent/error | agent/status | hook/failed | usage/daily'),
|
|
38
39
|
when: Schema.union([...TURN_END_REASONS]).description('可选过滤:对 turn/end 匹配结束原因(completed/error/aborted/blocked/max-tokens/interrupted);其他事件忽略该字段'),
|
|
39
40
|
match: Schema.dict(Schema.union([
|
|
40
41
|
Schema.regExp(),
|
|
@@ -63,7 +64,7 @@ export const Config = Schema.object({
|
|
|
63
64
|
.default('env')
|
|
64
65
|
.description('上下文传递方式:env 只传 DSH_HOOK_* 环境变量(默认);stdin 额外把完整上下文 JSON 写入命令标准输入'),
|
|
65
66
|
timeoutMs: Schema.number().default(10000).description('单次执行超时(毫秒)'),
|
|
66
|
-
retries: Schema.natural().default(0).description('
|
|
67
|
+
retries: Schema.natural().default(0).description('重试次数(默认 0 不重试):run 重试非零退出码(spawn 失败与超时不重试);notify 的 webhook 渠道重试传输失败与 HTTP 408/429/5xx(desktop 渠道不重试)'),
|
|
67
68
|
retryDelayMs: Schema.natural().default(500).description('重试基础间隔(毫秒),每次翻倍'),
|
|
68
69
|
enabled: Schema.boolean()
|
|
69
70
|
.default(true)
|
package/lib/context.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface HookContext {
|
|
|
35
35
|
* pairing tool/call was never seen, e.g. after a plugin restart).
|
|
36
36
|
*/
|
|
37
37
|
toolDurationMs?: number;
|
|
38
|
-
/** Aggregated token usage of the turn (turn/end),
|
|
38
|
+
/** Aggregated token usage of the turn (turn/end), or of the day (usage/daily). */
|
|
39
39
|
usageInputTokens?: number;
|
|
40
40
|
usageOutputTokens?: number;
|
|
41
41
|
usageCacheReadTokens?: number;
|
|
@@ -69,6 +69,15 @@ export interface HookContext {
|
|
|
69
69
|
hookFailedHook?: string;
|
|
70
70
|
/** Consecutive failure count when the alert fired (hook/failed). */
|
|
71
71
|
hookFailures?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Local calendar day (`YYYY-MM-DD`) the token totals below cover
|
|
74
|
+
* (usage/daily only; turn/end carries a single turn, not a day).
|
|
75
|
+
*/
|
|
76
|
+
usageDay?: string;
|
|
77
|
+
/** Turns with reported accounting that day (usage/daily). */
|
|
78
|
+
usageTurns?: number;
|
|
79
|
+
/** Distinct sessions that contributed usage that day (usage/daily). */
|
|
80
|
+
usageSessions?: number;
|
|
72
81
|
timestamp: string;
|
|
73
82
|
}
|
|
74
83
|
export declare function toEnv(ctx: HookContext): Record<string, string>;
|
package/lib/context.js
CHANGED
|
@@ -71,6 +71,12 @@ export function toEnv(ctx) {
|
|
|
71
71
|
env.DSH_HOOK_FAILED_HOOK = ctx.hookFailedHook;
|
|
72
72
|
if (ctx.hookFailures !== undefined)
|
|
73
73
|
env.DSH_HOOK_FAILURES = String(ctx.hookFailures);
|
|
74
|
+
if (ctx.usageDay !== undefined)
|
|
75
|
+
env.DSH_HOOK_USAGE_DAY = ctx.usageDay;
|
|
76
|
+
if (ctx.usageTurns !== undefined)
|
|
77
|
+
env.DSH_HOOK_USAGE_TURNS = String(ctx.usageTurns);
|
|
78
|
+
if (ctx.usageSessions !== undefined)
|
|
79
|
+
env.DSH_HOOK_USAGE_SESSIONS = String(ctx.usageSessions);
|
|
74
80
|
return env;
|
|
75
81
|
}
|
|
76
82
|
/** Render `{{DSH_HOOK_*}}` placeholders from the context map. */
|
package/lib/dry-run.d.ts
CHANGED
|
@@ -13,6 +13,33 @@ export declare function loadHooks(profile: string, paths?: {
|
|
|
13
13
|
hooks: HookSpec[];
|
|
14
14
|
source: string;
|
|
15
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the JSONL path a profile's dsh-hooks config writes history to
|
|
18
|
+
* (`config.history.path`, else the plugin default). Deliberately lenient:
|
|
19
|
+
* `tail` must keep working while the config file is missing or mid-edit.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadHistoryPath(profile: string, paths?: {
|
|
22
|
+
patchFile?: string;
|
|
23
|
+
}): string;
|
|
24
|
+
/**
|
|
25
|
+
* Numeric context fields a simulated event may override — the ones a `match`
|
|
26
|
+
* comparison can meaningfully target. Strings keep their dedicated CLI flag /
|
|
27
|
+
* tester input (`--tool`, `--session-name`, …) and the mock defaults.
|
|
28
|
+
*/
|
|
29
|
+
export declare const MOCK_NUMERIC_FIELDS: readonly ['turn', 'step', 'durationMs', 'toolDurationMs', 'runningSubagents', 'totalSubagents', 'treeDurationMs', 'usageTurns', 'usageSessions', 'usageInputTokens', 'usageOutputTokens', 'usageCacheReadTokens', 'usageCacheWriteTokens', 'usageReasoningTokens'];
|
|
30
|
+
export type MockNumericField = (typeof MOCK_NUMERIC_FIELDS)[number];
|
|
31
|
+
export interface MockFieldsResult {
|
|
32
|
+
ctx: HookContext;
|
|
33
|
+
/** Keys that were dropped: unknown field names or non-finite numbers. */
|
|
34
|
+
ignored: string[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Apply explicit numeric overrides to a simulated context. Values must be
|
|
38
|
+
* finite numbers; anything else (unknown field, string, NaN) is reported in
|
|
39
|
+
* `ignored` instead of being silently coerced — a tester must never "pass"
|
|
40
|
+
* because a filter was fed the wrong type.
|
|
41
|
+
*/
|
|
42
|
+
export declare function applyMockFields(ctx: HookContext, fields: Record<string, unknown> | undefined): MockFieldsResult;
|
|
16
43
|
/** A synthetic context for the simulated event, overridable per field. */
|
|
17
44
|
export declare function mockContext(event: string, overrides?: Partial<HookContext>): HookContext;
|
|
18
45
|
export interface DryRunLine {
|
|
@@ -34,6 +61,8 @@ export interface DryRunOptions {
|
|
|
34
61
|
reason?: TurnEndReasonKind;
|
|
35
62
|
tool?: string;
|
|
36
63
|
sessionName?: string;
|
|
64
|
+
/** Explicit numeric context overrides (see {@link MOCK_NUMERIC_FIELDS}). */
|
|
65
|
+
fields?: Record<string, unknown>;
|
|
37
66
|
/** Actually run the matching hooks (real side effects!). */
|
|
38
67
|
execute?: boolean;
|
|
39
68
|
print?: (line: string) => void;
|