@shidesheng0218/agentguard 0.8.0 → 0.9.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/dist/cli.js CHANGED
@@ -1,7 +1,55 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ HOOKS_STOP_BLOCK_REASON,
4
+ WIRE_VERIFY_CORRECTIVE,
5
+ analyzeCall,
6
+ blockKindStats,
7
+ buildStatus,
8
+ callsSince,
9
+ captureCheckpoint,
10
+ castVetoVote,
11
+ claudeDetected,
12
+ claudeHooksInstalled,
13
+ claudeSettingsPath,
14
+ codexDetected,
15
+ codexHooksPath,
16
+ collectVetoContext,
17
+ countBlocks,
18
+ countEvents,
19
+ detectKimiConfig,
20
+ findClaims,
21
+ fingerprint,
22
+ getMeta,
23
+ guardHome,
24
+ hasEvidence,
25
+ hasRecentEdits,
26
+ hashOutput,
27
+ installClaudeHooks,
28
+ knownSessions,
29
+ latestCheckpointFile,
30
+ latestSessionId,
31
+ listBlocks,
32
+ loadConfig,
33
+ normalizeCall,
34
+ oldestEventTs,
35
+ openDb,
36
+ outputSampleOf,
37
+ probeLogPath,
38
+ recordBlock,
39
+ recordCall,
40
+ recordEvent,
41
+ renderResumeBlock,
42
+ setBlockFeedback,
43
+ setMeta,
44
+ stateDbPath,
45
+ uninstallClaudeHooks,
46
+ userConfigPath,
47
+ vetoKeyConfigured,
48
+ writeConfigTemplate
49
+ } from "./chunk-SACTXNYK.js";
2
50
 
3
51
  // src/cli.ts
4
- import fs10 from "fs";
52
+ import fs6 from "fs";
5
53
  import { Command } from "commander";
6
54
 
7
55
  // src/version.ts
@@ -10,395 +58,9 @@ var require2 = createRequire(import.meta.url);
10
58
  var pkg = require2("../package.json");
11
59
  var version = pkg.version;
12
60
 
13
- // src/config.ts
14
- import fs2 from "fs";
15
- import { parse as parseToml } from "smol-toml";
16
-
17
- // src/paths.ts
61
+ // src/installer.ts
18
62
  import fs from "fs";
19
- import os from "os";
20
63
  import path from "path";
21
- function guardHome() {
22
- const env = process.env.AGENT_GUARD_HOME ?? process.env.KIMI_GUARD_HOME;
23
- if (env && env.trim()) return path.resolve(env);
24
- const fresh = path.join(os.homedir(), ".agent-guard");
25
- const legacy = path.join(os.homedir(), ".kimi-guard");
26
- if (!fs.existsSync(fresh) && fs.existsSync(legacy)) return legacy;
27
- return fresh;
28
- }
29
- function claudeSettingsPath() {
30
- const env = process.env.CLAUDE_SETTINGS_PATH;
31
- if (env && env.trim()) return path.resolve(env);
32
- return path.join(os.homedir(), ".claude", "settings.json");
33
- }
34
- function claudeDetected() {
35
- return fs.existsSync(claudeSettingsPath()) || fs.existsSync(path.join(os.homedir(), ".claude"));
36
- }
37
- function stateDbPath() {
38
- return path.join(guardHome(), "state.db");
39
- }
40
- function probeLogPath() {
41
- return path.join(guardHome(), "probe.jsonl");
42
- }
43
- function userConfigPath() {
44
- return path.join(guardHome(), "config.toml");
45
- }
46
- function detectKimiConfig() {
47
- const env = process.env.KIMI_CONFIG_PATH;
48
- if (env && env.trim()) {
49
- const p = path.resolve(env);
50
- return { path: p, exists: fs.existsSync(p) };
51
- }
52
- const candidates = [
53
- path.join(os.homedir(), ".kimi-code", "config.toml"),
54
- path.join(os.homedir(), ".kimi", "config.toml")
55
- ];
56
- for (const p of candidates) {
57
- if (fs.existsSync(p)) return { path: p, exists: true };
58
- }
59
- return { path: candidates[0], exists: false };
60
- }
61
-
62
- // src/toolsets.ts
63
- var DEFAULT_EDIT_TOOLS = ["WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit", "NotebookEdit"];
64
- var DEFAULT_READ_TOOLS = ["ReadFile", "Read"];
65
- var DEFAULT_SEARCH_TOOLS = ["Grep", "Glob"];
66
- var DEFAULT_SHELL_TOOLS = ["Shell", "Bash"];
67
- var CLAUDE_EDIT_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
68
- var CLAUDE_READ_TOOLS = ["Read"];
69
- var CLAUDE_SEARCH_TOOLS = ["Grep", "Glob"];
70
- var CLAUDE_SHELL_TOOLS = ["Bash"];
71
- function toolDefaultsFor(harness) {
72
- return harness === "claude" ? { edit: [...CLAUDE_EDIT_TOOLS], read: [...CLAUDE_READ_TOOLS], search: [...CLAUDE_SEARCH_TOOLS], shell: [...CLAUDE_SHELL_TOOLS] } : { edit: [...DEFAULT_EDIT_TOOLS], read: [...DEFAULT_READ_TOOLS], search: [...DEFAULT_SEARCH_TOOLS], shell: [...DEFAULT_SHELL_TOOLS] };
73
- }
74
- function resolve(list, fallback) {
75
- return new Set(list.length > 0 ? list : fallback);
76
- }
77
- function editTools(cfg) {
78
- return resolve(cfg.tools.edit, DEFAULT_EDIT_TOOLS);
79
- }
80
- function readTools(cfg) {
81
- return resolve(cfg.tools.read, DEFAULT_READ_TOOLS);
82
- }
83
- function searchTools(cfg) {
84
- return resolve(cfg.tools.search, DEFAULT_SEARCH_TOOLS);
85
- }
86
- function shellTools(cfg) {
87
- return resolve(cfg.tools.shell, DEFAULT_SHELL_TOOLS);
88
- }
89
-
90
- // src/config.ts
91
- var defaultConfig = {
92
- harness: "kimi",
93
- tools: toolDefaultsFor("kimi"),
94
- repeat: {
95
- enabled: true,
96
- maxRepeats: 3,
97
- warnAt: 2,
98
- windowMinutes: 30,
99
- watch: ["Grep", "Glob", "Shell", "Bash", "FetchURL", "SearchWeb", "ReadFile"],
100
- thresholds: { ReadFile: 5 },
101
- exemptPatterns: []
102
- },
103
- cycle: { enabled: true, windowMinutes: 30 },
104
- noProgress: { enabled: true, windowMinutes: 30, warnAt: 15, blockAt: 25 },
105
- nearRepeat: { enabled: true, windowMinutes: 30, warnAt: 6, blockAt: 10 },
106
- explore: { enabled: true, windowMinutes: 30, warnAt: 10, blockAt: 15 },
107
- verify: {
108
- enabled: true,
109
- blockOnNoEvidence: false,
110
- evidenceWindowMinutes: 60,
111
- claimPatterns: [],
112
- evidencePatterns: [],
113
- shellTools: ["Shell", "Bash"],
114
- veto: {
115
- enabled: false,
116
- model: "kimi-k3",
117
- baseUrl: "https://api.moonshot.cn/v1",
118
- maxCallsPerSession: 3,
119
- timeoutMs: 1e4
120
- }
121
- },
122
- thinking: { enabled: true, minThinkChars: 2e4, maxTextRatio: 0.1 },
123
- anchor: { enabled: true, everyNPrompts: 5, maxChars: 1e3 },
124
- context: { enabled: true, warnPercent: 85 },
125
- noGain: { enabled: true, windowMinutes: 30, warnAt: 3, blockAt: 4 },
126
- churn: {
127
- enabled: true,
128
- windowMinutes: 30,
129
- warnAt: 5,
130
- blockAt: 10,
131
- tools: ["WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit", "NotebookEdit"]
132
- },
133
- policy: { killSwitch: true, maxBlocksPerSession: 5, blockWindowMinutes: 60 },
134
- budget: {
135
- enabled: true,
136
- plan: "tier1",
137
- weekly: 0,
138
- fiveHour: 0,
139
- dispatchTools: ["Task", "Agent"],
140
- reservePercent: 10,
141
- subagentWeight: 5,
142
- warnPercent: 80,
143
- precise: false,
144
- preciseUrl: "",
145
- preciseCacheSeconds: 300
146
- },
147
- probe: false
148
- };
149
- var CONFIG_TEMPLATE = `# agent-guard configuration
150
- # Docs: https://github.com/shidesheng0218/kimi-guard
151
-
152
- [tools] # canonical tool-name taxonomy \u2014 if your CLI version renames tools, fix it HERE
153
- edit = ["WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit", "NotebookEdit"]
154
- read = ["ReadFile", "Read"]
155
- search = ["Grep", "Glob"]
156
- shell = ["Shell", "Bash"]
157
-
158
- [repeat]
159
- enabled = true
160
- maxRepeats = 3 # identical (tool, args) calls allowed per window
161
- warnAt = 2 # soft context warning before the hard block
162
- windowMinutes = 30
163
- watch = ["Grep", "Glob", "Shell", "Bash", "FetchURL", "SearchWeb", "ReadFile"]
164
- # exemptPatterns = ["git status"] # regexes over JSON-serialized args; matching calls are never repeat-blocked (polling commands like git status, sleep)
165
-
166
- [repeat.thresholds] # per-tool overrides
167
- ReadFile = 5
168
-
169
- [cycle] # A->B->A->B oscillation detection
170
- enabled = true
171
- windowMinutes = 30
172
-
173
- [noProgress] # long stretch of calls with no successful edit
174
- enabled = true
175
- windowMinutes = 30
176
- warnAt = 15
177
- blockAt = 25
178
-
179
- [nearRepeat] # fuzzy near-duplicates (punctuation/case/order differences)
180
- enabled = true
181
- windowMinutes = 30
182
- warnAt = 6
183
- blockAt = 10
184
-
185
- [explore] # pure-exploration streak: reads/searches with no action in between
186
- enabled = true
187
- windowMinutes = 30
188
- warnAt = 10
189
- blockAt = 15
190
-
191
- [verify] # completion-claim gate: "tests pass" must be backed by a real run
192
- enabled = true
193
- blockOnNoEvidence = false # hooks path: block Stop when edits landed but nothing was verified
194
- evidenceWindowMinutes = 60
195
- # deprecated: shell tool names now live in [tools] shell (this key still works)
196
-
197
- [verify.veto] # optional LLM veto vote to suppress false positives (self-critic style)
198
- enabled = false # requires KIMI_GUARD_VETO_API_KEY in the environment
199
- model = "kimi-k3" # use a cheap fast model \u2014 the LLM only votes, never authors
200
- baseUrl = "https://api.moonshot.cn/v1"
201
- maxCallsPerSession = 3 # anti "vote-laundering" cap: the model cannot retry its way out
202
- timeoutMs = 10000
203
-
204
- [thinking] # thinking-dominance (pure-reasoning turns), Wire mode only
205
- enabled = true
206
- minThinkChars = 20000
207
- maxTextRatio = 0.1
208
-
209
- [anchor] # goal anchoring: re-inject the original task periodically
210
- enabled = true
211
- everyNPrompts = 5 # re-inject the goal every N prompts / steps
212
- maxChars = 1000
213
-
214
- [context] # context-fill gate (Wire mode reads StatusUpdate.context_usage)
215
- enabled = true
216
- warnPercent = 85 # steer a wrap-up warning when context is this full
217
-
218
- [noGain] # different args, byte-identical output
219
- enabled = true
220
- windowMinutes = 30
221
- warnAt = 3
222
- blockAt = 4
223
-
224
- [churn] # same file edited over and over
225
- enabled = true
226
- windowMinutes = 30
227
- warnAt = 5
228
- blockAt = 10
229
- # deprecated: edit tool names now live in [tools] edit (this key still works)
230
-
231
- [policy]
232
- killSwitch = true # after maxBlocksPerSession interventions, block ALL tools
233
- maxBlocksPerSession = 5
234
- blockWindowMinutes = 60
235
-
236
- [budget] # request accounting for Kimi Coding Plans
237
- enabled = true
238
- plan = "tier1" # tier1: 1024/week | tier2: 2048 | tier3: 7168 (200 per 5h)
239
- weekly = 0 # override weekly requests (0 = use plan preset)
240
- fiveHour = 0 # override 5h requests (0 = use plan preset)
241
- dispatchTools = ["Task", "Agent"]
242
- reservePercent = 10 # keep this much headroom for you, not the agent
243
- subagentWeight = 5 # ~requests each dispatched subagent costs
244
- warnPercent = 80
245
- precise = false # poll the official Kimi usage API for exact windows (needs KIMI_API_KEY, sk-kimi-...)
246
- preciseUrl = "" # default https://api.kimi.com/coding/v1
247
- preciseCacheSeconds = 300 # the API is rate-limited; cache aggressively. Falls back to event-based on any error
248
-
249
- [probe]
250
- enabled = false
251
- `;
252
- function applyClaudeDefaults(cfg) {
253
- cfg.tools = toolDefaultsFor("claude");
254
- cfg.repeat.watch = ["Grep", "Glob", "Bash", "Read", "WebFetch", "WebSearch"];
255
- cfg.repeat.thresholds = { Read: 5 };
256
- cfg.budget.dispatchTools = ["Task"];
257
- }
258
- function num(v, fallback) {
259
- return typeof v === "number" && Number.isFinite(v) ? v : fallback;
260
- }
261
- function bool(v, fallback) {
262
- return typeof v === "boolean" ? v : fallback;
263
- }
264
- function strArr(v, fallback) {
265
- return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length > 0 ? v : fallback;
266
- }
267
- function strArrOrNull(v) {
268
- return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length > 0 ? v : null;
269
- }
270
- function loadConfig(configPath = userConfigPath(), harness = "kimi") {
271
- const cfg = structuredClone(defaultConfig);
272
- cfg.harness = harness;
273
- if (harness === "claude") applyClaudeDefaults(cfg);
274
- let raw;
275
- try {
276
- raw = fs2.readFileSync(configPath, "utf8");
277
- } catch {
278
- return cfg;
279
- }
280
- let data;
281
- try {
282
- data = parseToml(raw);
283
- } catch (err) {
284
- process.stderr.write(`[agent-guard] failed to parse ${configPath}: ${err.message}
285
- `);
286
- return cfg;
287
- }
288
- const section = (name) => data[name] ?? {};
289
- const tools = section("tools");
290
- const toolsEdit = strArrOrNull(tools["edit"]);
291
- const toolsRead = strArrOrNull(tools["read"]);
292
- const toolsSearch = strArrOrNull(tools["search"]);
293
- const toolsShell = strArrOrNull(tools["shell"]);
294
- if (toolsEdit) cfg.tools.edit = toolsEdit;
295
- if (toolsRead) cfg.tools.read = toolsRead;
296
- if (toolsSearch) cfg.tools.search = toolsSearch;
297
- if (toolsShell) cfg.tools.shell = toolsShell;
298
- const repeat = section("repeat");
299
- cfg.repeat.enabled = bool(repeat["enabled"], cfg.repeat.enabled);
300
- cfg.repeat.maxRepeats = num(repeat["maxRepeats"], cfg.repeat.maxRepeats);
301
- cfg.repeat.warnAt = num(repeat["warnAt"], cfg.repeat.warnAt);
302
- cfg.repeat.windowMinutes = num(repeat["windowMinutes"], cfg.repeat.windowMinutes);
303
- cfg.repeat.watch = strArr(repeat["watch"], cfg.repeat.watch);
304
- const exempt = repeat["exemptPatterns"];
305
- if (Array.isArray(exempt)) cfg.repeat.exemptPatterns = exempt.filter((p) => typeof p === "string");
306
- const th = repeat["thresholds"];
307
- if (th) {
308
- for (const [k, v] of Object.entries(th)) if (typeof v === "number") cfg.repeat.thresholds[k] = v;
309
- }
310
- const cycle = section("cycle");
311
- cfg.cycle.enabled = bool(cycle["enabled"], cfg.cycle.enabled);
312
- cfg.cycle.windowMinutes = num(cycle["windowMinutes"], cfg.cycle.windowMinutes);
313
- const noProgress = section("noProgress");
314
- cfg.noProgress.enabled = bool(noProgress["enabled"], cfg.noProgress.enabled);
315
- cfg.noProgress.windowMinutes = num(noProgress["windowMinutes"], cfg.noProgress.windowMinutes);
316
- cfg.noProgress.warnAt = num(noProgress["warnAt"], cfg.noProgress.warnAt);
317
- cfg.noProgress.blockAt = num(noProgress["blockAt"], cfg.noProgress.blockAt);
318
- const nearRepeat = section("nearRepeat");
319
- cfg.nearRepeat.enabled = bool(nearRepeat["enabled"], cfg.nearRepeat.enabled);
320
- cfg.nearRepeat.windowMinutes = num(nearRepeat["windowMinutes"], cfg.nearRepeat.windowMinutes);
321
- cfg.nearRepeat.warnAt = num(nearRepeat["warnAt"], cfg.nearRepeat.warnAt);
322
- cfg.nearRepeat.blockAt = num(nearRepeat["blockAt"], cfg.nearRepeat.blockAt);
323
- const explore = section("explore");
324
- cfg.explore.enabled = bool(explore["enabled"], cfg.explore.enabled);
325
- cfg.explore.windowMinutes = num(explore["windowMinutes"], cfg.explore.windowMinutes);
326
- cfg.explore.warnAt = num(explore["warnAt"], cfg.explore.warnAt);
327
- cfg.explore.blockAt = num(explore["blockAt"], cfg.explore.blockAt);
328
- const verify = section("verify");
329
- cfg.verify.enabled = bool(verify["enabled"], cfg.verify.enabled);
330
- cfg.verify.blockOnNoEvidence = bool(verify["blockOnNoEvidence"], cfg.verify.blockOnNoEvidence);
331
- cfg.verify.evidenceWindowMinutes = num(verify["evidenceWindowMinutes"], cfg.verify.evidenceWindowMinutes);
332
- const claims = verify["claimPatterns"];
333
- if (Array.isArray(claims)) cfg.verify.claimPatterns = claims.filter((c) => typeof c === "string");
334
- const evidence = verify["evidencePatterns"];
335
- if (Array.isArray(evidence)) cfg.verify.evidencePatterns = evidence.filter((c) => typeof c === "string");
336
- cfg.verify.shellTools = strArr(verify["shellTools"], cfg.verify.shellTools);
337
- const legacyShellTools = strArrOrNull(verify["shellTools"]);
338
- if (legacyShellTools && !toolsShell) cfg.tools.shell = legacyShellTools;
339
- const veto = verify["veto"];
340
- if (veto) {
341
- cfg.verify.veto.enabled = bool(veto["enabled"], cfg.verify.veto.enabled);
342
- cfg.verify.veto.model = typeof veto["model"] === "string" ? veto["model"] : cfg.verify.veto.model;
343
- cfg.verify.veto.baseUrl = typeof veto["baseUrl"] === "string" ? veto["baseUrl"] : cfg.verify.veto.baseUrl;
344
- cfg.verify.veto.maxCallsPerSession = num(veto["maxCallsPerSession"], cfg.verify.veto.maxCallsPerSession);
345
- cfg.verify.veto.timeoutMs = num(veto["timeoutMs"], cfg.verify.veto.timeoutMs);
346
- }
347
- const thinking = section("thinking");
348
- cfg.thinking.enabled = bool(thinking["enabled"], cfg.thinking.enabled);
349
- cfg.thinking.minThinkChars = num(thinking["minThinkChars"], cfg.thinking.minThinkChars);
350
- cfg.thinking.maxTextRatio = num(thinking["maxTextRatio"], cfg.thinking.maxTextRatio);
351
- const anchor = section("anchor");
352
- cfg.anchor.enabled = bool(anchor["enabled"], cfg.anchor.enabled);
353
- cfg.anchor.everyNPrompts = num(anchor["everyNPrompts"], cfg.anchor.everyNPrompts);
354
- cfg.anchor.maxChars = num(anchor["maxChars"], cfg.anchor.maxChars);
355
- const context = section("context");
356
- cfg.context.enabled = bool(context["enabled"], cfg.context.enabled);
357
- cfg.context.warnPercent = num(context["warnPercent"], cfg.context.warnPercent);
358
- const noGain = section("noGain");
359
- cfg.noGain.enabled = bool(noGain["enabled"], cfg.noGain.enabled);
360
- cfg.noGain.windowMinutes = num(noGain["windowMinutes"], cfg.noGain.windowMinutes);
361
- cfg.noGain.warnAt = num(noGain["warnAt"], cfg.noGain.warnAt);
362
- cfg.noGain.blockAt = num(noGain["blockAt"], cfg.noGain.blockAt);
363
- const churn = section("churn");
364
- cfg.churn.enabled = bool(churn["enabled"], cfg.churn.enabled);
365
- cfg.churn.windowMinutes = num(churn["windowMinutes"], cfg.churn.windowMinutes);
366
- cfg.churn.warnAt = num(churn["warnAt"], cfg.churn.warnAt);
367
- cfg.churn.blockAt = num(churn["blockAt"], cfg.churn.blockAt);
368
- cfg.churn.tools = strArr(churn["tools"], cfg.churn.tools);
369
- const legacyChurnTools = strArrOrNull(churn["tools"]);
370
- if (legacyChurnTools && !toolsEdit) cfg.tools.edit = legacyChurnTools;
371
- const policy = section("policy");
372
- cfg.policy.killSwitch = bool(policy["killSwitch"], cfg.policy.killSwitch);
373
- cfg.policy.maxBlocksPerSession = num(policy["maxBlocksPerSession"], cfg.policy.maxBlocksPerSession);
374
- cfg.policy.blockWindowMinutes = num(policy["blockWindowMinutes"], cfg.policy.blockWindowMinutes);
375
- const budget = section("budget");
376
- cfg.budget.enabled = bool(budget["enabled"], cfg.budget.enabled);
377
- cfg.budget.plan = typeof budget["plan"] === "string" ? budget["plan"] : cfg.budget.plan;
378
- cfg.budget.weekly = num(budget["weekly"], cfg.budget.weekly);
379
- cfg.budget.fiveHour = num(budget["fiveHour"], cfg.budget.fiveHour);
380
- cfg.budget.dispatchTools = strArr(budget["dispatchTools"], cfg.budget.dispatchTools);
381
- cfg.budget.reservePercent = num(budget["reservePercent"], cfg.budget.reservePercent);
382
- cfg.budget.subagentWeight = num(budget["subagentWeight"], cfg.budget.subagentWeight);
383
- cfg.budget.warnPercent = num(budget["warnPercent"], cfg.budget.warnPercent);
384
- cfg.budget.precise = bool(budget["precise"], cfg.budget.precise);
385
- cfg.budget.preciseUrl = typeof budget["preciseUrl"] === "string" ? budget["preciseUrl"] : cfg.budget.preciseUrl;
386
- cfg.budget.preciseCacheSeconds = num(budget["preciseCacheSeconds"], cfg.budget.preciseCacheSeconds);
387
- cfg.probe = bool(section("probe")["enabled"], cfg.probe);
388
- cfg.verify.shellTools = cfg.tools.shell;
389
- cfg.churn.tools = cfg.tools.edit;
390
- return cfg;
391
- }
392
- function writeConfigTemplate(configPath = userConfigPath()) {
393
- if (fs2.existsSync(configPath)) return false;
394
- fs2.mkdirSync(configPath.replace(/[/\\][^/\\]+$/, ""), { recursive: true });
395
- fs2.writeFileSync(configPath, CONFIG_TEMPLATE, "utf8");
396
- return true;
397
- }
398
-
399
- // src/installer.ts
400
- import fs3 from "fs";
401
- import path2 from "path";
402
64
  var MANAGED_BEGIN = "# >>> kimi-guard managed >>> DO NOT EDIT";
403
65
  var MANAGED_END = "# <<< kimi-guard <<<";
404
66
  function hookRules(commandName = "kguard", compat = false) {
@@ -443,12 +105,12 @@ function findManagedBlock(text) {
443
105
  function installHooks(commandName = "kguard", compat = false) {
444
106
  const { path: configPath, exists } = detectKimiConfig();
445
107
  const created = !exists;
446
- let text = exists ? fs3.readFileSync(configPath, "utf8") : "";
108
+ let text = exists ? fs.readFileSync(configPath, "utf8") : "";
447
109
  const block = findManagedBlock(text);
448
110
  const replaced = block !== void 0;
449
111
  if (exists && !replaced) {
450
112
  const backupPath = `${configPath}.kimi-guard.bak`;
451
- fs3.writeFileSync(backupPath, text, "utf8");
113
+ fs.writeFileSync(backupPath, text, "utf8");
452
114
  }
453
115
  const newBlock = managedBlock(commandName, compat);
454
116
  if (block) {
@@ -457,8 +119,8 @@ function installHooks(commandName = "kguard", compat = false) {
457
119
  const prefix = text.length === 0 ? "" : text.endsWith("\n") ? "\n" : "\n\n";
458
120
  text = text + prefix + newBlock + "\n";
459
121
  }
460
- fs3.mkdirSync(path2.dirname(configPath), { recursive: true });
461
- fs3.writeFileSync(configPath, text, "utf8");
122
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
123
+ fs.writeFileSync(configPath, text, "utf8");
462
124
  return {
463
125
  configPath,
464
126
  created,
@@ -469,71 +131,71 @@ function installHooks(commandName = "kguard", compat = false) {
469
131
  function uninstallHooks() {
470
132
  const { path: configPath, exists } = detectKimiConfig();
471
133
  if (!exists) return { configPath, removed: false };
472
- const text = fs3.readFileSync(configPath, "utf8");
134
+ const text = fs.readFileSync(configPath, "utf8");
473
135
  const block = findManagedBlock(text);
474
136
  if (!block) return { configPath, removed: false };
475
137
  let out = text.slice(0, block.start) + text.slice(block.end);
476
138
  out = out.replace(/^\n{2,}/, "\n");
477
- fs3.writeFileSync(configPath, out, "utf8");
139
+ fs.writeFileSync(configPath, out, "utf8");
478
140
  return { configPath, removed: true };
479
141
  }
480
142
  function hooksInstalled(configPath) {
481
143
  const p = configPath ?? detectKimiConfig().path;
482
144
  try {
483
- return fs3.readFileSync(p, "utf8").includes(MANAGED_BEGIN);
145
+ return fs.readFileSync(p, "utf8").includes(MANAGED_BEGIN);
484
146
  } catch {
485
147
  return false;
486
148
  }
487
149
  }
488
150
 
489
- // src/harness/claude.ts
490
- import fs4 from "fs";
491
- import path3 from "path";
492
- var CLAUDE_COMMAND_MARKER = "agentguard hook";
493
- var CLAUDE_EVENTS = [
151
+ // src/harness/codex.ts
152
+ import fs2 from "fs";
153
+ import path2 from "path";
154
+ var CODEX_COMMAND_MARKER = "agentguard hook";
155
+ var CODEX_EVENTS = [
494
156
  "PreToolUse",
495
157
  "PostToolUse",
496
- "PostToolUseFailure",
497
158
  "UserPromptSubmit",
498
159
  "Stop",
499
160
  "SubagentStart",
161
+ "SubagentStop",
500
162
  "SessionStart",
501
163
  "SessionEnd",
502
164
  "PreCompact",
503
165
  "PostCompact",
504
- "StopFailure"
166
+ "Interrupt"
505
167
  ];
506
- var CLAUDE_EVENTS_COMPAT = ["PreToolUse", "PostToolUse", "PostToolUseFailure"];
168
+ var CODEX_EVENTS_COMPAT = ["PreToolUse", "PostToolUse"];
507
169
  function isOurs(group) {
508
- return (group.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(CLAUDE_COMMAND_MARKER));
170
+ return (group.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(CODEX_COMMAND_MARKER));
509
171
  }
510
172
  function ourGroup(event, bin) {
511
173
  return {
512
174
  matcher: "",
513
- hooks: [{ type: "command", command: `${bin} hook ${event} --harness claude`, timeout: 5 }]
175
+ hooks: [{ type: "command", command: `${bin} hook ${event} --harness codex`, timeout: 5 }]
514
176
  };
515
177
  }
516
- function installClaudeHooks(bin = "agentguard", compat = false) {
517
- const configPath = claudeSettingsPath();
518
- const created = !fs4.existsSync(configPath);
519
- let settings = {};
178
+ function installCodexHooks(bin = "agentguard", compat = false) {
179
+ const configPath = codexHooksPath();
180
+ const created = !fs2.existsSync(configPath);
181
+ let file = {};
520
182
  let backupPath;
521
183
  if (!created) {
522
- const raw = fs4.readFileSync(configPath, "utf8");
184
+ const raw = fs2.readFileSync(configPath, "utf8");
523
185
  try {
524
- settings = JSON.parse(raw);
186
+ file = JSON.parse(raw);
525
187
  } catch {
526
188
  backupPath = `${configPath}.agentguard.bak`;
527
- fs4.copyFileSync(configPath, backupPath);
528
- settings = {};
189
+ fs2.copyFileSync(configPath, backupPath);
190
+ file = {};
529
191
  }
530
192
  if (!backupPath) {
531
193
  backupPath = `${configPath}.agentguard.bak`;
532
- fs4.writeFileSync(backupPath, raw, "utf8");
194
+ fs2.writeFileSync(backupPath, raw, "utf8");
533
195
  }
534
196
  }
535
- const events = compat ? CLAUDE_EVENTS_COMPAT : CLAUDE_EVENTS;
536
- const hooks = settings.hooks ??= {};
197
+ const events = compat ? CODEX_EVENTS_COMPAT : CODEX_EVENTS;
198
+ const hooks = file.hooks ??= {};
537
199
  let updated = false;
538
200
  for (const event of events) {
539
201
  const groups = (hooks[event] ?? []).filter((g) => !isOurs(g));
@@ -542,20 +204,20 @@ function installClaudeHooks(bin = "agentguard", compat = false) {
542
204
  hooks[event] = groups;
543
205
  if (JSON.stringify(groups) !== before) updated = true;
544
206
  }
545
- fs4.mkdirSync(path3.dirname(configPath), { recursive: true });
546
- fs4.writeFileSync(configPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
207
+ fs2.mkdirSync(path2.dirname(configPath), { recursive: true });
208
+ fs2.writeFileSync(configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
547
209
  return { configPath, created, updated: updated || created, backupPath: created ? void 0 : backupPath };
548
210
  }
549
- function uninstallClaudeHooks() {
550
- const configPath = claudeSettingsPath();
551
- if (!fs4.existsSync(configPath)) return { configPath, removed: false };
552
- let settings;
211
+ function uninstallCodexHooks() {
212
+ const configPath = codexHooksPath();
213
+ if (!fs2.existsSync(configPath)) return { configPath, removed: false };
214
+ let file;
553
215
  try {
554
- settings = JSON.parse(fs4.readFileSync(configPath, "utf8"));
216
+ file = JSON.parse(fs2.readFileSync(configPath, "utf8"));
555
217
  } catch {
556
218
  return { configPath, removed: false };
557
219
  }
558
- const hooks = settings.hooks;
220
+ const hooks = file.hooks;
559
221
  if (!hooks) return { configPath, removed: false };
560
222
  let removed = false;
561
223
  for (const event of Object.keys(hooks)) {
@@ -564,565 +226,20 @@ function uninstallClaudeHooks() {
564
226
  if (kept.length === 0) delete hooks[event];
565
227
  else hooks[event] = kept;
566
228
  }
567
- if (removed) fs4.writeFileSync(configPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
229
+ if (removed) fs2.writeFileSync(configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
568
230
  return { configPath, removed };
569
231
  }
570
- function claudeHooksInstalled(configPath = claudeSettingsPath()) {
232
+ function codexHooksInstalled(configPath = codexHooksPath()) {
571
233
  try {
572
- const settings = JSON.parse(fs4.readFileSync(configPath, "utf8"));
573
- return Object.values(settings.hooks ?? {}).some((groups) => groups.some(isOurs));
234
+ const file = JSON.parse(fs2.readFileSync(configPath, "utf8"));
235
+ return Object.values(file.hooks ?? {}).some((groups) => groups.some(isOurs));
574
236
  } catch {
575
237
  return false;
576
238
  }
577
239
  }
578
240
 
579
241
  // src/guard.ts
580
- import fs7 from "fs";
581
-
582
- // src/store.ts
583
- import fs5 from "fs";
584
- import path4 from "path";
585
- import { createRequire as createRequire2 } from "module";
586
- var nodeRequire = createRequire2(import.meta.url);
587
- function sqliteCtor() {
588
- return nodeRequire("node:sqlite").DatabaseSync;
589
- }
590
- var SCHEMA_VERSION = 3;
591
- var SCHEMA = `
592
- CREATE TABLE IF NOT EXISTS calls (
593
- id INTEGER PRIMARY KEY AUTOINCREMENT,
594
- session_id TEXT NOT NULL,
595
- tool_name TEXT NOT NULL,
596
- args_hash TEXT NOT NULL,
597
- args_json TEXT NOT NULL,
598
- output_hash TEXT,
599
- file_path TEXT,
600
- status TEXT NOT NULL,
601
- ts INTEGER NOT NULL
602
- );
603
- CREATE INDEX IF NOT EXISTS idx_calls_session ON calls(session_id, ts);
604
- CREATE INDEX IF NOT EXISTS idx_calls_sig ON calls(session_id, tool_name, args_hash, ts);
605
- CREATE INDEX IF NOT EXISTS idx_calls_out ON calls(session_id, tool_name, output_hash, ts);
606
- CREATE INDEX IF NOT EXISTS idx_calls_time ON calls(ts);
607
- CREATE TABLE IF NOT EXISTS events (
608
- id INTEGER PRIMARY KEY AUTOINCREMENT,
609
- session_id TEXT NOT NULL,
610
- kind TEXT NOT NULL,
611
- meta_json TEXT NOT NULL,
612
- ts INTEGER NOT NULL
613
- );
614
- CREATE INDEX IF NOT EXISTS idx_events_kind ON events(session_id, kind, ts);
615
- CREATE INDEX IF NOT EXISTS idx_events_time ON events(ts);
616
- CREATE TABLE IF NOT EXISTS blocks (
617
- id INTEGER PRIMARY KEY AUTOINCREMENT,
618
- session_id TEXT NOT NULL,
619
- tool_name TEXT NOT NULL,
620
- kind TEXT NOT NULL,
621
- ts INTEGER NOT NULL,
622
- feedback TEXT
623
- );
624
- CREATE INDEX IF NOT EXISTS idx_blocks_session ON blocks(session_id, ts);
625
- CREATE TABLE IF NOT EXISTS meta (
626
- k TEXT PRIMARY KEY,
627
- v TEXT NOT NULL
628
- );
629
- `;
630
- var db = null;
631
- function openDb() {
632
- if (db) return db;
633
- const file = stateDbPath();
634
- fs5.mkdirSync(path4.dirname(file), { recursive: true });
635
- const d = new (sqliteCtor())(file);
636
- d.exec("PRAGMA journal_mode = WAL;");
637
- migrate(d);
638
- db = d;
639
- return db;
640
- }
641
- function migrate(d) {
642
- d.exec("CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);");
643
- const row = d.prepare("SELECT v FROM meta WHERE k = 'schema_version'").get();
644
- const version2 = row ? Number(row.v) : 0;
645
- if (version2 === SCHEMA_VERSION) {
646
- d.exec(SCHEMA);
647
- return;
648
- }
649
- if (version2 === 2) {
650
- d.exec(SCHEMA);
651
- try {
652
- d.exec("ALTER TABLE blocks ADD COLUMN feedback TEXT");
653
- } catch {
654
- }
655
- } else {
656
- d.exec("DROP TABLE IF EXISTS calls; DROP TABLE IF EXISTS events; DROP TABLE IF EXISTS blocks;");
657
- d.exec(SCHEMA);
658
- }
659
- d.prepare(
660
- "INSERT INTO meta (k, v) VALUES ('schema_version', ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v"
661
- ).run(String(SCHEMA_VERSION));
662
- }
663
- function recordCall(call) {
664
- openDb().prepare(
665
- "INSERT INTO calls (session_id, tool_name, args_hash, args_json, output_hash, file_path, status, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
666
- ).run(
667
- call.sessionId,
668
- call.toolName,
669
- call.argsHash,
670
- call.argsJson,
671
- call.outputHash,
672
- call.filePath,
673
- call.status,
674
- call.ts ?? Date.now()
675
- );
676
- }
677
- function callsSince(sessionId, sinceTs, limit = 500) {
678
- return openDb().prepare(
679
- "SELECT tool_name, args_hash, args_json, output_hash, file_path, status, ts FROM calls WHERE session_id = ? AND ts >= ? ORDER BY ts ASC LIMIT ?"
680
- ).all(sessionId, sinceTs, limit);
681
- }
682
- function recordEvent(sessionId, kind, meta, ts = Date.now()) {
683
- openDb().prepare("INSERT INTO events (session_id, kind, meta_json, ts) VALUES (?, ?, ?, ?)").run(sessionId, kind, JSON.stringify(meta), ts);
684
- }
685
- function countEvents(sessionId, kinds, sinceTs) {
686
- const placeholders = kinds.map(() => "?").join(",");
687
- const row = openDb().prepare(
688
- `SELECT COUNT(*) AS n FROM events WHERE session_id = ? AND kind IN (${placeholders}) AND ts >= ?`
689
- ).get(sessionId, ...kinds, sinceTs);
690
- return Number(row?.n ?? 0);
691
- }
692
- function oldestEventTs(sessionId, kinds, sinceTs) {
693
- const placeholders = kinds.map(() => "?").join(",");
694
- const row = openDb().prepare(
695
- `SELECT MIN(ts) AS m FROM events WHERE session_id = ? AND kind IN (${placeholders}) AND ts >= ?`
696
- ).get(sessionId, ...kinds, sinceTs);
697
- return row?.m ?? null;
698
- }
699
- function recordBlock(sessionId, toolName, kind, ts = Date.now()) {
700
- const info = openDb().prepare("INSERT INTO blocks (session_id, tool_name, kind, ts) VALUES (?, ?, ?, ?)").run(sessionId, toolName, kind, ts);
701
- return Number(info.lastInsertRowid);
702
- }
703
- function listBlocks(limit = 20) {
704
- return openDb().prepare("SELECT id, session_id, tool_name, kind, ts, feedback FROM blocks ORDER BY id DESC LIMIT ?").all(limit);
705
- }
706
- function setBlockFeedback(id, verdict) {
707
- const info = openDb().prepare("UPDATE blocks SET feedback = ? WHERE id = ?").run(verdict, id);
708
- return Number(info.changes) > 0;
709
- }
710
- function blockKindStats() {
711
- const rows = openDb().prepare(
712
- `SELECT kind, COUNT(*) AS n,
713
- SUM(CASE WHEN feedback = 'fp' THEN 1 ELSE 0 END) AS fp,
714
- SUM(CASE WHEN feedback = 'tp' THEN 1 ELSE 0 END) AS tp
715
- FROM blocks GROUP BY kind ORDER BY n DESC`
716
- ).all();
717
- return rows.map((r) => ({ kind: r.kind, n: Number(r.n), fp: Number(r.fp), tp: Number(r.tp) }));
718
- }
719
- function countBlocks(sessionId, sinceTs) {
720
- const row = openDb().prepare("SELECT COUNT(*) AS n FROM blocks WHERE session_id = ? AND ts >= ?").get(sessionId, sinceTs);
721
- return Number(row?.n ?? 0);
722
- }
723
- function getMeta(key) {
724
- const row = openDb().prepare("SELECT v FROM meta WHERE k = ?").get(key);
725
- return row?.v;
726
- }
727
- function setMeta(key, value) {
728
- openDb().prepare("INSERT INTO meta (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v").run(key, value);
729
- }
730
- function knownSessions(limit = 5) {
731
- return openDb().prepare(
732
- `SELECT session_id, MAX(last_ts) AS last_ts, SUM(n) AS n FROM (
733
- SELECT session_id, MAX(ts) AS last_ts, COUNT(*) AS n FROM calls GROUP BY session_id
734
- UNION ALL
735
- SELECT session_id, MAX(ts) AS last_ts, COUNT(*) AS n FROM events GROUP BY session_id
736
- ) GROUP BY session_id ORDER BY last_ts DESC LIMIT ?`
737
- ).all(limit);
738
- }
739
- function buildStatus() {
740
- const d = openDb();
741
- const now = Date.now();
742
- const day = now - 864e5;
743
- const calls24h = Number(d.prepare("SELECT COUNT(*) AS n FROM calls WHERE ts >= ?").get(day).n);
744
- const blocks24h = d.prepare("SELECT kind, COUNT(*) AS n FROM blocks WHERE ts >= ? GROUP BY kind").all(day).map((r) => ({ kind: r.kind, n: Number(r.n) }));
745
- const topRepeated = d.prepare(
746
- "SELECT tool_name, args_hash, COUNT(*) AS n FROM calls WHERE ts >= ? GROUP BY session_id, tool_name, args_hash ORDER BY n DESC LIMIT 5"
747
- ).all(day).map((r) => ({ tool_name: r.tool_name, args_hash: r.args_hash, n: Number(r.n) }));
748
- const noGainPairs = d.prepare(
749
- "SELECT tool_name, COUNT(DISTINCT session_id || ':' || output_hash) AS n FROM calls WHERE ts >= ? AND output_hash IS NOT NULL GROUP BY tool_name ORDER BY n DESC LIMIT 5"
750
- ).all(day).map((r) => ({ tool_name: r.tool_name, n: Number(r.n) }));
751
- const events24h = d.prepare("SELECT kind, COUNT(*) AS n FROM events WHERE ts >= ? GROUP BY kind").all(day).map((r) => ({ kind: r.kind, n: Number(r.n) }));
752
- const lastActivity = d.prepare("SELECT MAX(m) AS m FROM (SELECT MAX(ts) AS m FROM calls UNION ALL SELECT MAX(ts) AS m FROM events)").get();
753
- return {
754
- calls24h,
755
- blocks24h,
756
- topRepeated,
757
- noGainPairs,
758
- events24h,
759
- lastActivityTs: lastActivity?.m ?? null
760
- };
761
- }
762
-
763
- // src/events.ts
764
- import { createHash } from "crypto";
765
- function pickString(payload, keys) {
766
- for (const k of keys) {
767
- const v = payload[k];
768
- if (typeof v === "string" && v) return v;
769
- }
770
- return "";
771
- }
772
- function pickField(payload, keys) {
773
- for (const k of keys) {
774
- if (payload[k] !== void 0) return payload[k];
775
- }
776
- return void 0;
777
- }
778
- function collapseWs(s) {
779
- return s.replace(/\s+/g, " ").trim();
780
- }
781
- function sortDeep(value, collapseStrings) {
782
- if (Array.isArray(value)) return value.map((x) => sortDeep(x, collapseStrings));
783
- if (value !== null && typeof value === "object") {
784
- const out = {};
785
- for (const k of Object.keys(value).sort()) {
786
- out[k] = sortDeep(value[k], collapseStrings);
787
- }
788
- return out;
789
- }
790
- if (value === void 0) return null;
791
- if (typeof value === "string") return collapseStrings ? collapseWs(value) : value;
792
- return value;
793
- }
794
- var WHITESPACE_SENSITIVE = /* @__PURE__ */ new Set(["Shell", "Bash", "Grep", "Glob", "FetchURL", "SearchWeb", "ReadFile", "WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit"]);
795
- function fingerprint(tool, args) {
796
- let v = args ?? {};
797
- if (v === null || typeof v !== "object") v = { value: v ?? null };
798
- const normalized = sortDeep(v, !WHITESPACE_SENSITIVE.has(tool));
799
- return createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 16);
800
- }
801
- function hashOutput(output) {
802
- if (output === void 0 || output === null) return null;
803
- let s;
804
- if (typeof output === "string") s = output;
805
- else {
806
- try {
807
- s = JSON.stringify(sortDeep(output, false));
808
- } catch {
809
- s = String(output);
810
- }
811
- }
812
- s = collapseWs(s).slice(0, 4096);
813
- if (!s) return null;
814
- return createHash("sha256").update(s).digest("hex").slice(0, 16);
815
- }
816
- var FILE_KEYS = ["file_path", "filePath", "path", "file", "filename", "notebook_path", "target"];
817
- function extractFile(args) {
818
- if (args === null || typeof args !== "object") return null;
819
- const obj = args;
820
- for (const k of FILE_KEYS) {
821
- const v = obj[k];
822
- if (typeof v === "string" && v) return v;
823
- }
824
- return null;
825
- }
826
- function normalizeCall(payload, event, ts = Date.now()) {
827
- const sessionId = pickString(payload, ["session_id", "sessionId", "session", "sessionID"]) || "unknown";
828
- const tool = pickString(payload, ["tool_name", "toolName", "tool"]);
829
- if (!tool) return null;
830
- const args = pickField(payload, ["tool_input", "toolInput", "input"]) ?? {};
831
- const outputKeys = ["tool_output", "toolOutput", "tool_response", "output", "result"];
832
- const output = event === "PostToolUse" ? pickField(payload, outputKeys) : event === "PostToolUseFailure" ? pickField(payload, ["error", "error_message", ...outputKeys]) : void 0;
833
- const argsJson = JSON.stringify(args).slice(0, 2048);
834
- return {
835
- sessionId,
836
- tool,
837
- args,
838
- argsHash: fingerprint(tool, args),
839
- argsJson,
840
- outputHash: output !== void 0 ? hashOutput(output) : null,
841
- filePath: extractFile(args),
842
- status: event === "PostToolUseFailure" ? "failure" : "ok",
843
- ts
844
- };
845
- }
846
-
847
- // src/analysis.ts
848
- var allow = [];
849
- function isRepeatExempt(proposed, cfg) {
850
- if (cfg.repeat.exemptPatterns.length === 0) return false;
851
- let text;
852
- try {
853
- text = JSON.stringify(proposed.args ?? {});
854
- } catch {
855
- text = String(proposed.args);
856
- }
857
- for (const p of cfg.repeat.exemptPatterns) {
858
- try {
859
- if (new RegExp(p).test(text)) return true;
860
- } catch {
861
- }
862
- }
863
- return false;
864
- }
865
- function analyzeRepetition(history, proposed, cfg, now) {
866
- if (!cfg.repeat.enabled) return allow;
867
- const watched = cfg.repeat.watch.includes(proposed.tool) || proposed.tool in cfg.repeat.thresholds;
868
- if (!watched) return allow;
869
- if (isRepeatExempt(proposed, cfg)) return allow;
870
- const threshold = cfg.repeat.thresholds[proposed.tool] ?? cfg.repeat.maxRepeats;
871
- const since = now - cfg.repeat.windowMinutes * 6e4;
872
- const n = history.filter(
873
- (r) => r.tool_name === proposed.tool && r.args_hash === proposed.argsHash && r.ts >= since
874
- ).length;
875
- if (n >= threshold) {
876
- return [
877
- {
878
- kind: "repeat",
879
- severity: "block",
880
- tool: proposed.tool,
881
- message: `"${proposed.tool}" has already been called ${n} times with identical arguments in the last ${cfg.repeat.windowMinutes} minutes. The previous results are already in context \u2014 use them instead of re-running. If a retry is genuinely required, change the arguments or state why the previous result is insufficient.`,
882
- evidence: `signature count=${n}, threshold=${threshold}`
883
- }
884
- ];
885
- }
886
- if (cfg.repeat.warnAt > 0 && n >= cfg.repeat.warnAt) {
887
- return [
888
- {
889
- kind: "repeat",
890
- severity: "warn",
891
- tool: proposed.tool,
892
- message: `"${proposed.tool}" has been called ${n} times with identical arguments \u2014 the result is already in context. Identical calls are blocked at ${threshold}; make sure any retry adds new information.`,
893
- evidence: `signature count=${n}, warnAt=${cfg.repeat.warnAt}`
894
- }
895
- ];
896
- }
897
- return allow;
898
- }
899
- function analyzeCycles(history, cfg, now) {
900
- if (!cfg.cycle.enabled) return allow;
901
- const since = now - cfg.cycle.windowMinutes * 6e4;
902
- const recent = history.filter((r) => r.ts >= since).slice(-16).map((r) => `${r.tool_name}:${r.args_hash}`);
903
- if (recent.length < 8) return allow;
904
- const findings = [];
905
- for (let period = 1; period <= 3; period++) {
906
- const minReps = period === 1 ? 5 : 3;
907
- const needed = period * minReps;
908
- const tail = recent.slice(-needed);
909
- if (tail.length < needed) continue;
910
- const base = tail.slice(0, period);
911
- let isCycle = true;
912
- for (let i = period; i < tail.length; i++) {
913
- if (tail[i] !== base[i % period]) {
914
- isCycle = false;
915
- break;
916
- }
917
- }
918
- if (isCycle) {
919
- const desc = period === 1 ? `the same call (${base[0]})` : `a ${period}-step cycle (${base.map((s) => s.split(":")[0]).join(" \u2192 ")})`;
920
- findings.push({
921
- kind: "cycle",
922
- severity: "block",
923
- tool: base[0]?.split(":")[0],
924
- message: `Loop detected: the agent has repeated ${desc} ${minReps}+ times in a row without progress. Stop re-running this sequence. Re-read the results already in context, reassess the approach, and either proceed differently or end the turn with a summary.`,
925
- evidence: `period=${period}, reps>=${minReps}`
926
- });
927
- break;
928
- }
929
- }
930
- return findings;
931
- }
932
- function analyzeNoGain(history, cfg, now) {
933
- if (!cfg.noGain.enabled) return allow;
934
- const since = now - cfg.noGain.windowMinutes * 6e4;
935
- const byPair = /* @__PURE__ */ new Map();
936
- for (const r of history) {
937
- if (r.ts < since || !r.output_hash) continue;
938
- const key = `${r.tool_name}:${r.output_hash}`;
939
- byPair.set(key, (byPair.get(key) ?? 0) + 1);
940
- }
941
- const findings = [];
942
- for (const [key, n] of byPair) {
943
- if (n < cfg.noGain.warnAt) continue;
944
- const tool = key.split(":")[0];
945
- if (n >= cfg.noGain.blockAt) {
946
- findings.push({
947
- kind: "noGain",
948
- severity: "block",
949
- tool,
950
- message: `No-progress loop: ${tool} returned the exact same output ${n} times despite different arguments. You are not gaining new information. Stop calling ${tool}, analyze the result you already have, change strategy, or report your findings.`,
951
- evidence: `tool=${tool} identical_output_count=${n}`
952
- });
953
- } else {
954
- findings.push({
955
- kind: "noGain",
956
- severity: "warn",
957
- tool,
958
- message: `${tool} has returned the same output ${n} times \u2014 verify you are not repeating work.`,
959
- evidence: `tool=${tool} identical_output_count=${n}`
960
- });
961
- }
962
- }
963
- return findings.slice(0, 2);
964
- }
965
- function editToolSet(cfg) {
966
- return editTools(cfg);
967
- }
968
- function analyzeNoProgress(history, proposed, cfg, now) {
969
- if (!cfg.noProgress.enabled) return allow;
970
- if (editToolSet(cfg).has(proposed.tool)) return allow;
971
- const since = now - cfg.noProgress.windowMinutes * 6e4;
972
- const tools = editToolSet(cfg);
973
- let lastEditTs = -1;
974
- for (const r of history) {
975
- if (r.ts < since) continue;
976
- if (tools.has(r.tool_name) && r.status === "ok") lastEditTs = Math.max(lastEditTs, r.ts);
977
- }
978
- const stretch = history.filter((r) => r.ts >= since && r.ts > lastEditTs).length;
979
- if (stretch < cfg.noProgress.warnAt) return allow;
980
- if (stretch >= cfg.noProgress.blockAt) {
981
- return [
982
- {
983
- kind: "noProgress",
984
- severity: "block",
985
- tool: proposed.tool,
986
- message: `No progress: ${stretch} tool calls in the last ${cfg.noProgress.windowMinutes} minutes with no successful file edit landing. You are circling, not converging. Stop, pick the single most valuable next change, make it deliberately, or report what is blocking you.`,
987
- evidence: `stretch=${stretch} warnAt=${cfg.noProgress.warnAt} blockAt=${cfg.noProgress.blockAt}`
988
- }
989
- ];
990
- }
991
- return [
992
- {
993
- kind: "noProgress",
994
- severity: "warn",
995
- tool: proposed.tool,
996
- message: `${stretch} calls without a landed edit recently \u2014 make sure the next step actually produces a change.`,
997
- evidence: `stretch=${stretch}`
998
- }
999
- ];
1000
- }
1001
- function analyzeChurn(history, cfg, now) {
1002
- if (!cfg.churn.enabled) return allow;
1003
- const since = now - cfg.churn.windowMinutes * 6e4;
1004
- const tools = editTools(cfg);
1005
- const byFile = /* @__PURE__ */ new Map();
1006
- for (const r of history) {
1007
- if (r.ts < since || !r.file_path || !tools.has(r.tool_name)) continue;
1008
- byFile.set(r.file_path, (byFile.get(r.file_path) ?? 0) + 1);
1009
- }
1010
- const findings = [];
1011
- for (const [file, n] of byFile) {
1012
- if (n >= cfg.churn.blockAt) {
1013
- findings.push({
1014
- kind: "churn",
1015
- severity: "block",
1016
- tool: "edit",
1017
- message: `Edit churn: ${file} has been modified ${n} times in the last ${cfg.churn.windowMinutes} minutes without converging. Stop editing. Re-read the file and the error output, form an explicit hypothesis about why previous fixes failed, then make a single deliberate change \u2014 or ask the user for help.`,
1018
- evidence: `file=${file} edits=${n}`
1019
- });
1020
- } else if (n >= cfg.churn.warnAt) {
1021
- findings.push({
1022
- kind: "churn",
1023
- severity: "warn",
1024
- tool: "edit",
1025
- message: `${file} has been edited ${n} times recently \u2014 step back and verify your approach before editing again.`,
1026
- evidence: `file=${file} edits=${n}`
1027
- });
1028
- }
1029
- }
1030
- return findings.sort((a, b) => a.severity === b.severity ? 0 : a.severity === "block" ? -1 : 1).slice(0, 1);
1031
- }
1032
- function analyzeExplore(history, proposed, cfg, now) {
1033
- if (!cfg.explore.enabled) return allow;
1034
- const passive = /* @__PURE__ */ new Set([...readTools(cfg), ...searchTools(cfg)]);
1035
- if (!passive.has(proposed.tool)) return allow;
1036
- const since = now - cfg.explore.windowMinutes * 6e4;
1037
- let streak = 0;
1038
- for (let i = history.length - 1; i >= 0; i--) {
1039
- const r = history[i];
1040
- if (r.ts < since || !passive.has(r.tool_name)) break;
1041
- streak++;
1042
- }
1043
- if (streak < cfg.explore.warnAt) return allow;
1044
- if (streak >= cfg.explore.blockAt) {
1045
- return [
1046
- {
1047
- kind: "explore",
1048
- severity: "block",
1049
- tool: proposed.tool,
1050
- message: `Exploration without implementation: ${streak} consecutive read/search calls with no action in the last ${cfg.explore.windowMinutes} minutes. You have gathered enough \u2014 pick the most valuable thing you learned and act on it (edit, run, or write). If nothing is actionable, summarize what you found and say so.`,
1051
- evidence: `streak=${streak} blockAt=${cfg.explore.blockAt}`
1052
- }
1053
- ];
1054
- }
1055
- return [
1056
- {
1057
- kind: "explore",
1058
- severity: "warn",
1059
- tool: proposed.tool,
1060
- message: `${streak} consecutive read/search calls \u2014 make sure the next step acts on what you already learned.`,
1061
- evidence: `streak=${streak}`
1062
- }
1063
- ];
1064
- }
1065
- function fuzzyKey(tool, argsJson) {
1066
- let text = argsJson;
1067
- try {
1068
- const obj = JSON.parse(argsJson);
1069
- const parts = [];
1070
- for (const v of Object.values(obj)) {
1071
- if (typeof v === "string") parts.push(v);
1072
- else if (v !== null && v !== void 0) parts.push(JSON.stringify(v));
1073
- }
1074
- if (parts.length > 0) text = parts.join("|");
1075
- } catch {
1076
- }
1077
- return `${tool}:${text.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]/g, "")}`;
1078
- }
1079
- function analyzeNearRepeat(history, cfg, now = Date.now()) {
1080
- if (!cfg.nearRepeat.enabled) return allow;
1081
- const since = now - cfg.nearRepeat.windowMinutes * 6e4;
1082
- const byKey = /* @__PURE__ */ new Map();
1083
- for (const r of history) {
1084
- if (r.ts < since) continue;
1085
- const key = fuzzyKey(r.tool_name, r.args_json);
1086
- const cur = byKey.get(key) ?? { n: 0, tool: r.tool_name };
1087
- cur.n++;
1088
- byKey.set(key, cur);
1089
- }
1090
- const findings = [];
1091
- for (const [, v] of byKey) {
1092
- if (v.n >= cfg.nearRepeat.blockAt) {
1093
- findings.push({
1094
- kind: "nearRepeat",
1095
- severity: "block",
1096
- tool: v.tool,
1097
- message: `Near-duplicate loop: ${v.tool} has been called ${v.n} times with arguments that differ only trivially (punctuation, case, spacing, order). You are not trying anything new. Use the results already in context or change the approach substantially.`,
1098
- evidence: `fuzzy_count=${v.n} blockAt=${cfg.nearRepeat.blockAt}`
1099
- });
1100
- } else if (v.n >= cfg.nearRepeat.warnAt) {
1101
- findings.push({
1102
- kind: "nearRepeat",
1103
- severity: "warn",
1104
- tool: v.tool,
1105
- message: `${v.tool} has ${v.n} near-identical calls recently \u2014 verify these calls differ meaningfully.`,
1106
- evidence: `fuzzy_count=${v.n}`
1107
- });
1108
- }
1109
- }
1110
- return findings.sort((a, b) => a.severity === b.severity ? 0 : a.severity === "block" ? -1 : 1).slice(0, 1);
1111
- }
1112
- function analyzeCall(history, proposed, cfg, now = Date.now()) {
1113
- const findings = [
1114
- ...analyzeRepetition(history, proposed, cfg, now),
1115
- ...analyzeCycles(history, cfg, now),
1116
- ...analyzeNoGain(history, cfg, now),
1117
- ...analyzeChurn(history, cfg, now),
1118
- ...analyzeNoProgress(history, proposed, cfg, now),
1119
- ...analyzeNearRepeat(history, cfg, now),
1120
- ...analyzeExplore(history, proposed, cfg, now)
1121
- ];
1122
- const rank = { block: 0, warn: 1 };
1123
- findings.sort((a, b) => rank[a.severity] - rank[b.severity]);
1124
- return { findings: findings.slice(0, 2) };
1125
- }
242
+ import fs3 from "fs";
1126
243
 
1127
244
  // src/policy.ts
1128
245
  var KILL_SWITCH_MESSAGE = "[agent-guard] CIRCUIT BREAK: this session has hit the intervention limit. Stop making tool calls immediately. Do not attempt to work around this guard. Summarize what you have learned so far, state what remains blocked and why, and end your turn so the user can review the situation.";
@@ -1244,9 +361,9 @@ async function refreshPreciseUsage(cfg, env = process.env) {
1244
361
  if (fresh) return fresh;
1245
362
  const base = (cfg.preciseUrl || DEFAULT_URL).replace(/\/+$/, "");
1246
363
  const key = env.KIMI_API_KEY.trim();
1247
- for (const path7 of ["/usages", "/usage"]) {
364
+ for (const path4 of ["/usages", "/usage"]) {
1248
365
  try {
1249
- const res = await fetch(base + path7, {
366
+ const res = await fetch(base + path4, {
1250
367
  headers: { Authorization: `Bearer ${key}`, "User-Agent": "KimiCLI/1.6" },
1251
368
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
1252
369
  });
@@ -1383,204 +500,6 @@ function formatSnapshot(snap) {
1383
500
  ].join("\n");
1384
501
  }
1385
502
 
1386
- // src/checkpoint.ts
1387
- import fs6 from "fs";
1388
- import path5 from "path";
1389
- function argSummary(argsJson, max = 100) {
1390
- try {
1391
- const obj = JSON.parse(argsJson);
1392
- const parts = [];
1393
- for (const [k, v] of Object.entries(obj)) {
1394
- const s = typeof v === "string" ? v : JSON.stringify(v);
1395
- parts.push(`${k}=${s.length > 60 ? s.slice(0, 57) + "..." : s}`);
1396
- }
1397
- const joined = parts.join(", ");
1398
- return joined.length > max ? joined.slice(0, max - 3) + "..." : joined || "{}";
1399
- } catch {
1400
- return argsJson.slice(0, max);
1401
- }
1402
- }
1403
- function buildBrief(sessionId, now = Date.now(), windowMs = 6 * 36e5, cfg = loadConfig()) {
1404
- const calls = callsSince(sessionId, now - windowMs, 1e3);
1405
- if (calls.length === 0) return "";
1406
- const shells = shellTools(cfg);
1407
- const edits = editTools(cfg);
1408
- const reads = readTools(cfg);
1409
- const searchesSet = searchTools(cfg);
1410
- const files = /* @__PURE__ */ new Map();
1411
- const commands = [];
1412
- const searches = [];
1413
- const failures = [];
1414
- for (const r of calls) {
1415
- const summary = argSummary(r.args_json, 90);
1416
- if (edits.has(r.tool_name) && r.file_path) {
1417
- const f = files.get(r.file_path) ?? { reads: 0, edits: 0 };
1418
- f.edits++;
1419
- files.set(r.file_path, f);
1420
- } else if (r.file_path && reads.has(r.tool_name)) {
1421
- const f = files.get(r.file_path) ?? { reads: 0, edits: 0 };
1422
- f.reads++;
1423
- files.set(r.file_path, f);
1424
- }
1425
- if (shells.has(r.tool_name)) commands.push(summary);
1426
- if (searchesSet.has(r.tool_name)) searches.push(summary);
1427
- if (r.status === "failure") failures.push(`${r.tool_name}: ${summary}`);
1428
- }
1429
- const lines = [];
1430
- lines.push("## Observed activity (auto-captured by agent-guard)");
1431
- lines.push("");
1432
- if (files.size > 0) {
1433
- lines.push("### Files touched");
1434
- for (const [f, c] of [...files.entries()].slice(0, 25)) {
1435
- lines.push(`- ${f} (read \xD7${c.reads}, edited \xD7${c.edits})`);
1436
- }
1437
- lines.push("");
1438
- }
1439
- if (commands.length > 0) {
1440
- lines.push("### Commands run (most recent last)");
1441
- for (const c of commands.slice(-10)) lines.push(`- ${c}`);
1442
- lines.push("");
1443
- }
1444
- if (searches.length > 0) {
1445
- lines.push("### Searches performed (results are already known \u2014 do not redo them)");
1446
- const seen = /* @__PURE__ */ new Set();
1447
- for (const s of searches.slice(-15)) {
1448
- if (seen.has(s)) continue;
1449
- seen.add(s);
1450
- lines.push(`- ${s}`);
1451
- }
1452
- lines.push("");
1453
- }
1454
- if (failures.length > 0) {
1455
- lines.push("### Failed calls (avoid repeating these)");
1456
- const seen = /* @__PURE__ */ new Set();
1457
- for (const f of failures.slice(-8)) {
1458
- if (seen.has(f)) continue;
1459
- seen.add(f);
1460
- lines.push(`- ${f}`);
1461
- }
1462
- lines.push("");
1463
- }
1464
- lines.push(`Total recorded tool calls in window: ${calls.length}`);
1465
- return lines.join("\n");
1466
- }
1467
- function captureCheckpoint(sessionId, reason, now = Date.now(), cfg = loadConfig()) {
1468
- const brief = buildBrief(sessionId, now, 6 * 36e5, cfg);
1469
- if (!brief) return null;
1470
- const dir = path5.join(guardHome(), "checkpoints", sessionId.replace(/[^\w.-]/g, "_"));
1471
- fs6.mkdirSync(dir, { recursive: true });
1472
- const file = path5.join(dir, `${now}-${reason.replace(/[^\w-]/g, "_")}.md`);
1473
- const header = [
1474
- `# agent-guard checkpoint`,
1475
- ``,
1476
- `- session: ${sessionId}`,
1477
- `- time: ${new Date(now).toISOString()}`,
1478
- `- reason: ${reason}`,
1479
- ``
1480
- ].join("\n");
1481
- fs6.writeFileSync(file, header + brief + "\n", "utf8");
1482
- recordEvent(sessionId, "checkpoint", { reason, file }, now);
1483
- return { sessionId, path: file, brief, reason, ts: now };
1484
- }
1485
- function latestSessionId() {
1486
- const sessions = knownSessions(1);
1487
- return sessions[0]?.session_id ?? null;
1488
- }
1489
- function latestCheckpointFile(sessionId) {
1490
- const base = path5.join(guardHome(), "checkpoints");
1491
- if (!fs6.existsSync(base)) return null;
1492
- let dir = sessionId ? path5.join(base, sessionId.replace(/[^\w.-]/g, "_")) : "";
1493
- if (!dir || !fs6.existsSync(dir)) {
1494
- const dirs = fs6.readdirSync(base).map((d) => ({ d, m: fs6.statSync(path5.join(base, d)).mtimeMs })).sort((a, b) => b.m - a.m);
1495
- if (dirs.length === 0) return null;
1496
- dir = path5.join(base, dirs[0].d);
1497
- }
1498
- const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().reverse();
1499
- return files[0] ? path5.join(dir, files[0]) : null;
1500
- }
1501
- function renderResumeBlock(brief, reason) {
1502
- return [
1503
- `<agent-guard-resume reason="${reason}">`,
1504
- "You are resuming a task that was interrupted. Use the observed state below as verified",
1505
- "prior knowledge. Do NOT re-explore files you have already read, do NOT redo searches",
1506
- "listed here, and do NOT repeat failed calls. Continue from the last known state.",
1507
- "",
1508
- brief,
1509
- "</agent-guard-resume>"
1510
- ].join("\n");
1511
- }
1512
-
1513
- // src/verify.ts
1514
- var DEFAULT_CLAIM_PATTERNS = [
1515
- /\btests?\b.{0,40}\b(pass(?:ed|ing)?|green)\b/i,
1516
- /\ball\b.{0,24}\btests?\b.{0,24}\bpass/i,
1517
- /\bbuild\b.{0,30}\b(succeed(?:ed)?|passed|ok)\b/i,
1518
- /\bcompil(?:es?|ed)\b.{0,20}\bsuccessfully\b/i,
1519
- /\blint\b.{0,30}\b(clean|passed|no issues)\b/i,
1520
- /\bfixed\b.{0,40}\b(all|every)\b/i,
1521
- /测试(全部|都)?通过/,
1522
- /全部(测试)?通过/,
1523
- /构建成功/,
1524
- /编译通过/,
1525
- /零(错误|警告)/,
1526
- /问题已全部解决/
1527
- ];
1528
- var DEFAULT_EVIDENCE_PATTERNS = [
1529
- /\b(test|tests|vitest|jest|mocha|pytest|cargo test|go test|make test)\b/i,
1530
- /\b(npm|pnpm|yarn)\s+(run\s+)?(test|check)\b/i,
1531
- /\b(mvn|gradle|sbt|dotnet\s+test)\b/i,
1532
- /\b(tsc|pyright|mypy|eslint|biome|ruff|flake8|clippy)\b/i,
1533
- /\b(build|compile|lint|check|verify)\b/i,
1534
- /\bmake\b/i
1535
- ];
1536
- function findClaims(text, cfg) {
1537
- if (!text) return [];
1538
- const patterns = cfg.verify.claimPatterns.length > 0 ? cfg.verify.claimPatterns.map((p) => new RegExp(p)) : DEFAULT_CLAIM_PATTERNS;
1539
- const claims = [];
1540
- for (const p of patterns) {
1541
- const m = p.exec(text);
1542
- if (m) {
1543
- claims.push({
1544
- pattern: String(p),
1545
- snippet: text.slice(Math.max(0, (m.index ?? 0) - 40), (m.index ?? 0) + m[0].length + 40).replace(/\s+/g, " ").trim()
1546
- });
1547
- }
1548
- if (claims.length >= 3) break;
1549
- }
1550
- return claims;
1551
- }
1552
- function hasEvidence(sessionId, cfg, now = Date.now()) {
1553
- const vouched = getMeta(`vouched:${sessionId}`) === "1";
1554
- if (vouched) return true;
1555
- const since = now - cfg.verify.evidenceWindowMinutes * 6e4;
1556
- const patterns = cfg.verify.evidencePatterns.length > 0 ? cfg.verify.evidencePatterns.map((p) => new RegExp(p)) : DEFAULT_EVIDENCE_PATTERNS;
1557
- const shells = shellTools(cfg);
1558
- const calls = callsSince(sessionId, since, 400);
1559
- for (const r of calls) {
1560
- if (r.status !== "ok") continue;
1561
- if (!shells.has(r.tool_name)) continue;
1562
- try {
1563
- const args = JSON.parse(r.args_json);
1564
- const cmd = args.command ?? "";
1565
- for (const p of patterns) {
1566
- if (p.test(cmd)) return true;
1567
- }
1568
- } catch {
1569
- continue;
1570
- }
1571
- }
1572
- return false;
1573
- }
1574
- function hasRecentEdits(sessionId, cfg, now = Date.now()) {
1575
- const since = now - cfg.verify.evidenceWindowMinutes * 6e4;
1576
- const edits = editTools(cfg);
1577
- return callsSince(sessionId, since, 400).some(
1578
- (r) => r.status === "ok" && edits.has(r.tool_name)
1579
- );
1580
- }
1581
- var HOOKS_STOP_BLOCK_REASON = "[agent-guard] Blocked (verify): this turn ended after successful file edits with no successful verification command (test/build/lint) in the session. Run your verification before claiming completion, or state explicitly why it cannot run here.";
1582
- var WIRE_VERIFY_CORRECTIVE = "[agent-guard verification] Your final message claims tests/build pass, but no successful verification command was recorded in this session. Actually run the verification now and base your claims on real results, then restate the conclusion.";
1583
-
1584
503
  // src/guard.ts
1585
504
  function probeEnabled() {
1586
505
  if (process.env.KIMI_GUARD_PROBE === "1") return true;
@@ -1592,7 +511,7 @@ function probeEnabled() {
1592
511
  }
1593
512
  function appendProbe(event, payload) {
1594
513
  const line = JSON.stringify({ ts: Date.now(), event, payload }) + "\n";
1595
- fs7.appendFileSync(probeLogPath(), line, "utf8");
514
+ fs3.appendFileSync(probeLogPath(), line, "utf8");
1596
515
  }
1597
516
  async function readStdinJson() {
1598
517
  const chunks = [];
@@ -1626,7 +545,7 @@ function processHookEvent(event, cfg, payload, now = Date.now()) {
1626
545
  case "PostToolUseFailure":
1627
546
  return handlePostTool(event, cfg, payload, sessionId, now);
1628
547
  case "Stop": {
1629
- if (cfg.harness === "claude") recordEvent(sessionId, "turn", { origin: "stop" }, now);
548
+ if (cfg.harness !== "kimi") recordEvent(sessionId, "turn", { origin: "stop" }, now);
1630
549
  if (!cfg.verify.enabled || !cfg.verify.blockOnNoEvidence) return { code: 0 };
1631
550
  if (!hasRecentEdits(sessionId, cfg, now)) return { code: 0 };
1632
551
  if (hasEvidence(sessionId, cfg, now)) return { code: 0 };
@@ -1746,6 +665,7 @@ function handlePostTool(event, cfg, payload, sessionId, now) {
1746
665
  argsHash: call.argsHash,
1747
666
  argsJson: call.argsJson,
1748
667
  outputHash: call.outputHash,
668
+ outputSample: call.outputSample,
1749
669
  filePath: call.filePath,
1750
670
  status: call.status,
1751
671
  ts: now
@@ -1755,7 +675,7 @@ function handlePostTool(event, cfg, payload, sessionId, now) {
1755
675
 
1756
676
  // src/hook.ts
1757
677
  function encodeHint(event, harness, text) {
1758
- if (harness === "claude") {
678
+ if (harness !== "kimi") {
1759
679
  return JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text } });
1760
680
  }
1761
681
  return text;
@@ -1781,82 +701,9 @@ async function runHook(event, harness = "kimi") {
1781
701
  }
1782
702
 
1783
703
  // src/status.ts
1784
- import fs8 from "fs";
704
+ import fs4 from "fs";
1785
705
  import { spawnSync } from "child_process";
1786
706
  import pc from "picocolors";
1787
-
1788
- // src/veto.ts
1789
- function vetoKeyConfigured(env = process.env) {
1790
- return Boolean(env.KIMI_GUARD_VETO_API_KEY?.trim());
1791
- }
1792
- function vetoBaseUrls(cfg, env = process.env) {
1793
- return {
1794
- baseUrl: env.KIMI_GUARD_VETO_BASE_URL?.trim() || cfg.baseUrl,
1795
- model: env.KIMI_GUARD_VETO_MODEL?.trim() || cfg.model
1796
- };
1797
- }
1798
- function collectVetoContext(sessionId, cfg, now = Date.now()) {
1799
- const since = now - cfg.verify.evidenceWindowMinutes * 6e4;
1800
- const calls = callsSince(sessionId, since, 400);
1801
- const shells = shellTools(cfg);
1802
- const recentCommands = [];
1803
- const editedFiles = [];
1804
- for (const r of calls.slice(-40)) {
1805
- if (shells.has(r.tool_name)) {
1806
- try {
1807
- const args = JSON.parse(r.args_json);
1808
- if (args.command) recentCommands.push(args.command.slice(0, 120));
1809
- } catch {
1810
- }
1811
- }
1812
- if (r.file_path && editedFiles.length < 10) editedFiles.push(r.file_path);
1813
- }
1814
- return { sessionId, claims: [], goal: "", recentCommands: recentCommands.slice(-5), editedFiles };
1815
- }
1816
- var PROMPT_HEADER = "You are a false-positive detector for an AI-agent guardrail. An agent just finished its turn claiming completion, but the session's recorded command history contains NO successful verification command (test/build/lint). Decide whether blocking would be a FALSE POSITIVE \u2014 i.e. the agent has a legitimate reason why verification cannot run in this session.\nRules: base your vote ONLY on the facts below. A claim that verification is unnecessary or happens elsewhere is NOT by itself a reason to veto. Answer with EXACTLY one line:\nVETO: yes (false positive \u2014 allow the completion)\nVETO: no (block stands \u2014 the agent must actually run verification)\nDo not write anything else.\n\nFacts:\n";
1817
- function buildVetoPrompt(ctx) {
1818
- const lines = [];
1819
- lines.push(`- user goal: ${ctx.goal.slice(0, 300) || "(unknown)"}`);
1820
- lines.push(`- claims made by the agent:`);
1821
- for (const c of ctx.claims.slice(0, 3)) lines.push(` "${c.snippet}"`);
1822
- lines.push(`- recent commands the agent ran: ${ctx.recentCommands.length > 0 ? ctx.recentCommands.join(" ; ") : "(none)"}`);
1823
- lines.push(`- files the agent edited: ${ctx.editedFiles.length > 0 ? ctx.editedFiles.join(", ") : "(none)"}`);
1824
- lines.push("- recorded successful verification commands in session history: none");
1825
- return PROMPT_HEADER + lines.join("\n");
1826
- }
1827
- async function castVetoVote(ctx, cfg, env = process.env) {
1828
- if (!cfg.enabled || !vetoKeyConfigured(env)) return { vetoed: false, error: "veto disabled" };
1829
- const calls = Number(getMeta(`veto_calls:${ctx.sessionId}`) ?? "0");
1830
- if (calls >= cfg.maxCallsPerSession) return { vetoed: false, error: "session vote budget exhausted" };
1831
- setMeta(`veto_calls:${ctx.sessionId}`, String(calls + 1));
1832
- const { baseUrl, model } = vetoBaseUrls(cfg, env);
1833
- const key = env.KIMI_GUARD_VETO_API_KEY.trim();
1834
- try {
1835
- const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
1836
- method: "POST",
1837
- headers: {
1838
- "Content-Type": "application/json",
1839
- Authorization: `Bearer ${key}`
1840
- },
1841
- body: JSON.stringify({
1842
- model,
1843
- messages: [{ role: "user", content: buildVetoPrompt(ctx) }],
1844
- max_tokens: 8,
1845
- temperature: 0,
1846
- stream: false
1847
- }),
1848
- signal: AbortSignal.timeout(cfg.timeoutMs)
1849
- });
1850
- if (!res.ok) return { vetoed: false, error: `http ${res.status}` };
1851
- const data = await res.json();
1852
- const raw = (data.choices?.[0]?.message?.content ?? "").trim();
1853
- return { vetoed: /^VETO:\s*yes\b/i.test(raw), raw };
1854
- } catch (err) {
1855
- return { vetoed: false, error: err.message };
1856
- }
1857
- }
1858
-
1859
- // src/status.ts
1860
707
  function ok(msg) {
1861
708
  console.log(`${pc.green("\u2713")} ${msg}`);
1862
709
  }
@@ -1870,6 +717,7 @@ var CALIBRATION_KEYS = {
1870
717
  repeat: "repeat.maxRepeats",
1871
718
  nearRepeat: "nearRepeat.blockAt",
1872
719
  noGain: "noGain.blockAt",
720
+ noGainFuzzy: "noGain.fuzzyBlockAt",
1873
721
  churn: "churn.blockAt",
1874
722
  noProgress: "noProgress.blockAt",
1875
723
  explore: "explore.blockAt",
@@ -1924,7 +772,7 @@ function agentProcessRunning() {
1924
772
  return r.stdout.split("\n").some((line) => {
1925
773
  const l = line.trim();
1926
774
  if (l.includes("agentguard") || l.includes("kimi-guard") || l.includes("kguard")) return false;
1927
- return /(?:^|[/\s])(kimi|claude)(?:\s|$)/.test(l);
775
+ return /(?:^|[/\s])(kimi|claude|codex)(?:\s|$)/.test(l);
1928
776
  });
1929
777
  } catch {
1930
778
  return false;
@@ -1993,17 +841,18 @@ function cmdDoctor() {
1993
841
  `node ${process.versions.node} is too old (need >=22.13 for node:sqlite)`
1994
842
  );
1995
843
  try {
1996
- fs8.mkdirSync(guardHome(), { recursive: true });
1997
- fs8.accessSync(guardHome(), fs8.constants.W_OK);
844
+ fs4.mkdirSync(guardHome(), { recursive: true });
845
+ fs4.accessSync(guardHome(), fs4.constants.W_OK);
1998
846
  check(true, `state dir writable: ${guardHome()}`, "");
1999
847
  openDb().prepare("SELECT 1").get();
2000
- check(true, `state db opens (schema v2): ${stateDbPath()}`, "");
848
+ check(true, `state db opens: ${stateDbPath()}`, "");
2001
849
  } catch (err) {
2002
850
  check(false, "", `state dir/db problem: ${err.message}`);
2003
851
  }
2004
852
  const kimi = detectKimiConfig();
2005
853
  const hasClaude = claudeDetected();
2006
- if (kimi.exists || !hasClaude) {
854
+ const hasCodex = codexDetected();
855
+ if (kimi.exists || !hasClaude && !hasCodex) {
2007
856
  check(
2008
857
  kimi.exists,
2009
858
  `kimi config found: ${kimi.path}`,
@@ -2024,7 +873,16 @@ function cmdDoctor() {
2024
873
  } else {
2025
874
  console.log(` ${pc.dim("-")} claude code not detected (skipped)`);
2026
875
  }
2027
- const kimiConfigText = fs8.existsSync(kimi.path) ? fs8.readFileSync(kimi.path, "utf8") : "";
876
+ if (codexDetected()) {
877
+ check(
878
+ codexHooksInstalled(),
879
+ `[codex] hooks present in ${codexHooksPath()}`,
880
+ `[codex] hooks not installed \u2014 run: agentguard install --harness codex`
881
+ );
882
+ } else {
883
+ console.log(` ${pc.dim("-")} codex not detected (skipped)`);
884
+ }
885
+ const kimiConfigText = fs4.existsSync(kimi.path) ? fs4.readFileSync(kimi.path, "utf8") : "";
2028
886
  const hasSecurityLayer = /kimi-boost managed|destructive|secret-guard|branch-guard|block-dangerous/i.test(kimiConfigText);
2029
887
  if (hasSecurityLayer) {
2030
888
  ok("security-layer hooks detected (authorization axis covered)");
@@ -2038,8 +896,8 @@ function cmdDoctor() {
2038
896
  "agentguard is not on PATH \u2014 hook commands will fail-open. Install globally: npm i -g @shidesheng0218/agentguard"
2039
897
  );
2040
898
  const probeFile = probeLogPath();
2041
- if (fs8.existsSync(probeFile)) {
2042
- const lines = fs8.readFileSync(probeFile, "utf8").trim().split("\n").filter(Boolean);
899
+ if (fs4.existsSync(probeFile)) {
900
+ const lines = fs4.readFileSync(probeFile, "utf8").trim().split("\n").filter(Boolean);
2043
901
  ok(`probe log has ${lines.length} samples (${probeFile})`);
2044
902
  const keys = /* @__PURE__ */ new Map();
2045
903
  for (const line of lines.slice(-50)) {
@@ -2055,7 +913,7 @@ function cmdDoctor() {
2055
913
  } else {
2056
914
  warn("no probe samples yet \u2014 run 'kguard probe on', use Kimi Code a bit, then 'kguard doctor'");
2057
915
  }
2058
- if (fs8.existsSync(userConfigPath())) ok(`config present: ${userConfigPath()}`);
916
+ if (fs4.existsSync(userConfigPath())) ok(`config present: ${userConfigPath()}`);
2059
917
  else warn("no user config (defaults in effect) \u2014 run 'kguard config init' to create one");
2060
918
  try {
2061
919
  const normalizeMisses = Number(getMeta("normalize_misses") ?? "0");
@@ -2098,8 +956,8 @@ ${failures} check(s) failed.`);
2098
956
  }
2099
957
 
2100
958
  // src/wire/supervisor.ts
2101
- import fs9 from "fs";
2102
- import path6 from "path";
959
+ import fs5 from "fs";
960
+ import path3 from "path";
2103
961
 
2104
962
  // src/wire/client.ts
2105
963
  import { spawn } from "child_process";
@@ -2201,12 +1059,12 @@ var WireClient = class {
2201
1059
  }
2202
1060
  async request(method, params, timeoutMs = this.opts.requestTimeoutMs ?? 6e5) {
2203
1061
  const id = this.id();
2204
- const promise = new Promise((resolve2, reject) => {
1062
+ const promise = new Promise((resolve, reject) => {
2205
1063
  const timer = setTimeout(() => {
2206
1064
  this.pending.delete(id);
2207
1065
  reject(new Error(`wire request timeout: ${method}`));
2208
1066
  }, timeoutMs);
2209
- this.pending.set(id, { resolve: resolve2, reject, timer });
1067
+ this.pending.set(id, { resolve, reject, timer });
2210
1068
  });
2211
1069
  this.write({ jsonrpc: "2.0", method, id, params });
2212
1070
  return promise;
@@ -2260,12 +1118,12 @@ function addUsage(acc, u) {
2260
1118
  async function runSupervised(opts) {
2261
1119
  const cfg = opts.config ?? loadConfig();
2262
1120
  const runId = `run-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${process.pid}`;
2263
- const logDir = path6.join(guardHome(), "runs", runId);
2264
- fs9.mkdirSync(logDir, { recursive: true });
2265
- const logPath = path6.join(logDir, "wire.jsonl");
1121
+ const logDir = path3.join(guardHome(), "runs", runId);
1122
+ fs5.mkdirSync(logDir, { recursive: true });
1123
+ const logPath = path3.join(logDir, "wire.jsonl");
2266
1124
  const rawLog = (direction, line) => {
2267
1125
  try {
2268
- fs9.appendFileSync(logPath, JSON.stringify({ dir: direction, ts: Date.now(), line }) + "\n");
1126
+ fs5.appendFileSync(logPath, JSON.stringify({ dir: direction, ts: Date.now(), line }) + "\n");
2269
1127
  } catch {
2270
1128
  }
2271
1129
  };
@@ -2290,7 +1148,7 @@ async function runSupervised(opts) {
2290
1148
  verifyRounds: 0,
2291
1149
  vetoes: 0,
2292
1150
  thinkingDominance: 0,
2293
- reportPath: path6.join(logDir, "report.json"),
1151
+ reportPath: path3.join(logDir, "report.json"),
2294
1152
  logPath
2295
1153
  };
2296
1154
  let steersSent = 0;
@@ -2474,7 +1332,8 @@ async function runSupervised(opts) {
2474
1332
  argsHash: fingerprint(call.name, call.args),
2475
1333
  argsJson: JSON.stringify(call.args).slice(0, 2048),
2476
1334
  outputHash: hashOutput(output),
2477
- filePath: extractFile2(call.args),
1335
+ outputSample: outputSampleOf(output),
1336
+ filePath: extractFile(call.args),
2478
1337
  status: returnValue.is_error ? "failure" : "ok"
2479
1338
  });
2480
1339
  }
@@ -2631,12 +1490,12 @@ ${brief.brief}
2631
1490
  report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2632
1491
  recordEvent(sessionId(), "run_end", { endReason: report.endReason, blocks: report.blocks.length });
2633
1492
  try {
2634
- fs9.writeFileSync(report.reportPath, JSON.stringify(report, null, 2), "utf8");
1493
+ fs5.writeFileSync(report.reportPath, JSON.stringify(report, null, 2), "utf8");
2635
1494
  } catch {
2636
1495
  }
2637
1496
  return report;
2638
1497
  }
2639
- function extractFile2(args) {
1498
+ function extractFile(args) {
2640
1499
  if (args === null || typeof args !== "object") return null;
2641
1500
  const obj = args;
2642
1501
  for (const k of ["file_path", "filePath", "path", "file", "filename"]) {
@@ -2670,15 +1529,17 @@ function formatReport(r) {
2670
1529
  // src/cli.ts
2671
1530
  var program = new Command();
2672
1531
  program.name("agentguard").description("Runtime behavior guard for coding agents (Kimi Code CLI & Claude Code): loop detection, quota gates, checkpoints").version(version, "-V, --version", "print version");
2673
- program.command("install").description("install hook rules into detected agent CLIs (Kimi Code config.toml and/or Claude Code settings.json)").option("--compat", "legacy-safe mode: only the 3 universally supported hook events (for older kimi-cli versions)").option("--harness <name>", "kimi | claude | all (default: auto-detect installed harnesses, fallback kimi)").action((opts) => {
1532
+ program.command("install").description("install hook rules into detected agent CLIs (Kimi Code config.toml and/or Claude Code settings.json)").option("--compat", "legacy-safe mode: only the 3 universally supported hook events (for older kimi-cli versions)").option("--harness <name>", "kimi | claude | codex | all (default: auto-detect installed harnesses, fallback kimi)").action((opts) => {
2674
1533
  const which = opts.harness ?? "auto";
2675
- const doKimi = which === "kimi" || which === "all" || which === "auto";
2676
- const doClaude = which === "claude" || which === "all" || which === "auto" && claudeDetected();
2677
- if (!doKimi && !doClaude) {
1534
+ const known = ["kimi", "claude", "codex", "all", "auto"];
1535
+ if (!known.includes(which)) {
2678
1536
  console.error(`unknown harness: ${which}`);
2679
1537
  process.exitCode = 1;
2680
1538
  return;
2681
1539
  }
1540
+ const doKimi = which === "kimi" || which === "all" || which === "auto";
1541
+ const doClaude = which === "claude" || which === "all" || which === "auto" && claudeDetected();
1542
+ const doCodex = which === "codex" || which === "all" || which === "auto" && codexDetected();
2682
1543
  if (doKimi) {
2683
1544
  const r = installHooks("agentguard", Boolean(opts.compat));
2684
1545
  console.log(`\u2713 [kimi] config: ${r.configPath}${r.created ? " (created)" : ""}`);
@@ -2694,9 +1555,16 @@ program.command("install").description("install hook rules into detected agent C
2694
1555
  if (r.backupPath) console.log(`\u2713 [claude] backup: ${r.backupPath}`);
2695
1556
  console.log(`\u2713 [claude] hooks ${r.updated ? "installed" : "already up to date"}${opts.compat ? " (compat)" : ""}`);
2696
1557
  }
1558
+ if (doCodex) {
1559
+ const r = installCodexHooks("agentguard", Boolean(opts.compat));
1560
+ console.log(`\u2713 [codex] config: ${r.configPath}${r.created ? " (created)" : ""}`);
1561
+ if (r.backupPath) console.log(`\u2713 [codex] backup: ${r.backupPath}`);
1562
+ console.log(`\u2713 [codex] hooks ${r.updated ? "installed" : "already up to date"}${opts.compat ? " (compat)" : ""}`);
1563
+ console.log(" note: Codex hooks cover shell/apply_patch/local function tools; hosted tools (e.g. WebSearch) are not observable.");
1564
+ }
2697
1565
  console.log(" restart the agent CLI (or /reload) to take effect.");
2698
1566
  });
2699
- program.command("uninstall").description("remove the managed hook entries from Kimi Code config.toml and Claude Code settings.json").option("--harness <name>", "kimi | claude | all (default: all)").action((opts) => {
1567
+ program.command("uninstall").description("remove the managed hook entries from Kimi Code config.toml and Claude Code settings.json").option("--harness <name>", "kimi | claude | codex | all (default: all)").action((opts) => {
2700
1568
  const which = opts.harness ?? "all";
2701
1569
  if (which === "kimi" || which === "all") {
2702
1570
  const r = uninstallHooks();
@@ -2706,9 +1574,13 @@ program.command("uninstall").description("remove the managed hook entries from K
2706
1574
  const r = uninstallClaudeHooks();
2707
1575
  console.log(r.removed ? `\u2713 [claude] removed hooks from ${r.configPath}` : `[claude] no managed hooks found in ${r.configPath}`);
2708
1576
  }
1577
+ if (which === "codex" || which === "all") {
1578
+ const r = uninstallCodexHooks();
1579
+ console.log(r.removed ? `\u2713 [codex] removed hooks from ${r.configPath}` : `[codex] no managed hooks found in ${r.configPath}`);
1580
+ }
2709
1581
  });
2710
- program.command("hook").argument("<event>", "hook event name, e.g. PreToolUse").description("hook entrypoint invoked by the agent CLI (reads JSON payload from stdin)").option("--harness <name>", "kimi | claude (default: kimi)", "kimi").action(async (event, opts) => {
2711
- const harness = opts.harness === "claude" ? "claude" : "kimi";
1582
+ program.command("hook").argument("<event>", "hook event name, e.g. PreToolUse").description("hook entrypoint invoked by the agent CLI (reads JSON payload from stdin)").option("--harness <name>", "kimi | claude | codex (default: kimi)", "kimi").action(async (event, opts) => {
1583
+ const harness = opts.harness === "claude" ? "claude" : opts.harness === "codex" ? "codex" : "kimi";
2712
1584
  process.exitCode = await runHook(event, harness);
2713
1585
  });
2714
1586
  program.command("status").description("guard activity: calls, interventions, sessions, budget windows").action(() => cmdStatus());
@@ -2745,11 +1617,11 @@ program.command("checkpoint").description("capture a research-state checkpoint f
2745
1617
  });
2746
1618
  program.command("resume").description("print a paste-ready context block built from the latest checkpoint").option("-f, --file <path>", "use a specific checkpoint file (defaults to the latest)").action((opts) => {
2747
1619
  const file = opts.file ?? latestCheckpointFile();
2748
- if (!file || !fs10.existsSync(file)) {
1620
+ if (!file || !fs6.existsSync(file)) {
2749
1621
  console.log("no checkpoints found \u2014 run 'kguard checkpoint' first");
2750
1622
  return;
2751
1623
  }
2752
- const content = fs10.readFileSync(file, "utf8");
1624
+ const content = fs6.readFileSync(file, "utf8");
2753
1625
  const reason = /- reason: (.*)/.exec(content)?.[1] ?? "interrupted";
2754
1626
  const idx = content.indexOf("## Observed activity");
2755
1627
  const brief = idx >= 0 ? content.slice(idx) : content;
@@ -2806,11 +1678,11 @@ probe.command("off").action(() => {
2806
1678
  console.log("\u2713 probe off");
2807
1679
  });
2808
1680
  probe.command("show").option("-n, --last <n>", "how many samples to show", "10").action((opts) => {
2809
- if (!fs10.existsSync(probeLogPath())) {
1681
+ if (!fs6.existsSync(probeLogPath())) {
2810
1682
  console.log("no probe samples yet \u2014 run 'kguard probe on' first");
2811
1683
  return;
2812
1684
  }
2813
- const lines = fs10.readFileSync(probeLogPath(), "utf8").trim().split("\n").filter(Boolean);
1685
+ const lines = fs6.readFileSync(probeLogPath(), "utf8").trim().split("\n").filter(Boolean);
2814
1686
  for (const line of lines.slice(-Number(opts.last))) console.log(line);
2815
1687
  });
2816
1688
  var cfgCmd = program.command("config").description("manage the guard config.toml");
@@ -2828,12 +1700,29 @@ cfgCmd.command("get <key>").description("print one effective value, e.g. budget.
2828
1700
  const value = key.split(".").reduce((acc, k) => acc && typeof acc === "object" ? acc[k] : void 0, cfg);
2829
1701
  console.log(value === void 0 ? `<unset: ${key}>` : typeof value === "object" ? JSON.stringify(value) : String(value));
2830
1702
  });
2831
- program.command("run").description("supervised headless run: spawns the agent in Wire mode, enforces loop guards, meters tokens, auto-checkpoints").argument("[prompt...]", "task prompt (or use --prompt)").option("-p, --prompt <text>", "task prompt").option("-e, --exec <command...>", "agent command to supervise (default: kimi --wire)").option("--max-steps <n>", "hard step cap (per turn)", "200").option("--max-minutes <n>", "hard wall-clock cap for the whole run", "30").option("--auto-resume <n>", "re-prompt with checkpoint brief after max_steps/kill-switch", "0").option("--max-verify-rounds <n>", "corrective rounds when the final message makes unbacked completion claims", "2").option("--no-steer", "disable soft mid-turn corrections").option("--max-steers <n>", "cap on steer injections", "5").option("--yolo", "auto-approve every approval request").option("--json", "print machine-readable report JSON").action(async (promptParts, opts) => {
1703
+ program.command("run").description("supervised headless run: loop guards, token metering, auto-checkpoints. Kimi: Wire protocol (in-process steer); Claude Code: claude -p stream-json supervision").argument("[prompt...]", "task prompt (or use --prompt)").option("-p, --prompt <text>", "task prompt").option("-e, --exec <command...>", "agent command to supervise (default: kimi --wire / claude)").option("--harness <name>", "kimi | claude (default: kimi; claude uses claude -p stream-json)").option("--max-steps <n>", "hard step cap (per turn)", "200").option("--max-minutes <n>", "hard wall-clock cap for the whole run", "30").option("--auto-resume <n>", "re-prompt with checkpoint brief after max_steps/kill-switch", "0").option("--max-verify-rounds <n>", "corrective rounds when the final message makes unbacked completion claims", "2").option("--no-steer", "disable soft mid-turn corrections").option("--max-steers <n>", "cap on steer injections", "5").option("--yolo", "auto-approve every approval request").option("--json", "print machine-readable report JSON").action(async (promptParts, opts) => {
2832
1704
  const prompt = opts.prompt ?? promptParts.join(" ");
2833
1705
  if (!prompt.trim()) {
2834
1706
  console.error("error: a prompt is required (argument or --prompt)");
2835
1707
  process.exit(1);
2836
1708
  }
1709
+ if (opts.harness === "claude") {
1710
+ const { runClaudeSupervised } = await import("./claude-ZFYJRREP.js");
1711
+ const report2 = await runClaudeSupervised({
1712
+ prompt,
1713
+ command: opts.exec ?? ["claude"],
1714
+ maxSteps: Number(opts.maxSteps),
1715
+ maxMinutes: Number(opts.maxMinutes),
1716
+ autoResume: Number(opts.autoResume),
1717
+ maxVerifyRounds: Number(opts.maxVerifyRounds),
1718
+ approval: opts.yolo ? "approve" : "reject",
1719
+ json: Boolean(opts.json)
1720
+ });
1721
+ if (opts.json) console.log(JSON.stringify(report2, null, 2));
1722
+ else console.log(formatReport(report2));
1723
+ process.exitCode = report2.endReason === "finished" ? 0 : 2;
1724
+ return;
1725
+ }
2837
1726
  const report = await runSupervised({
2838
1727
  prompt,
2839
1728
  command: opts.exec ?? ["kimi", "--wire"],