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/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.regExp()).description('可选通用过滤:字段 → 正则,全部匹配才触发。字段为上下文键(tool/sessionName/sessionId/error/source/cwd/content/reason/…),上下文中不存在的字段视为不匹配'),
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
- /** Aggregated token usage of the turn (turn/end), when reported. */
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
- return {
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.source])))},`
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 → regex filters. Every declared regex
88
- * must match its context field (String-coerced); a field the context does
89
- * not carry never matches. An empty/absent `match` passes everything.
90
- * RegExps come pre-compiled from the config schema; non-RegExp entries are
91
- * rejected defensively (never match).
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
- /** Tool name for an in-flight call, remembered at `tool/call` and consumed at `tool/result`. */
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 → regex filters. Every declared regex
144
- * must match its context field (String-coerced); a field the context does
145
- * not carry never matches. An empty/absent `match` passes everything.
146
- * RegExps come pre-compiled from the config schema; non-RegExp entries are
147
- * rejected defensively (never match).
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 (!pattern.test(String(value)))
159
- return false;
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
- callTools.set(key, typeof name === 'string' ? name : String(name));
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: typeof name === 'string' ? name : String(name),
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 tool = callTools.get(key);
234
- if (tool !== undefined)
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 17 \u7C7B\u4E8B\u4EF6\uFF09\uFF0C\u652F\u6301 when \u539F\u56E0\u8FC7\u6EE4\u3001match \u5B57\u6BB5\u6B63\u5219\u8FC7\u6EE4\u3001stdin JSON \u8F93\u5165\u3001opt-in \u91CD\u8BD5\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";
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;