dsh-hooks 0.10.0 → 0.12.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 +107 -10
- package/README.zh.md +107 -10
- package/lib/client.js +87 -6
- package/lib/config.d.ts +46 -7
- package/lib/config.js +18 -2
- package/lib/context.d.ts +15 -1
- package/lib/context.js +8 -0
- package/lib/dry-run.js +35 -6
- package/lib/events.d.ts +21 -15
- package/lib/events.js +100 -15
- package/lib/history.d.ts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +140 -28
- package/lib/patch-config.d.ts +4 -0
- package/lib/patch-config.js +5 -1
- package/lib/runner.d.ts +12 -1
- package/lib/runner.js +42 -4
- package/lib/server.d.ts +5 -1
- package/lib/server.js +20 -1
- package/lib/usage.d.ts +79 -0
- package/lib/usage.js +110 -0
- package/package.json +5 -5
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,9 +35,18 @@ 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
|
-
match: Schema.dict(Schema.
|
|
40
|
+
match: Schema.dict(Schema.union([
|
|
41
|
+
Schema.regExp(),
|
|
42
|
+
Schema.object({
|
|
43
|
+
gt: Schema.number(),
|
|
44
|
+
gte: Schema.number(),
|
|
45
|
+
lt: Schema.number(),
|
|
46
|
+
lte: Schema.number(),
|
|
47
|
+
eq: Schema.number(),
|
|
48
|
+
}).description('数值比较(可组合,全部满足才匹配;要求上下文字段为数字)'),
|
|
49
|
+
])).description('可选通用过滤:字段 → 正则或数值比较,全部匹配才触发。正则匹配字段的字符串表示;数值比较支持对象语法 { gt: 10000 } 或字符串语法 \'>10000\'(gt/gte/lt/lte/eq),只对数字字段生效,非数字字段永不匹配。字段为上下文键(tool/sessionName/sessionId/error/source/cwd/content/reason/turn/durationMs/runningSubagents/…),上下文中不存在的字段视为不匹配'),
|
|
40
50
|
run: Schema.string().description('触发时通过系统 shell 执行的命令(与 notify 二选一)'),
|
|
41
51
|
notify: Schema.union([
|
|
42
52
|
Schema.object({
|
|
@@ -56,6 +66,12 @@ export const Config = Schema.object({
|
|
|
56
66
|
timeoutMs: Schema.number().default(10000).description('单次执行超时(毫秒)'),
|
|
57
67
|
retries: Schema.natural().default(0).description('非零退出码的重试次数(默认 0 不重试;spawn 失败与超时不重试)'),
|
|
58
68
|
retryDelayMs: Schema.natural().default(500).description('重试基础间隔(毫秒),每次翻倍'),
|
|
69
|
+
enabled: Schema.boolean()
|
|
70
|
+
.default(true)
|
|
71
|
+
.description('停用开关:false 保留配置但跳过派发(静默跳过,不计失败;默认 true)'),
|
|
72
|
+
cwd: Schema.union([Schema.const('session'), Schema.string()]).description('执行工作目录:session 在会话工作目录执行;绝对路径在指定目录执行;缺省用插件进程目录(只作用于 run)'),
|
|
73
|
+
maxConcurrent: Schema.natural().description('该 hook 允许的最大并发进程数;超过上限的触发被丢弃(历史记 skipped)。缺省不限(0 同样视为不限)'),
|
|
74
|
+
debounceMs: Schema.natural().description('去抖窗口(毫秒):高频事件(step/end、tool/* 等)窗口内的多次触发合并为一次 trailing 执行,携带最新上下文。缺省 0 = 不去抖'),
|
|
59
75
|
}).description('一个事件 → 命令/通知的 hook 声明'))
|
|
60
76
|
.default([])
|
|
61
77
|
.description('事件触发时执行的外部命令列表;按声明顺序触发'),
|
package/lib/context.d.ts
CHANGED
|
@@ -30,7 +30,12 @@ export interface HookContext {
|
|
|
30
30
|
error?: string;
|
|
31
31
|
/** Event content snapshot: turn assistant text, tool result text, … */
|
|
32
32
|
content?: string;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Wall-clock tool execution time, ms (tool/result only; undefined when the
|
|
35
|
+
* pairing tool/call was never seen, e.g. after a plugin restart).
|
|
36
|
+
*/
|
|
37
|
+
toolDurationMs?: number;
|
|
38
|
+
/** Aggregated token usage of the turn (turn/end), or of the day (usage/daily). */
|
|
34
39
|
usageInputTokens?: number;
|
|
35
40
|
usageOutputTokens?: number;
|
|
36
41
|
usageCacheReadTokens?: number;
|
|
@@ -64,6 +69,15 @@ export interface HookContext {
|
|
|
64
69
|
hookFailedHook?: string;
|
|
65
70
|
/** Consecutive failure count when the alert fired (hook/failed). */
|
|
66
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;
|
|
67
81
|
timestamp: string;
|
|
68
82
|
}
|
|
69
83
|
export declare function toEnv(ctx: HookContext): Record<string, string>;
|
package/lib/context.js
CHANGED
|
@@ -35,6 +35,8 @@ export function toEnv(ctx) {
|
|
|
35
35
|
env.DSH_HOOK_ERROR = ctx.error;
|
|
36
36
|
if (ctx.content !== undefined)
|
|
37
37
|
env.DSH_HOOK_CONTENT = ctx.content;
|
|
38
|
+
if (ctx.toolDurationMs !== undefined)
|
|
39
|
+
env.DSH_HOOK_TOOL_DURATION_MS = String(ctx.toolDurationMs);
|
|
38
40
|
if (ctx.usageInputTokens !== undefined)
|
|
39
41
|
env.DSH_HOOK_USAGE_INPUT_TOKENS = String(ctx.usageInputTokens);
|
|
40
42
|
if (ctx.usageOutputTokens !== undefined)
|
|
@@ -69,6 +71,12 @@ export function toEnv(ctx) {
|
|
|
69
71
|
env.DSH_HOOK_FAILED_HOOK = ctx.hookFailedHook;
|
|
70
72
|
if (ctx.hookFailures !== undefined)
|
|
71
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);
|
|
72
80
|
return env;
|
|
73
81
|
}
|
|
74
82
|
/** Render `{{DSH_HOOK_*}}` placeholders from the context map. */
|
package/lib/dry-run.js
CHANGED
|
@@ -9,6 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import YAML from 'yaml';
|
|
10
10
|
import { Config } from './config.js';
|
|
11
11
|
import { matchFilters } from './events.js';
|
|
12
|
+
import { localDayKey } from './usage.js';
|
|
12
13
|
import { createHookRunner } from './runner.js';
|
|
13
14
|
import { fireNotify } from './notify.js';
|
|
14
15
|
/** Profile patch file for a profile name. */
|
|
@@ -48,7 +49,7 @@ export function loadHooks(profile, paths = {}) {
|
|
|
48
49
|
}
|
|
49
50
|
/** A synthetic context for the simulated event, overridable per field. */
|
|
50
51
|
export function mockContext(event, overrides = {}) {
|
|
51
|
-
|
|
52
|
+
const ctx = {
|
|
52
53
|
event,
|
|
53
54
|
sessionId: 'dry-run',
|
|
54
55
|
sessionName: 'dry-run 会话',
|
|
@@ -59,25 +60,53 @@ export function mockContext(event, overrides = {}) {
|
|
|
59
60
|
callId: 'dry-run-call',
|
|
60
61
|
content: 'dry-run 模拟内容',
|
|
61
62
|
timestamp: new Date().toISOString(),
|
|
62
|
-
...overrides,
|
|
63
63
|
};
|
|
64
|
+
if (event === 'usage/daily') {
|
|
65
|
+
// A daily report always describes a day that already ended, and the
|
|
66
|
+
// simulated numbers must be non-zero so `match` filters on them (e.g.
|
|
67
|
+
// `{ usageInputTokens: '>0' }`) are actually exercisable.
|
|
68
|
+
ctx.usageDay = localDayKey(new Date(Date.now() - 86_400_000));
|
|
69
|
+
ctx.usageTurns = 12;
|
|
70
|
+
ctx.usageSessions = 3;
|
|
71
|
+
ctx.usageInputTokens = 120_000;
|
|
72
|
+
ctx.usageOutputTokens = 45_000;
|
|
73
|
+
ctx.usageCacheReadTokens = 90_000;
|
|
74
|
+
ctx.usageCacheWriteTokens = 6_000;
|
|
75
|
+
ctx.usageReasoningTokens = 8_000;
|
|
76
|
+
}
|
|
77
|
+
return { ...ctx, ...overrides };
|
|
78
|
+
}
|
|
79
|
+
/** Render a match value (regex source, comparison op, or object form). */
|
|
80
|
+
function matchText(value) {
|
|
81
|
+
if (value instanceof RegExp)
|
|
82
|
+
return value.source;
|
|
83
|
+
return JSON.stringify(value);
|
|
64
84
|
}
|
|
65
85
|
/** One-line hook description for report rows. */
|
|
66
86
|
export function describeHook(hook) {
|
|
67
87
|
const when = hook.when ? ` when=${hook.when}` : '';
|
|
68
88
|
const match = hook.match && Object.keys(hook.match).length > 0
|
|
69
|
-
? ` match=${JSON.stringify(Object.fromEntries(Object.entries(hook.match).map(([key, re]) => [key, re
|
|
89
|
+
? ` match=${JSON.stringify(Object.fromEntries(Object.entries(hook.match).map(([key, re]) => [key, matchText(re)])))},`
|
|
70
90
|
: '';
|
|
91
|
+
const options = [
|
|
92
|
+
hook.enabled === false ? ' enabled:false' : '',
|
|
93
|
+
hook.cwd !== undefined ? ` cwd:${hook.cwd}` : '',
|
|
94
|
+
hook.maxConcurrent !== undefined && hook.maxConcurrent > 0 ? ` maxConcurrent:${hook.maxConcurrent}` : '',
|
|
95
|
+
hook.debounceMs !== undefined && hook.debounceMs > 0 ? ` debounceMs:${hook.debounceMs}` : '',
|
|
96
|
+
].join('');
|
|
71
97
|
if (hook.run)
|
|
72
|
-
return `[${hook.on}${when}]${match} run: ${hook.run}`;
|
|
98
|
+
return `[${hook.on}${when}]${match} run: ${hook.run}${options}`;
|
|
73
99
|
if (hook.notify)
|
|
74
|
-
return `[${hook.on}${when}]${match} notify: ${hook.notify.channel}${hook.notify.url ? ` ${hook.notify.url}` : ''}`;
|
|
75
|
-
return `[${hook.on}${when}]${match} (既无 run 也无 notify)`;
|
|
100
|
+
return `[${hook.on}${when}]${match} notify: ${hook.notify.channel}${hook.notify.url ? ` ${hook.notify.url}` : ''}${options}`;
|
|
101
|
+
return `[${hook.on}${when}]${match} (既无 run 也无 notify)${options}`;
|
|
76
102
|
}
|
|
77
103
|
/** Evaluate every hook against the simulated event/context. */
|
|
78
104
|
export function evaluateHooks(hooks, event, ctx, reasonKind) {
|
|
79
105
|
return hooks.map((hook, index) => {
|
|
80
106
|
const summary = describeHook(hook);
|
|
107
|
+
if (hook.enabled === false) {
|
|
108
|
+
return { index: index + 1, matched: false, why: 'enabled: false(已停用)', summary };
|
|
109
|
+
}
|
|
81
110
|
if (hook.on !== event) {
|
|
82
111
|
return { index: index + 1, matched: false, why: `事件不匹配(${hook.on} ≠ ${event})`, summary };
|
|
83
112
|
}
|
package/lib/events.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Session, SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session';
|
|
2
2
|
import type { HookContext } from './context.js';
|
|
3
|
-
import type { HookSpec, TurnEndReasonKind } from './config.js';
|
|
3
|
+
import type { HookSpec, NumericMatch, TurnEndReasonKind } from './config.js';
|
|
4
|
+
import type { DailyUsageTotals, UsageTotals } from './usage.js';
|
|
4
5
|
import type { AgentLike } from './types.js';
|
|
6
|
+
export type { UsageTotals } from './usage.js';
|
|
5
7
|
/** `approval/asked` payload (merge-extensible, declared by dsh-user-approval). */
|
|
6
8
|
export interface ApprovalAskedData {
|
|
7
9
|
id: string;
|
|
@@ -65,14 +67,6 @@ export declare function sessionTitle(session: Session): string | undefined;
|
|
|
65
67
|
* their own display truncation.
|
|
66
68
|
*/
|
|
67
69
|
export declare function turnContent(session: Session, turn: number): string | undefined;
|
|
68
|
-
/** Aggregated turn usage for hook contexts (only fields actually reported). */
|
|
69
|
-
export interface UsageTotals {
|
|
70
|
-
inputTokens: number;
|
|
71
|
-
outputTokens: number;
|
|
72
|
-
cacheReadTokens?: number;
|
|
73
|
-
cacheWriteTokens?: number;
|
|
74
|
-
reasoningTokens?: number;
|
|
75
|
-
}
|
|
76
70
|
/**
|
|
77
71
|
* Sum the `usage` of every `assistant/message` of a turn. Steps without
|
|
78
72
|
* reported accounting are skipped; returns undefined when no step reported
|
|
@@ -84,13 +78,15 @@ export declare function clearTurnTracking(session: Session): void;
|
|
|
84
78
|
/** Does a declared hook match this event (type + optional `when` filter)? */
|
|
85
79
|
export declare function hookMatches(spec: HookSpec, event: string, reasonKind?: TurnEndReasonKind): boolean;
|
|
86
80
|
/**
|
|
87
|
-
* Apply the optional `match` field →
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
81
|
+
* Apply the optional `match` field → filter map. Each value is either a
|
|
82
|
+
* regex (compiled by the config schema; tested against the String-coerced
|
|
83
|
+
* field) or a numeric comparison — declared as an object (`{ gt: 10000 }`)
|
|
84
|
+
* or as a string that parses as one (`'>10000'`). Comparison semantics
|
|
85
|
+
* apply only when the context field is a number; on a non-numeric field a
|
|
86
|
+
* comparison never matches. Every declared filter must pass. An empty or
|
|
87
|
+
* absent `match` passes everything; unsupported shapes never match.
|
|
92
88
|
*/
|
|
93
|
-
export declare function matchFilters(match: Record<string, RegExp> | undefined, ctx: HookContext): boolean;
|
|
89
|
+
export declare function matchFilters(match: Record<string, RegExp | NumericMatch> | undefined, ctx: HookContext): boolean;
|
|
94
90
|
export declare function turnEndContext(session: Session, turn: number, reason: TurnEndReason | string): HookContext;
|
|
95
91
|
export declare function turnStartContext(session: Session, turn: number): HookContext;
|
|
96
92
|
export declare function stepEndContext(session: Session, turn: number, step: number): HookContext;
|
|
@@ -126,6 +122,16 @@ export declare function treeSettledContext(session: Session, totalSubagents: num
|
|
|
126
122
|
* session identity of the event that triggered the failing hook.
|
|
127
123
|
*/
|
|
128
124
|
export declare function hookFailedContext(origin: HookContext, hookFailedHook: string, hookFailures: number): HookContext;
|
|
125
|
+
/**
|
|
126
|
+
* Synthetic `usage/daily` context: the local calendar day that just ended,
|
|
127
|
+
* with its aggregated token usage. Emitted by index.ts when the day rolls
|
|
128
|
+
* over (detected from ordinary event traffic — no timers); `origin` supplies
|
|
129
|
+
* the session identity of the event that triggered the report.
|
|
130
|
+
*
|
|
131
|
+
* The token fields reuse the `turn/end` names on purpose: a hook reads the
|
|
132
|
+
* same variables, with the day's aggregate instead of one turn's.
|
|
133
|
+
*/
|
|
134
|
+
export declare function usageDailyContext(origin: HookContext, totals: DailyUsageTotals): HookContext;
|
|
129
135
|
export declare function agentCreatedContext(agent: AgentLike): HookContext;
|
|
130
136
|
export declare function agentDisposedContext(agent: AgentLike): HookContext;
|
|
131
137
|
export declare function agentErrorContext(agent: AgentLike, turn: number | undefined, error: unknown): HookContext;
|
package/lib/events.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Per-session turn start timestamps for duration reporting. */
|
|
2
2
|
const turnStarts = new Map();
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Tool name + start timestamp for an in-flight call, remembered at
|
|
5
|
+
* `tool/call` and consumed at `tool/result` (name back-fill + duration).
|
|
6
|
+
*/
|
|
4
7
|
const callTools = new Map();
|
|
5
8
|
/** Approval identity remembered at `approval/asked` and consumed at `approval/decided`. */
|
|
6
9
|
const approvalTools = new Map();
|
|
@@ -139,24 +142,76 @@ export function hookMatches(spec, event, reasonKind) {
|
|
|
139
142
|
return true;
|
|
140
143
|
return spec.when === reasonKind;
|
|
141
144
|
}
|
|
145
|
+
/** Comparison-prefixed string syntax: `'>10000'`, `'>=5'`, `'<2'`, `'<=9'`, `'=42'`. */
|
|
146
|
+
const COMPARE_STRING = /^([<>]=?|=)\s*(-?\d+(?:\.\d+)?)$/;
|
|
147
|
+
const OPERATOR_SYMBOLS = {
|
|
148
|
+
'>': 'gt',
|
|
149
|
+
'>=': 'gte',
|
|
150
|
+
'<': 'lt',
|
|
151
|
+
'<=': 'lte',
|
|
152
|
+
'=': 'eq',
|
|
153
|
+
};
|
|
154
|
+
/** Symbol/field form of a comparison matcher: `{ gt: 10000 }`, `' > 10000'`, … */
|
|
155
|
+
function numericOps(value) {
|
|
156
|
+
if (!(value instanceof RegExp))
|
|
157
|
+
return value;
|
|
158
|
+
const parsed = COMPARE_STRING.exec(value.source);
|
|
159
|
+
if (parsed === null)
|
|
160
|
+
return undefined;
|
|
161
|
+
const op = OPERATOR_SYMBOLS[parsed[1]];
|
|
162
|
+
if (op === undefined)
|
|
163
|
+
return undefined;
|
|
164
|
+
return { [op]: Number(parsed[2]) };
|
|
165
|
+
}
|
|
166
|
+
/** Does a numeric context field satisfy every declared comparison op? */
|
|
167
|
+
function compareNumber(n, ops) {
|
|
168
|
+
if (ops.gt !== undefined && !(n > ops.gt))
|
|
169
|
+
return false;
|
|
170
|
+
if (ops.gte !== undefined && !(n >= ops.gte))
|
|
171
|
+
return false;
|
|
172
|
+
if (ops.lt !== undefined && !(n < ops.lt))
|
|
173
|
+
return false;
|
|
174
|
+
if (ops.lte !== undefined && !(n <= ops.lte))
|
|
175
|
+
return false;
|
|
176
|
+
if (ops.eq !== undefined && !(n === ops.eq))
|
|
177
|
+
return false;
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
142
180
|
/**
|
|
143
|
-
* Apply the optional `match` field →
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
181
|
+
* Apply the optional `match` field → filter map. Each value is either a
|
|
182
|
+
* regex (compiled by the config schema; tested against the String-coerced
|
|
183
|
+
* field) or a numeric comparison — declared as an object (`{ gt: 10000 }`)
|
|
184
|
+
* or as a string that parses as one (`'>10000'`). Comparison semantics
|
|
185
|
+
* apply only when the context field is a number; on a non-numeric field a
|
|
186
|
+
* comparison never matches. Every declared filter must pass. An empty or
|
|
187
|
+
* absent `match` passes everything; unsupported shapes never match.
|
|
148
188
|
*/
|
|
149
189
|
export function matchFilters(match, ctx) {
|
|
150
190
|
if (match === undefined)
|
|
151
191
|
return true;
|
|
152
192
|
for (const [field, pattern] of Object.entries(match)) {
|
|
153
|
-
if (!(pattern instanceof RegExp))
|
|
154
|
-
return false;
|
|
155
193
|
const value = ctx[field];
|
|
156
194
|
if (value === undefined)
|
|
157
195
|
return false;
|
|
158
|
-
if (
|
|
159
|
-
|
|
196
|
+
if (pattern instanceof RegExp) {
|
|
197
|
+
const ops = numericOps(pattern);
|
|
198
|
+
if (ops !== undefined) {
|
|
199
|
+
// Comparison syntax: numbers only, never coerced strings.
|
|
200
|
+
if (typeof value !== 'number' || !compareNumber(value, ops))
|
|
201
|
+
return false;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (!pattern.test(String(value)))
|
|
205
|
+
return false;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Object form `{ gt: … }` comes pre-typed from the schema.
|
|
209
|
+
if (typeof pattern === 'object' && pattern !== null) {
|
|
210
|
+
if (typeof value !== 'number' || !compareNumber(value, pattern))
|
|
211
|
+
return false;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
160
215
|
}
|
|
161
216
|
return true;
|
|
162
217
|
}
|
|
@@ -218,20 +273,21 @@ export function stepEndContext(session, turn, step) {
|
|
|
218
273
|
}
|
|
219
274
|
export function toolCallContext(session, turn, step, callId, name, args) {
|
|
220
275
|
const key = callKey(session, callId);
|
|
221
|
-
|
|
276
|
+
const tool = typeof name === 'string' ? name : String(name);
|
|
277
|
+
callTools.set(key, { tool, startedAt: Date.now() });
|
|
222
278
|
return {
|
|
223
279
|
...baseContext(session, 'tool/call'),
|
|
224
280
|
turn,
|
|
225
281
|
step,
|
|
226
|
-
tool
|
|
282
|
+
tool,
|
|
227
283
|
callId: String(callId),
|
|
228
284
|
toolArgs: typeof args === 'string' ? args.slice(0, 4000) : undefined,
|
|
229
285
|
};
|
|
230
286
|
}
|
|
231
287
|
export function toolResultContext(session, turn, step, callId, message, error) {
|
|
232
288
|
const key = callKey(session, callId);
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
289
|
+
const paired = callTools.get(key);
|
|
290
|
+
if (paired !== undefined)
|
|
235
291
|
callTools.delete(key);
|
|
236
292
|
let toolError;
|
|
237
293
|
if (error !== undefined) {
|
|
@@ -245,8 +301,9 @@ export function toolResultContext(session, turn, step, callId, message, error) {
|
|
|
245
301
|
...baseContext(session, 'tool/result'),
|
|
246
302
|
turn,
|
|
247
303
|
step,
|
|
248
|
-
tool,
|
|
304
|
+
tool: paired?.tool,
|
|
249
305
|
callId: String(callId),
|
|
306
|
+
toolDurationMs: paired === undefined ? undefined : Date.now() - paired.startedAt,
|
|
250
307
|
toolError,
|
|
251
308
|
content: content === undefined ? undefined : content.slice(0, 4000),
|
|
252
309
|
};
|
|
@@ -346,6 +403,34 @@ export function hookFailedContext(origin, hookFailedHook, hookFailures) {
|
|
|
346
403
|
timestamp: new Date().toISOString(),
|
|
347
404
|
};
|
|
348
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Synthetic `usage/daily` context: the local calendar day that just ended,
|
|
408
|
+
* with its aggregated token usage. Emitted by index.ts when the day rolls
|
|
409
|
+
* over (detected from ordinary event traffic — no timers); `origin` supplies
|
|
410
|
+
* the session identity of the event that triggered the report.
|
|
411
|
+
*
|
|
412
|
+
* The token fields reuse the `turn/end` names on purpose: a hook reads the
|
|
413
|
+
* same variables, with the day's aggregate instead of one turn's.
|
|
414
|
+
*/
|
|
415
|
+
export function usageDailyContext(origin, totals) {
|
|
416
|
+
return {
|
|
417
|
+
event: 'usage/daily',
|
|
418
|
+
sessionId: origin.sessionId,
|
|
419
|
+
sessionName: origin.sessionName,
|
|
420
|
+
cwd: origin.cwd,
|
|
421
|
+
usageDay: totals.day,
|
|
422
|
+
usageTurns: totals.turns,
|
|
423
|
+
usageSessions: totals.sessions,
|
|
424
|
+
usageInputTokens: totals.inputTokens,
|
|
425
|
+
usageOutputTokens: totals.outputTokens,
|
|
426
|
+
// Only fields some turn actually reported: an absent variable is easier to
|
|
427
|
+
// reason about (and to match on) than one that is present-but-undefined.
|
|
428
|
+
...(totals.cacheReadTokens !== undefined ? { usageCacheReadTokens: totals.cacheReadTokens } : {}),
|
|
429
|
+
...(totals.cacheWriteTokens !== undefined ? { usageCacheWriteTokens: totals.cacheWriteTokens } : {}),
|
|
430
|
+
...(totals.reasoningTokens !== undefined ? { usageReasoningTokens: totals.reasoningTokens } : {}),
|
|
431
|
+
timestamp: new Date().toISOString(),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
349
434
|
export function agentCreatedContext(agent) {
|
|
350
435
|
return {
|
|
351
436
|
event: 'agent/created',
|
package/lib/history.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface HookRunRecord {
|
|
|
9
9
|
command: string;
|
|
10
10
|
sessionId?: string;
|
|
11
11
|
sessionName?: string;
|
|
12
|
-
outcome: 'spawned' | 'spawn-failed' | 'timeout' | 'exit-0' | 'exit-nonzero' | 'sent' | 'send-failed';
|
|
12
|
+
outcome: 'spawned' | 'spawn-failed' | 'timeout' | 'exit-0' | 'exit-nonzero' | 'skipped' | 'sent' | 'send-failed';
|
|
13
13
|
exitCode?: number;
|
|
14
14
|
durationMs?: number;
|
|
15
15
|
/** stderr tail or error message. */
|
package/lib/index.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export { createHistorySink } from './history.js';
|
|
|
59
59
|
* Model-facing announcement, installed only when the system-prompt service
|
|
60
60
|
* exists (web profile). Tells agents the plugin exists and how to cooperate.
|
|
61
61
|
*/
|
|
62
|
-
export declare const DSH_HOOKS_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-hooks \u63D2\u4EF6\uFF08DeepSeek Harness \u914D\u7F6E\u9A71\u52A8\u751F\u547D\u5468\u671F hooks\uFF09\uFF1A\u53EF\u5728 profile \u7684 cordis.patch.yml \u58F0\u660E\u300C\u4E8B\u4EF6 \u2192 \u547D\u4EE4/\u901A\u77E5\u300D\u7684 hook\uFF08turn/start\u3001turn/end\u3001tree/settled\u3001step/end\u3001tool/call\u3001tool/result\u3001user/message\u3001approval/asked\u3001approval/decided\u3001session/title\u3001session/created\u3001session/disposed\u3001agent/created\u3001agent/disposed\u3001agent/error\u3001agent/status\u3001hook/failed \u5171
|
|
62
|
+
export declare const DSH_HOOKS_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-hooks \u63D2\u4EF6\uFF08DeepSeek Harness \u914D\u7F6E\u9A71\u52A8\u751F\u547D\u5468\u671F hooks\uFF09\uFF1A\u53EF\u5728 profile \u7684 cordis.patch.yml \u58F0\u660E\u300C\u4E8B\u4EF6 \u2192 \u547D\u4EE4/\u901A\u77E5\u300D\u7684 hook\uFF08turn/start\u3001turn/end\u3001tree/settled\u3001step/end\u3001tool/call\u3001tool/result\u3001user/message\u3001approval/asked\u3001approval/decided\u3001session/title\u3001session/created\u3001session/disposed\u3001agent/created\u3001agent/disposed\u3001agent/error\u3001agent/status\u3001hook/failed\u3001usage/daily \u5171 18 \u7C7B\u4E8B\u4EF6\uFF09\uFF0C\u652F\u6301 when \u539F\u56E0\u8FC7\u6EE4\u3001match \u5B57\u6BB5\u6B63\u5219/\u6570\u503C\u6BD4\u8F83\u8FC7\u6EE4\uFF08\u5982 '>10000'\uFF09\u3001stdin JSON \u8F93\u5165\u3001opt-in \u91CD\u8BD5\u3001\u6267\u884C\u9009\u9879\uFF08enabled \u505C\u7528 / cwd \u5DE5\u4F5C\u76EE\u5F55 / maxConcurrent + debounceMs \u9632\u9AD8\u9891\u98CE\u66B4\uFF09\u3001\u5185\u7F6E webhook/desktop \u901A\u77E5\u6E20\u9053\uFF1B\u6267\u884C\u5386\u53F2\u8BB0\u5F55\u4E8E ~/.dsh/dsh-hooks/history.jsonl\uFF1B`dsh-hooks dry-run <event>` \u53EF\u6A21\u62DF\u4E8B\u4EF6\u9A8C\u8BC1\u914D\u7F6E\u3002\u7528\u6237\u63D0\u5230\u300Chooks / \u94A9\u5B50 / \u751F\u547D\u5468\u671F / \u901A\u77E5\u914D\u7F6E\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
|
|
63
63
|
export declare function apply(ctx: Context, config?: Config): void;
|
|
64
64
|
export declare const _internals: {
|
|
65
65
|
clearTurnTracking: typeof clearTurnTracking;
|