clikit-plugin 0.2.35 → 0.2.37

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.
Files changed (53) hide show
  1. package/README.md +13 -14
  2. package/command/init.md +70 -152
  3. package/command/issue.md +1 -1
  4. package/command/plan.md +9 -4
  5. package/command/research.md +5 -5
  6. package/command/ship.md +51 -59
  7. package/command/verify.md +74 -50
  8. package/dist/.tsbuildinfo +1 -1
  9. package/dist/agents/index.d.ts.map +1 -1
  10. package/dist/cli.d.ts +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +45 -107
  13. package/dist/cli.test.d.ts +2 -0
  14. package/dist/cli.test.d.ts.map +1 -0
  15. package/dist/clikit.schema.json +154 -136
  16. package/dist/commands/index.d.ts.map +1 -1
  17. package/dist/config.d.ts +13 -0
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.test.d.ts +2 -0
  20. package/dist/config.test.d.ts.map +1 -0
  21. package/dist/hooks/error-logger.d.ts +10 -0
  22. package/dist/hooks/error-logger.d.ts.map +1 -0
  23. package/dist/hooks/index.d.ts +1 -1
  24. package/dist/hooks/index.d.ts.map +1 -1
  25. package/dist/hooks/memory-digest.d.ts +2 -0
  26. package/dist/hooks/memory-digest.d.ts.map +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +770 -154
  29. package/dist/skills/index.d.ts +10 -0
  30. package/dist/skills/index.d.ts.map +1 -1
  31. package/dist/tools/cass-memory.d.ts +61 -0
  32. package/dist/tools/cass-memory.d.ts.map +1 -0
  33. package/dist/tools/index.d.ts +1 -0
  34. package/dist/tools/index.d.ts.map +1 -1
  35. package/package.json +2 -2
  36. package/skill/cass-village/SKILL.md +217 -0
  37. package/src/agents/AGENTS.md +2 -1
  38. package/src/agents/build.md +17 -16
  39. package/src/agents/index.ts +33 -4
  40. package/src/agents/oracle.md +49 -68
  41. package/src/agents/plan.md +14 -15
  42. package/src/agents/research.md +76 -0
  43. package/src/agents/review.md +1 -1
  44. package/src/agents/vision.md +1 -1
  45. package/dist/hooks/git-guard.test.d.ts +0 -2
  46. package/dist/hooks/git-guard.test.d.ts.map +0 -1
  47. package/dist/hooks/security-check.test.d.ts +0 -2
  48. package/dist/hooks/security-check.test.d.ts.map +0 -1
  49. package/src/agents/general.md +0 -92
  50. package/src/agents/librarian.md +0 -116
  51. package/src/agents/looker.md +0 -112
  52. package/src/agents/scout.md +0 -84
  53. /package/command/{status.md → status-beads.md} +0 -0
package/dist/index.js CHANGED
@@ -3492,8 +3492,8 @@ var require_gray_matter = __commonJS((exports, module) => {
3492
3492
  });
3493
3493
 
3494
3494
  // src/index.ts
3495
- import { execFile } from "child_process";
3496
- import { promisify } from "util";
3495
+ import { execFile as execFile2 } from "child_process";
3496
+ import { promisify as promisify2 } from "util";
3497
3497
 
3498
3498
  // src/agents/index.ts
3499
3499
  var import_gray_matter = __toESM(require_gray_matter(), 1);
@@ -3501,9 +3501,20 @@ import * as fs from "fs";
3501
3501
  import * as path from "path";
3502
3502
  var AGENTS_DIR_CANDIDATES = [
3503
3503
  import.meta.dir,
3504
- path.join(import.meta.dir, "../../src/agents")
3504
+ path.join(import.meta.dir, "../src/agents"),
3505
+ path.join(import.meta.dir, "../../src/agents"),
3506
+ path.join(import.meta.dir, "../agents")
3505
3507
  ];
3506
3508
  function resolveAgentsDir() {
3509
+ for (const dir of AGENTS_DIR_CANDIDATES) {
3510
+ if (!fs.existsSync(dir))
3511
+ continue;
3512
+ try {
3513
+ const hasAgentFiles = fs.readdirSync(dir).some((f) => f.endsWith(".md") && f !== "AGENTS.md");
3514
+ if (hasAgentFiles)
3515
+ return dir;
3516
+ } catch {}
3517
+ }
3507
3518
  for (const dir of AGENTS_DIR_CANDIDATES) {
3508
3519
  if (fs.existsSync(dir)) {
3509
3520
  return dir;
@@ -3557,14 +3568,24 @@ function loadAgents() {
3557
3568
  return agents;
3558
3569
  }
3559
3570
  var _cachedAgents = null;
3560
- var _cachedAgentsMtime = 0;
3571
+ var _cachedAgentsFingerprint = "";
3572
+ function getAgentsFingerprint(agentsDir) {
3573
+ const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md") && f !== "AGENTS.md").sort();
3574
+ const parts = files.map((file) => {
3575
+ const fullPath = path.join(agentsDir, file);
3576
+ const stat = fs.statSync(fullPath);
3577
+ return `${file}:${stat.mtimeMs}`;
3578
+ });
3579
+ return parts.join("|");
3580
+ }
3561
3581
  function getBuiltinAgents() {
3562
3582
  try {
3563
- const mtime = fs.statSync(resolveAgentsDir()).mtimeMs;
3564
- if (_cachedAgents && _cachedAgentsMtime === mtime)
3583
+ const agentsDir = resolveAgentsDir();
3584
+ const fingerprint = getAgentsFingerprint(agentsDir);
3585
+ if (_cachedAgents && _cachedAgentsFingerprint === fingerprint)
3565
3586
  return _cachedAgents;
3566
3587
  _cachedAgents = loadAgents();
3567
- _cachedAgentsMtime = mtime;
3588
+ _cachedAgentsFingerprint = fingerprint;
3568
3589
  return _cachedAgents;
3569
3590
  } catch {
3570
3591
  return _cachedAgents ?? loadAgents();
@@ -3629,14 +3650,24 @@ function loadCommands() {
3629
3650
  return commands;
3630
3651
  }
3631
3652
  var _cachedCommands = null;
3632
- var _cachedCommandsMtime = 0;
3653
+ var _cachedCommandsFingerprint = "";
3654
+ function getCommandsFingerprint(commandsDir) {
3655
+ const files = fs2.readdirSync(commandsDir).filter((f) => f.endsWith(".md")).sort();
3656
+ const parts = files.map((file) => {
3657
+ const fullPath = path2.join(commandsDir, file);
3658
+ const stat = fs2.statSync(fullPath);
3659
+ return `${file}:${stat.mtimeMs}`;
3660
+ });
3661
+ return parts.join("|");
3662
+ }
3633
3663
  function getBuiltinCommands() {
3634
3664
  try {
3635
- const mtime = fs2.statSync(resolveCommandsDir()).mtimeMs;
3636
- if (_cachedCommands && _cachedCommandsMtime === mtime)
3665
+ const commandsDir = resolveCommandsDir();
3666
+ const fingerprint = getCommandsFingerprint(commandsDir);
3667
+ if (_cachedCommands && _cachedCommandsFingerprint === fingerprint)
3637
3668
  return _cachedCommands;
3638
3669
  _cachedCommands = loadCommands();
3639
- _cachedCommandsMtime = mtime;
3670
+ _cachedCommandsFingerprint = fingerprint;
3640
3671
  return _cachedCommands;
3641
3672
  } catch {
3642
3673
  return _cachedCommands ?? loadCommands();
@@ -3747,12 +3778,22 @@ var DEFAULT_CONFIG = {
3747
3778
  enabled: true,
3748
3779
  max_per_type: 10,
3749
3780
  include_types: ["decision", "learning", "blocker", "progress", "handoff"],
3781
+ index_highlights_per_type: 2,
3782
+ write_topic_files: true,
3750
3783
  log: false
3751
3784
  },
3752
3785
  todo_beads_sync: {
3753
3786
  enabled: true,
3754
3787
  close_missing: true,
3755
3788
  log: false
3789
+ },
3790
+ cass_memory: {
3791
+ enabled: true,
3792
+ context_on_session_created: true,
3793
+ reflect_on_session_idle: true,
3794
+ context_limit: 5,
3795
+ reflect_days: 7,
3796
+ log: false
3756
3797
  }
3757
3798
  }
3758
3799
  };
@@ -3856,7 +3897,10 @@ function deepMerge(base, override) {
3856
3897
  function loadCliKitConfig(projectDirectory) {
3857
3898
  const safeDir = typeof projectDirectory === "string" && projectDirectory ? projectDirectory : process.cwd();
3858
3899
  const userBaseDir = getOpenCodeConfigDir();
3859
- const projectBaseDir = path4.join(safeDir, ".opencode");
3900
+ const projectBaseDirs = [
3901
+ safeDir,
3902
+ path4.join(safeDir, ".opencode")
3903
+ ];
3860
3904
  const configCandidates = ["clikit.jsonc", "clikit.json", "clikit.config.json"];
3861
3905
  let config = { ...DEFAULT_CONFIG };
3862
3906
  for (const candidate of configCandidates) {
@@ -3867,12 +3911,14 @@ function loadCliKitConfig(projectDirectory) {
3867
3911
  break;
3868
3912
  }
3869
3913
  }
3870
- for (const candidate of configCandidates) {
3871
- const projectConfigPath = path4.join(projectBaseDir, candidate);
3872
- const projectConfig = loadJsonFile(projectConfigPath);
3873
- if (projectConfig) {
3874
- config = deepMerge(config, projectConfig);
3875
- break;
3914
+ for (const baseDir of projectBaseDirs) {
3915
+ for (const candidate of configCandidates) {
3916
+ const projectConfigPath = path4.join(baseDir, candidate);
3917
+ const projectConfig = loadJsonFile(projectConfigPath);
3918
+ if (projectConfig) {
3919
+ config = deepMerge(config, projectConfig);
3920
+ return config;
3921
+ }
3876
3922
  }
3877
3923
  }
3878
3924
  return config;
@@ -3937,7 +3983,7 @@ function filterSkills(skills, config) {
3937
3983
  enabledSet = new Set(skillsConfig.enable);
3938
3984
  }
3939
3985
  if (Array.isArray(skillsConfig.disable) && skillsConfig.disable.length > 0) {
3940
- disabledSet = new Set(skillsConfig.disable);
3986
+ disabledSet = new Set([...disabledSet, ...skillsConfig.disable]);
3941
3987
  }
3942
3988
  const { sources: _sources, enable: _enable, disable: _disable, ...rest } = skillsConfig;
3943
3989
  overrides = rest;
@@ -3955,10 +4001,33 @@ function filterSkills(skills, config) {
3955
4001
  continue;
3956
4002
  }
3957
4003
  if (override && typeof override === "object") {
3958
- filtered[name] = {
4004
+ if (override.disable === true) {
4005
+ continue;
4006
+ }
4007
+ const mergedSkill = {
3959
4008
  ...skill,
3960
- ...override.description ? { description: override.description } : {}
4009
+ ...override.description ? { description: override.description } : {},
4010
+ ...override.template ? { content: override.template } : {}
3961
4011
  };
4012
+ if (override.from !== undefined)
4013
+ mergedSkill.from = override.from;
4014
+ if (override.model !== undefined)
4015
+ mergedSkill.model = override.model;
4016
+ if (override.agent !== undefined)
4017
+ mergedSkill.agent = override.agent;
4018
+ if (override.subtask !== undefined)
4019
+ mergedSkill.subtask = override.subtask;
4020
+ if (override["argument-hint"] !== undefined)
4021
+ mergedSkill["argument-hint"] = override["argument-hint"];
4022
+ if (override.license !== undefined)
4023
+ mergedSkill.license = override.license;
4024
+ if (override.compatibility !== undefined)
4025
+ mergedSkill.compatibility = override.compatibility;
4026
+ if (override.metadata !== undefined)
4027
+ mergedSkill.metadata = override.metadata;
4028
+ if (override["allowed-tools"] !== undefined)
4029
+ mergedSkill["allowed-tools"] = [...override["allowed-tools"]];
4030
+ filtered[name] = mergedSkill;
3962
4031
  continue;
3963
4032
  }
3964
4033
  filtered[name] = skill;
@@ -4119,9 +4188,9 @@ var DANGEROUS_PATTERNS = [
4119
4188
  { pattern: /git\s+push\s+.*-f\b/, reason: "Force push can destroy remote history" },
4120
4189
  { pattern: /git\s+reset\s+--hard/, reason: "Hard reset discards all uncommitted changes" },
4121
4190
  { pattern: /git\s+clean\s+-fd/, reason: "git clean -fd permanently deletes untracked files" },
4122
- { pattern: /rm\s+-rf\s+\//, reason: "rm -rf / is catastrophically dangerous" },
4123
- { pattern: /rm\s+-rf\s+~/, reason: "rm -rf ~ would delete home directory" },
4124
- { pattern: /rm\s+-rf\s+\.\s/, reason: "rm -rf . would delete current directory" },
4191
+ { pattern: /\brm\s+-rf\s+\/(?:\s|$|[;|&])/, reason: "rm -rf / is catastrophically dangerous" },
4192
+ { pattern: /\brm\s+-rf\s+~(?:\s|$|[;|&])/, reason: "rm -rf ~ would delete home directory" },
4193
+ { pattern: /\brm\s+-rf\s+\.\/?(?:\s|$|[;|&])/, reason: "rm -rf . would delete current directory" },
4125
4194
  { pattern: /git\s+branch\s+-D/, reason: "Force-deleting branch may lose unmerged work" }
4126
4195
  ];
4127
4196
  function checkDangerousCommand(command, allowForceWithLease = true) {
@@ -4220,17 +4289,17 @@ function formatSecurityWarning(result) {
4220
4289
  }
4221
4290
  // src/hooks/subagent-question-blocker.ts
4222
4291
  var QUESTION_INDICATORS = [
4223
- "shall I",
4224
- "should I",
4292
+ "shall i",
4293
+ "should i",
4225
4294
  "would you like",
4226
4295
  "do you want",
4227
4296
  "could you clarify",
4228
4297
  "can you confirm",
4229
4298
  "what do you prefer",
4230
4299
  "which approach",
4231
- "before I proceed",
4300
+ "before i proceed",
4232
4301
  "please let me know",
4233
- "I need more information",
4302
+ "i need more information",
4234
4303
  "could you provide"
4235
4304
  ];
4236
4305
  function containsQuestion(text) {
@@ -4455,6 +4524,51 @@ function formatDate(iso) {
4455
4524
  return iso;
4456
4525
  }
4457
4526
  }
4527
+ function writeTopicFile(memoryDir, type, heading, rows) {
4528
+ const topicPath = path6.join(memoryDir, `${type}.md`);
4529
+ const lines = [];
4530
+ lines.push(`# ${heading}`);
4531
+ lines.push("");
4532
+ lines.push(`> Auto-generated from SQLite observations (${rows.length} entries).`);
4533
+ lines.push("");
4534
+ for (const row of rows) {
4535
+ const date = formatDate(row.created_at);
4536
+ const facts = parseJsonArray(row.facts);
4537
+ const concepts = parseJsonArray(row.concepts);
4538
+ const filesModified = parseJsonArray(row.files_modified);
4539
+ lines.push(`## ${date} \u2014 ${row.narrative.split(`
4540
+ `)[0]}`);
4541
+ if (row.confidence < 1) {
4542
+ lines.push(`> Confidence: ${(row.confidence * 100).toFixed(0)}%`);
4543
+ }
4544
+ lines.push("");
4545
+ lines.push(row.narrative);
4546
+ lines.push("");
4547
+ if (facts.length > 0) {
4548
+ lines.push("**Facts:**");
4549
+ for (const fact of facts) {
4550
+ lines.push(`- ${fact}`);
4551
+ }
4552
+ lines.push("");
4553
+ }
4554
+ if (filesModified.length > 0) {
4555
+ lines.push(`**Files:** ${filesModified.map((f) => `\`${f}\``).join(", ")}`);
4556
+ lines.push("");
4557
+ }
4558
+ if (concepts.length > 0) {
4559
+ lines.push(`**Concepts:** ${concepts.join(", ")}`);
4560
+ lines.push("");
4561
+ }
4562
+ if (row.bead_id) {
4563
+ lines.push(`**Bead:** ${row.bead_id}`);
4564
+ lines.push("");
4565
+ }
4566
+ lines.push("---");
4567
+ lines.push("");
4568
+ }
4569
+ fs5.writeFileSync(topicPath, lines.join(`
4570
+ `), "utf-8");
4571
+ }
4458
4572
  function generateMemoryDigest(projectDir, config) {
4459
4573
  const result = { written: false, path: "", counts: {} };
4460
4574
  if (typeof projectDir !== "string" || !projectDir)
@@ -4465,6 +4579,8 @@ function generateMemoryDigest(projectDir, config) {
4465
4579
  return result;
4466
4580
  }
4467
4581
  const maxPerType = config?.max_per_type ?? 10;
4582
+ const indexHighlightsPerType = config?.index_highlights_per_type ?? 2;
4583
+ const writeTopicFiles = config?.write_topic_files !== false;
4468
4584
  const includeTypes = config?.include_types ?? [
4469
4585
  "decision",
4470
4586
  "learning",
@@ -4501,44 +4617,17 @@ function generateMemoryDigest(projectDir, config) {
4501
4617
  totalCount += rows.length;
4502
4618
  const label = typeLabels[type] || { heading: type, emoji: "\uD83D\uDCCC" };
4503
4619
  sections.push(`## ${label.emoji} ${label.heading}`);
4504
- sections.push("");
4505
- for (const row of rows) {
4620
+ sections.push(`- Entries: ${rows.length}`);
4621
+ sections.push(`- Topic file: \`${writeTopicFiles ? `${type}.md` : "(disabled)"}\``);
4622
+ for (const row of rows.slice(0, indexHighlightsPerType)) {
4506
4623
  const date = formatDate(row.created_at);
4507
- const facts = parseJsonArray(row.facts);
4508
- const concepts = parseJsonArray(row.concepts);
4509
- const filesModified = parseJsonArray(row.files_modified);
4510
- sections.push(`### ${date} \u2014 ${row.narrative.split(`
4511
- `)[0]}`);
4512
- if (row.confidence < 1) {
4513
- sections.push(`> Confidence: ${(row.confidence * 100).toFixed(0)}%`);
4514
- }
4515
- sections.push("");
4516
- if (row.narrative.includes(`
4517
- `)) {
4518
- sections.push(row.narrative);
4519
- sections.push("");
4520
- }
4521
- if (facts.length > 0) {
4522
- sections.push("**Facts:**");
4523
- for (const fact of facts) {
4524
- sections.push(`- ${fact}`);
4525
- }
4526
- sections.push("");
4527
- }
4528
- if (filesModified.length > 0) {
4529
- sections.push(`**Files:** ${filesModified.map((f) => `\`${f}\``).join(", ")}`);
4530
- sections.push("");
4531
- }
4532
- if (concepts.length > 0) {
4533
- sections.push(`**Concepts:** ${concepts.join(", ")}`);
4534
- sections.push("");
4535
- }
4536
- if (row.bead_id) {
4537
- sections.push(`**Bead:** ${row.bead_id}`);
4538
- sections.push("");
4539
- }
4540
- sections.push("---");
4541
- sections.push("");
4624
+ const headline = row.narrative.split(`
4625
+ `)[0];
4626
+ sections.push(`- ${date}: ${headline}`);
4627
+ }
4628
+ sections.push("");
4629
+ if (writeTopicFiles) {
4630
+ writeTopicFile(memoryDir, type, label.heading, rows);
4542
4631
  }
4543
4632
  } catch {}
4544
4633
  }
@@ -4679,8 +4768,380 @@ function formatTodoBeadsSyncLog(result) {
4679
4768
  }
4680
4769
  return `[CliKit:todo-beads-sync] session=${result.sessionID} todos=${result.totalTodos} created=${result.created} updated=${result.updated} closed=${result.closed}`;
4681
4770
  }
4682
- // src/index.ts
4771
+ // src/hooks/error-logger.ts
4772
+ var BLOCKED_TOOL_ERROR_PREFIX = "[CliKit] Blocked tool execution:";
4773
+ function getErrorMessage(error) {
4774
+ if (error instanceof Error) {
4775
+ return error.message;
4776
+ }
4777
+ if (typeof error === "string") {
4778
+ return error;
4779
+ }
4780
+ try {
4781
+ return JSON.stringify(error);
4782
+ } catch {
4783
+ return String(error);
4784
+ }
4785
+ }
4786
+ function getErrorStack(error) {
4787
+ if (error instanceof Error && typeof error.stack === "string") {
4788
+ return error.stack;
4789
+ }
4790
+ return;
4791
+ }
4792
+ function isBlockedToolExecutionError(error) {
4793
+ return error instanceof Error && error.message.startsWith(BLOCKED_TOOL_ERROR_PREFIX);
4794
+ }
4795
+ function formatHookErrorLog(hookName, error, context) {
4796
+ const message = getErrorMessage(error);
4797
+ const contextPart = context && Object.keys(context).length > 0 ? ` context=${JSON.stringify(context)}` : "";
4798
+ const stack = getErrorStack(error);
4799
+ return [
4800
+ `[CliKit:${hookName}] Hook error: ${message}${contextPart}`,
4801
+ ...stack ? [stack] : []
4802
+ ].join(`
4803
+ `);
4804
+ }
4805
+ function logHookError(hookName, error, context) {
4806
+ console.error(formatHookErrorLog(hookName, error, context));
4807
+ }
4808
+ // src/tools/cass-memory.ts
4809
+ import { execFile } from "child_process";
4810
+ import { promisify } from "util";
4811
+
4812
+ // src/tools/memory-db.ts
4813
+ import * as fs7 from "fs";
4814
+ import * as path8 from "path";
4815
+ import { Database as Database3 } from "bun:sqlite";
4816
+ function getMemoryPaths(projectDir = process.cwd()) {
4817
+ const memoryDir = path8.join(projectDir, ".opencode", "memory");
4818
+ const memoryDbPath = path8.join(memoryDir, "memory.db");
4819
+ return { memoryDir, memoryDbPath };
4820
+ }
4821
+ function ensureObservationSchema(db) {
4822
+ db.exec(`
4823
+ CREATE TABLE IF NOT EXISTS observations (
4824
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
4825
+ type TEXT NOT NULL,
4826
+ narrative TEXT NOT NULL,
4827
+ facts TEXT DEFAULT '[]',
4828
+ confidence REAL DEFAULT 1.0,
4829
+ files_read TEXT DEFAULT '[]',
4830
+ files_modified TEXT DEFAULT '[]',
4831
+ concepts TEXT DEFAULT '[]',
4832
+ bead_id TEXT,
4833
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
4834
+ expires_at TEXT
4835
+ );
4836
+
4837
+ CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
4838
+ CREATE INDEX IF NOT EXISTS idx_observations_bead_id ON observations(bead_id);
4839
+ CREATE INDEX IF NOT EXISTS idx_observations_created_at ON observations(created_at);
4840
+
4841
+ CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
4842
+ id UNINDEXED,
4843
+ type,
4844
+ narrative,
4845
+ facts,
4846
+ content='observations',
4847
+ content_rowid='id'
4848
+ );
4849
+
4850
+ CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
4851
+ INSERT INTO observations_fts (id, type, narrative, facts)
4852
+ VALUES (new.id, new.type, new.narrative, new.facts);
4853
+ END;
4854
+
4855
+ CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
4856
+ INSERT INTO observations_fts (observations_fts, id, type, narrative, facts)
4857
+ VALUES ('delete', old.id, old.type, old.narrative, old.facts);
4858
+ END;
4859
+
4860
+ CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
4861
+ INSERT INTO observations_fts (observations_fts, id, type, narrative, facts)
4862
+ VALUES ('delete', old.id, old.type, old.narrative, old.facts);
4863
+ INSERT INTO observations_fts (id, type, narrative, facts)
4864
+ VALUES (new.id, new.type, new.narrative, new.facts);
4865
+ END;
4866
+ `);
4867
+ try {
4868
+ db.exec(`ALTER TABLE observations ADD COLUMN concepts TEXT DEFAULT '[]'`);
4869
+ } catch {}
4870
+ try {
4871
+ db.exec(`ALTER TABLE observations ADD COLUMN bead_id TEXT`);
4872
+ } catch {}
4873
+ try {
4874
+ db.exec(`ALTER TABLE observations ADD COLUMN expires_at TEXT`);
4875
+ } catch {}
4876
+ }
4877
+ function openMemoryDb(options2 = {}) {
4878
+ const { projectDir, readonly = false } = options2;
4879
+ const { memoryDir, memoryDbPath } = getMemoryPaths(projectDir);
4880
+ if (!readonly && !fs7.existsSync(memoryDir)) {
4881
+ fs7.mkdirSync(memoryDir, { recursive: true });
4882
+ }
4883
+ const db = new Database3(memoryDbPath, readonly ? { readonly: true } : undefined);
4884
+ if (!readonly) {
4885
+ ensureObservationSchema(db);
4886
+ }
4887
+ return db;
4888
+ }
4889
+
4890
+ // src/tools/memory.ts
4891
+ function normalizeLimit(value, fallback = 10) {
4892
+ if (typeof value !== "number" || !Number.isFinite(value)) {
4893
+ return fallback;
4894
+ }
4895
+ return Math.max(1, Math.floor(value));
4896
+ }
4897
+ function getDb() {
4898
+ return openMemoryDb();
4899
+ }
4900
+ function memorySearch(params) {
4901
+ if (!params || typeof params !== "object") {
4902
+ return [];
4903
+ }
4904
+ const p = params;
4905
+ if (!p.query || typeof p.query !== "string") {
4906
+ return [];
4907
+ }
4908
+ const db = getDb();
4909
+ try {
4910
+ const limit = normalizeLimit(p.limit, 10);
4911
+ let sql;
4912
+ let args;
4913
+ if (p.type) {
4914
+ sql = `
4915
+ SELECT o.id, o.type, o.narrative, o.confidence, o.created_at
4916
+ FROM observations o
4917
+ JOIN observations_fts fts ON o.id = fts.id
4918
+ WHERE observations_fts MATCH ? AND o.type = ?
4919
+ ORDER BY o.confidence DESC, o.created_at DESC
4920
+ LIMIT ?
4921
+ `;
4922
+ args = [p.query, p.type, limit];
4923
+ } else {
4924
+ sql = `
4925
+ SELECT o.id, o.type, o.narrative, o.confidence, o.created_at
4926
+ FROM observations o
4927
+ JOIN observations_fts fts ON o.id = fts.id
4928
+ WHERE observations_fts MATCH ?
4929
+ ORDER BY o.confidence DESC, o.created_at DESC
4930
+ LIMIT ?
4931
+ `;
4932
+ args = [p.query, limit];
4933
+ }
4934
+ const rows = db.prepare(sql).all(...args);
4935
+ return rows;
4936
+ } finally {
4937
+ db.close();
4938
+ }
4939
+ }
4940
+
4941
+ // src/tools/cass-memory.ts
4683
4942
  var execFileAsync = promisify(execFile);
4943
+ var _cmPathCache;
4944
+ async function findCmBinary(hint) {
4945
+ if (hint) {
4946
+ try {
4947
+ const { stdout } = await execFileAsync(hint, ["--version"], { timeout: 5000 });
4948
+ if (stdout.trim())
4949
+ return hint;
4950
+ } catch {}
4951
+ }
4952
+ if (_cmPathCache !== undefined)
4953
+ return _cmPathCache;
4954
+ for (const candidate of ["cm"]) {
4955
+ try {
4956
+ const { stdout } = await execFileAsync(candidate, ["--version"], { timeout: 5000 });
4957
+ if (stdout.trim()) {
4958
+ _cmPathCache = candidate;
4959
+ return candidate;
4960
+ }
4961
+ } catch {}
4962
+ }
4963
+ _cmPathCache = false;
4964
+ return false;
4965
+ }
4966
+ async function runCm(args, opts = {}) {
4967
+ const cmPath = await findCmBinary(opts.cmPath);
4968
+ if (!cmPath) {
4969
+ return { ok: false, command: ["cm", ...args], error: "cm binary not found", source: "cm" };
4970
+ }
4971
+ try {
4972
+ const { stdout, stderr } = await execFileAsync(cmPath, args, {
4973
+ timeout: opts.timeoutMs ?? 30000,
4974
+ cwd: opts.cwd,
4975
+ maxBuffer: 1024 * 1024,
4976
+ env: { ...process.env, NO_COLOR: "1", CASS_MEMORY_NO_EMOJI: "1" }
4977
+ });
4978
+ const trimmed = stdout.trim();
4979
+ if (!trimmed) {
4980
+ return {
4981
+ ok: true,
4982
+ command: ["cm", ...args],
4983
+ raw: stderr.trim() || "",
4984
+ source: "cm"
4985
+ };
4986
+ }
4987
+ try {
4988
+ const parsed = JSON.parse(trimmed);
4989
+ return {
4990
+ ok: parsed.success !== false,
4991
+ command: ["cm", ...args],
4992
+ data: parsed.data ?? parsed,
4993
+ raw: trimmed,
4994
+ source: "cm"
4995
+ };
4996
+ } catch {
4997
+ return {
4998
+ ok: true,
4999
+ command: ["cm", ...args],
5000
+ data: trimmed,
5001
+ raw: trimmed,
5002
+ source: "cm"
5003
+ };
5004
+ }
5005
+ } catch (err) {
5006
+ const message = err instanceof Error ? err.message : String(err);
5007
+ return {
5008
+ ok: false,
5009
+ command: ["cm", ...args],
5010
+ error: `cm execution failed: ${message}`,
5011
+ source: "cm"
5012
+ };
5013
+ }
5014
+ }
5015
+ var ANTI_PATTERN_TYPES = new Set(["cass_feedback_harmful", "cass_anti_pattern"]);
5016
+ function scoreType(type) {
5017
+ switch (type) {
5018
+ case "decision":
5019
+ return 0.16;
5020
+ case "learning":
5021
+ return 0.14;
5022
+ case "cass_feedback_helpful":
5023
+ return 0.1;
5024
+ case "cass_anti_pattern":
5025
+ return 0.12;
5026
+ case "cass_feedback_harmful":
5027
+ return 0.08;
5028
+ case "progress":
5029
+ return 0.04;
5030
+ default:
5031
+ return 0.02;
5032
+ }
5033
+ }
5034
+ function scoreRecency(createdAt) {
5035
+ const time = Date.parse(createdAt);
5036
+ if (Number.isNaN(time)) {
5037
+ return 0.35;
5038
+ }
5039
+ const ageDays = Math.max(0, (Date.now() - time) / 86400000);
5040
+ return Math.exp(-ageDays / 30);
5041
+ }
5042
+ function rankRows(rows) {
5043
+ return rows.map((row) => {
5044
+ const confidence = Math.max(0, Math.min(1, row.confidence));
5045
+ const recency = scoreRecency(row.created_at);
5046
+ const typeWeight = scoreType(row.type);
5047
+ const relevanceScore = confidence * 0.55 + recency * 0.35 + typeWeight;
5048
+ return {
5049
+ ...row,
5050
+ relevanceScore: Number(relevanceScore.toFixed(4))
5051
+ };
5052
+ }).sort((a, b) => b.relevanceScore - a.relevanceScore || b.id - a.id);
5053
+ }
5054
+ function embeddedContext(params) {
5055
+ const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? Math.max(1, Math.floor(params.limit)) : 10;
5056
+ const searchLimit = Math.max(limit * 4, 20);
5057
+ const rows = memorySearch({ query: params.task, limit: searchLimit });
5058
+ const rankedRows = rankRows(rows);
5059
+ const relevantBullets = rankedRows.filter((r) => !ANTI_PATTERN_TYPES.has(r.type)).slice(0, limit).map((r) => ({ ...r, bulletId: `obs-${r.id}` }));
5060
+ const antiPatterns = rankedRows.filter((r) => ANTI_PATTERN_TYPES.has(r.type)).slice(0, limit).map((r) => ({ ...r, bulletId: `obs-${r.id}` }));
5061
+ return {
5062
+ ok: true,
5063
+ command: ["embedded", "context"],
5064
+ source: "embedded",
5065
+ data: {
5066
+ task: params.task,
5067
+ relevantBullets,
5068
+ antiPatterns,
5069
+ historySnippets: rankedRows.slice(0, Math.max(limit * 2, 10)).map(({ relevanceScore: _score, ...row }) => row),
5070
+ degraded: {
5071
+ cass: {
5072
+ available: false,
5073
+ reason: "Running in embedded CliKit mode (no external cm binary found).",
5074
+ suggestedFix: [
5075
+ "npm install -g cass-memory-system",
5076
+ "cm init"
5077
+ ]
5078
+ }
5079
+ }
5080
+ }
5081
+ };
5082
+ }
5083
+ function embeddedReflect(params) {
5084
+ return {
5085
+ ok: true,
5086
+ command: ["embedded", "reflect"],
5087
+ source: "embedded",
5088
+ data: {
5089
+ reflected: true,
5090
+ mode: "embedded",
5091
+ days: params.days ?? 7,
5092
+ maxSessions: params.maxSessions ?? 10,
5093
+ dryRun: !!params.dryRun,
5094
+ note: "Embedded reflection is a stub. Install cm for full playbook-based reflection."
5095
+ }
5096
+ };
5097
+ }
5098
+ async function cassMemoryContext(params) {
5099
+ if (!params || typeof params !== "object") {
5100
+ return { ok: false, command: ["context"], error: "Invalid params" };
5101
+ }
5102
+ const p = params;
5103
+ if (!p.task || typeof p.task !== "string") {
5104
+ return { ok: false, command: ["context"], error: "Missing task" };
5105
+ }
5106
+ const cmPath = await findCmBinary(p.cmPath);
5107
+ if (cmPath) {
5108
+ const args = ["context", p.task, "--json"];
5109
+ if (typeof p.limit === "number")
5110
+ args.push("--limit", String(p.limit));
5111
+ if (typeof p.history === "number")
5112
+ args.push("--history", String(p.history));
5113
+ if (typeof p.days === "number")
5114
+ args.push("--days", String(p.days));
5115
+ if (p.noHistory)
5116
+ args.push("--no-history");
5117
+ const result = await runCm(args, p);
5118
+ if (result.ok)
5119
+ return result;
5120
+ }
5121
+ return embeddedContext(p);
5122
+ }
5123
+ async function cassMemoryReflect(params = {}) {
5124
+ const p = params && typeof params === "object" ? params : {};
5125
+ const cmPath = await findCmBinary(p.cmPath);
5126
+ if (cmPath) {
5127
+ const args = ["reflect", "--json"];
5128
+ if (typeof p.days === "number")
5129
+ args.push("--days", String(p.days));
5130
+ if (typeof p.maxSessions === "number")
5131
+ args.push("--max-sessions", String(p.maxSessions));
5132
+ if (p.dryRun)
5133
+ args.push("--dry-run");
5134
+ if (p.workspace)
5135
+ args.push("--workspace", p.workspace);
5136
+ const result = await runCm(args, p);
5137
+ if (result.ok)
5138
+ return result;
5139
+ }
5140
+ return embeddedReflect(p);
5141
+ }
5142
+
5143
+ // src/index.ts
5144
+ var execFileAsync2 = promisify2(execFile2);
4684
5145
  var CliKitPlugin = async (ctx) => {
4685
5146
  const todosBySession = new Map;
4686
5147
  const defaultMcpEntries = {
@@ -4753,7 +5214,7 @@ var CliKitPlugin = async (ctx) => {
4753
5214
  }
4754
5215
  async function getStagedFiles() {
4755
5216
  try {
4756
- const { stdout } = await execFileAsync("git", ["diff", "--cached", "--name-only"], {
5217
+ const { stdout } = await execFileAsync2("git", ["diff", "--cached", "--name-only"], {
4757
5218
  cwd: ctx.directory,
4758
5219
  encoding: "utf-8"
4759
5220
  });
@@ -4765,7 +5226,18 @@ var CliKitPlugin = async (ctx) => {
4765
5226
  }
4766
5227
  async function getStagedDiff() {
4767
5228
  try {
4768
- const { stdout } = await execFileAsync("git", ["diff", "--cached", "--no-color"], {
5229
+ const { stdout } = await execFileAsync2("git", ["diff", "--cached", "--no-color"], {
5230
+ cwd: ctx.directory,
5231
+ encoding: "utf-8"
5232
+ });
5233
+ return stdout;
5234
+ } catch {
5235
+ return "";
5236
+ }
5237
+ }
5238
+ async function getStagedFileContent(file) {
5239
+ try {
5240
+ const { stdout } = await execFileAsync2("git", ["show", `:${file}`], {
4769
5241
  cwd: ctx.directory,
4770
5242
  encoding: "utf-8"
4771
5243
  });
@@ -4774,12 +5246,24 @@ var CliKitPlugin = async (ctx) => {
4774
5246
  return "";
4775
5247
  }
4776
5248
  }
5249
+ function isToolNamed(name, expected) {
5250
+ return name.toLowerCase() === expected.toLowerCase();
5251
+ }
5252
+ function toSingleLinePreview(text, maxLength = 72) {
5253
+ const normalized = text.replace(/\s+/g, " ").trim();
5254
+ if (normalized.length <= maxLength) {
5255
+ return normalized;
5256
+ }
5257
+ return `${normalized.slice(0, maxLength - 1)}\u2026`;
5258
+ }
4777
5259
  const pluginConfig = loadCliKitConfig(ctx.directory) ?? {};
4778
5260
  const debugLogsEnabled = pluginConfig.hooks?.session_logging === true && process.env.CLIKIT_DEBUG === "1";
4779
5261
  const toolLogsEnabled = pluginConfig.hooks?.tool_logging === true && process.env.CLIKIT_DEBUG === "1";
4780
5262
  const DIGEST_THROTTLE_MS = 60000;
4781
5263
  let lastDigestTime = 0;
4782
5264
  let lastTodoHash = "";
5265
+ let lastCassReflectTime = 0;
5266
+ const CASS_REFLECT_THROTTLE_MS = 5 * 60000;
4783
5267
  const builtinAgents = getBuiltinAgents();
4784
5268
  const builtinCommands = getBuiltinCommands();
4785
5269
  const builtinSkills = getBuiltinSkills();
@@ -4801,19 +5285,35 @@ var CliKitPlugin = async (ctx) => {
4801
5285
  }
4802
5286
  return {
4803
5287
  config: async (config) => {
4804
- config.agent = {
4805
- ...filteredAgents,
4806
- ...config.agent
4807
- };
5288
+ const mergedAgents = { ...filteredAgents };
5289
+ if (config.agent) {
5290
+ for (const [name, existingAgent] of Object.entries(config.agent)) {
5291
+ if (existingAgent && mergedAgents[name]) {
5292
+ mergedAgents[name] = deepMerge(mergedAgents[name], existingAgent);
5293
+ } else if (existingAgent) {
5294
+ mergedAgents[name] = existingAgent;
5295
+ }
5296
+ }
5297
+ }
5298
+ config.agent = mergedAgents;
4808
5299
  config.command = {
4809
5300
  ...filteredCommands,
4810
5301
  ...config.command
4811
5302
  };
5303
+ if (filteredCommands["status-beads"]) {
5304
+ delete config.command.status;
5305
+ }
4812
5306
  const runtimeConfig = config;
4813
5307
  runtimeConfig.skill = {
4814
5308
  ...filteredSkills,
4815
5309
  ...runtimeConfig.skill || {}
4816
5310
  };
5311
+ const existingSkillPaths = runtimeConfig.skills?.paths || [];
5312
+ const resolvedSkillsDir = resolveSkillsDir();
5313
+ runtimeConfig.skills = {
5314
+ ...runtimeConfig.skills || {},
5315
+ paths: existingSkillPaths.includes(resolvedSkillsDir) ? existingSkillPaths : [resolvedSkillsDir, ...existingSkillPaths]
5316
+ };
4817
5317
  runtimeConfig.mcp = {
4818
5318
  ...defaultMcpEntries,
4819
5319
  ...runtimeConfig.mcp || {}
@@ -4845,10 +5345,41 @@ var CliKitPlugin = async (ctx) => {
4845
5345
  console.log(`[CliKit] Session created: ${info?.id || "unknown"}`);
4846
5346
  }
4847
5347
  if (pluginConfig.hooks?.memory_digest?.enabled !== false) {
4848
- const digestResult = generateMemoryDigest(ctx.directory, pluginConfig.hooks?.memory_digest);
4849
- lastDigestTime = Date.now();
4850
- if (pluginConfig.hooks?.memory_digest?.log !== false) {
4851
- console.log(formatDigestLog(digestResult));
5348
+ try {
5349
+ const digestResult = generateMemoryDigest(ctx.directory, pluginConfig.hooks?.memory_digest);
5350
+ lastDigestTime = Date.now();
5351
+ if (pluginConfig.hooks?.memory_digest?.log !== false) {
5352
+ console.log(formatDigestLog(digestResult));
5353
+ }
5354
+ } catch (error) {
5355
+ logHookError("memory-digest", error, { event: event.type, phase: "session.created" });
5356
+ }
5357
+ }
5358
+ const cassHookConfig = pluginConfig.hooks?.cass_memory;
5359
+ if (cassHookConfig?.enabled !== false && cassHookConfig?.context_on_session_created !== false) {
5360
+ try {
5361
+ const sessionTitle = info?.title?.trim() || "session-start";
5362
+ const cassResult = await cassMemoryContext({
5363
+ task: sessionTitle,
5364
+ limit: cassHookConfig?.context_limit,
5365
+ cmPath: cassHookConfig?.cm_path
5366
+ });
5367
+ if (cassHookConfig?.log === true || debugLogsEnabled) {
5368
+ const source = cassResult.source ?? "unknown";
5369
+ const data = cassResult.data;
5370
+ const bullets = data?.relevantBullets ?? [];
5371
+ const bulletCount = bullets.length;
5372
+ console.log(`[CliKit:cass-memory] Context loaded via ${source} (${bulletCount} bullets)`);
5373
+ if (bulletCount > 0) {
5374
+ const topBullets = bullets.slice(0, 3).map((bullet, index) => `${index + 1}. ${toSingleLinePreview(bullet.narrative ?? "")}`);
5375
+ console.log(`[CliKit:cass-memory] Top bullets: ${topBullets.join(" | ")}`);
5376
+ if (cassHookConfig?.log === true) {
5377
+ await showToast(topBullets.join(" \u2022 "), "info", "Cass Memory");
5378
+ }
5379
+ }
5380
+ }
5381
+ } catch (error) {
5382
+ logHookError("cass-memory", error, { event: event.type, phase: "session.created" });
4852
5383
  }
4853
5384
  }
4854
5385
  }
@@ -4863,13 +5394,17 @@ var CliKitPlugin = async (ctx) => {
4863
5394
  if (typeof sessionID === "string") {
4864
5395
  const todos = normalizeTodos(props?.todos);
4865
5396
  todosBySession.set(sessionID, todos);
4866
- const todoHash = JSON.stringify(todos.map((t) => `${t.id}:${t.status}`));
5397
+ const todoHash = JSON.stringify([...todos].sort((a, b) => a.id.localeCompare(b.id)).map((t) => `${t.id}:${t.status}`));
4867
5398
  if (todoHash !== lastTodoHash) {
4868
5399
  lastTodoHash = todoHash;
4869
5400
  if (pluginConfig.hooks?.todo_beads_sync?.enabled !== false) {
4870
- const result = syncTodosToBeads(ctx.directory, sessionID, todos, pluginConfig.hooks?.todo_beads_sync);
4871
- if (pluginConfig.hooks?.todo_beads_sync?.log === true) {
4872
- console.log(formatTodoBeadsSyncLog(result));
5401
+ try {
5402
+ const result = syncTodosToBeads(ctx.directory, sessionID, todos, pluginConfig.hooks?.todo_beads_sync);
5403
+ if (pluginConfig.hooks?.todo_beads_sync?.log === true) {
5404
+ console.log(formatTodoBeadsSyncLog(result));
5405
+ }
5406
+ } catch (error) {
5407
+ logHookError("todo-beads-sync", error, { event: event.type, sessionID });
4873
5408
  }
4874
5409
  }
4875
5410
  }
@@ -4883,20 +5418,47 @@ var CliKitPlugin = async (ctx) => {
4883
5418
  }
4884
5419
  const todoConfig = pluginConfig.hooks?.todo_enforcer;
4885
5420
  if (todoConfig?.enabled !== false) {
4886
- const todos = normalizeTodos(props?.todos);
4887
- const effectiveTodos = todos.length > 0 ? todos : sessionTodos;
4888
- if (effectiveTodos.length > 0) {
4889
- const result = checkTodoCompletion(effectiveTodos);
4890
- if (!result.complete && todoConfig?.warn_on_incomplete !== false) {
4891
- console.warn(formatIncompleteWarning(result, sessionID));
5421
+ try {
5422
+ const todos = normalizeTodos(props?.todos);
5423
+ const effectiveTodos = todos.length > 0 ? todos : sessionTodos;
5424
+ if (effectiveTodos.length > 0) {
5425
+ const result = checkTodoCompletion(effectiveTodos);
5426
+ if (!result.complete && todoConfig?.warn_on_incomplete !== false) {
5427
+ console.warn(formatIncompleteWarning(result, sessionID));
5428
+ }
4892
5429
  }
5430
+ } catch (error) {
5431
+ logHookError("todo-enforcer", error, { event: event.type, sessionID });
4893
5432
  }
4894
5433
  }
4895
5434
  if (pluginConfig.hooks?.memory_digest?.enabled !== false) {
4896
- const now = Date.now();
4897
- if (now - lastDigestTime >= DIGEST_THROTTLE_MS) {
4898
- generateMemoryDigest(ctx.directory, pluginConfig.hooks?.memory_digest);
4899
- lastDigestTime = now;
5435
+ try {
5436
+ const now = Date.now();
5437
+ if (now - lastDigestTime >= DIGEST_THROTTLE_MS) {
5438
+ generateMemoryDigest(ctx.directory, pluginConfig.hooks?.memory_digest);
5439
+ lastDigestTime = now;
5440
+ }
5441
+ } catch (error) {
5442
+ logHookError("memory-digest", error, { event: event.type, phase: "session.idle" });
5443
+ }
5444
+ }
5445
+ const cassHookConfig = pluginConfig.hooks?.cass_memory;
5446
+ if (cassHookConfig?.enabled !== false && cassHookConfig?.reflect_on_session_idle !== false) {
5447
+ try {
5448
+ const now = Date.now();
5449
+ if (now - lastCassReflectTime >= CASS_REFLECT_THROTTLE_MS) {
5450
+ const reflectResult = await cassMemoryReflect({
5451
+ days: cassHookConfig?.reflect_days,
5452
+ cmPath: cassHookConfig?.cm_path
5453
+ });
5454
+ lastCassReflectTime = now;
5455
+ if (cassHookConfig?.log === true || debugLogsEnabled) {
5456
+ const source = reflectResult.source ?? "unknown";
5457
+ console.log(`[CliKit:cass-memory] Reflection completed via ${source} on session idle`);
5458
+ }
5459
+ }
5460
+ } catch (error) {
5461
+ logHookError("cass-memory", error, { event: event.type, phase: "session.idle" });
4900
5462
  }
4901
5463
  }
4902
5464
  }
@@ -4910,80 +5472,126 @@ var CliKitPlugin = async (ctx) => {
4910
5472
  },
4911
5473
  "tool.execute.before": async (input, output) => {
4912
5474
  const toolName = input.tool;
4913
- const toolInput = getToolInput(output.args);
5475
+ const beforeOutput = output;
5476
+ const beforeInput = input;
5477
+ const toolInput = getToolInput(beforeOutput.args ?? beforeInput.args);
4914
5478
  if (toolLogsEnabled) {
4915
5479
  console.log(`[CliKit] Tool executing: ${toolName}`);
4916
5480
  }
4917
5481
  if (pluginConfig.hooks?.git_guard?.enabled !== false) {
4918
- if (toolName === "bash" || toolName === "Bash") {
4919
- const command = toolInput.command;
4920
- if (command) {
4921
- const allowForceWithLease = pluginConfig.hooks?.git_guard?.allow_force_with_lease !== false;
4922
- const result = checkDangerousCommand(command, allowForceWithLease);
4923
- if (result.blocked) {
4924
- console.warn(formatBlockedWarning(result));
4925
- await showToast(result.reason || "Blocked dangerous git command", "warning", "CliKit Guard");
4926
- blockToolExecution(result.reason || "Dangerous git command");
5482
+ if (isToolNamed(toolName, "bash")) {
5483
+ const command = toolInput.command ?? toolInput.cmd;
5484
+ try {
5485
+ if (command) {
5486
+ const allowForceWithLease = pluginConfig.hooks?.git_guard?.allow_force_with_lease !== false;
5487
+ const result = checkDangerousCommand(command, allowForceWithLease);
5488
+ if (result.blocked) {
5489
+ console.warn(formatBlockedWarning(result));
5490
+ await showToast(result.reason || "Blocked dangerous git command", "warning", "CliKit Guard");
5491
+ blockToolExecution(result.reason || "Dangerous git command");
5492
+ }
4927
5493
  }
5494
+ } catch (error) {
5495
+ if (isBlockedToolExecutionError(error)) {
5496
+ throw error;
5497
+ }
5498
+ logHookError("git-guard", error, { tool: toolName, command });
4928
5499
  }
4929
5500
  }
4930
5501
  }
4931
5502
  if (pluginConfig.hooks?.security_check?.enabled !== false) {
4932
- if (toolName === "bash" || toolName === "Bash") {
4933
- const command = toolInput.command;
4934
- if (command && /git\s+(commit|add)/.test(command)) {
4935
- const secConfig = pluginConfig.hooks?.security_check;
4936
- let shouldBlock = false;
4937
- const [stagedFiles, stagedDiff] = await Promise.all([
4938
- getStagedFiles(),
4939
- getStagedDiff()
4940
- ]);
4941
- for (const file of stagedFiles) {
4942
- if (isSensitiveFile(file)) {
4943
- console.warn(`[CliKit:security] Sensitive file staged: ${file}`);
4944
- shouldBlock = true;
5503
+ if (isToolNamed(toolName, "bash")) {
5504
+ const command = toolInput.command ?? toolInput.cmd;
5505
+ try {
5506
+ if (command && /git\s+(commit|add)/.test(command)) {
5507
+ const secConfig = pluginConfig.hooks?.security_check;
5508
+ let shouldBlock = false;
5509
+ const [stagedFiles, stagedDiff] = await Promise.all([
5510
+ getStagedFiles(),
5511
+ getStagedDiff()
5512
+ ]);
5513
+ for (const file of stagedFiles) {
5514
+ if (isSensitiveFile(file)) {
5515
+ console.warn(`[CliKit:security] Sensitive file staged: ${file}`);
5516
+ shouldBlock = true;
5517
+ }
4945
5518
  }
4946
- }
4947
- if (stagedDiff) {
4948
- const scanResult = scanContentForSecrets(stagedDiff);
4949
- if (!scanResult.safe) {
4950
- console.warn(formatSecurityWarning(scanResult));
4951
- shouldBlock = true;
5519
+ if (stagedDiff) {
5520
+ const scanResult = scanContentForSecrets(stagedDiff);
5521
+ if (!scanResult.safe) {
5522
+ console.warn(formatSecurityWarning(scanResult));
5523
+ shouldBlock = true;
5524
+ }
5525
+ }
5526
+ const contentScans = await Promise.all(stagedFiles.map(async (file) => ({
5527
+ file,
5528
+ content: await getStagedFileContent(file)
5529
+ })));
5530
+ for (const { file, content } of contentScans) {
5531
+ if (!content) {
5532
+ continue;
5533
+ }
5534
+ const scanResult = scanContentForSecrets(content, file);
5535
+ if (!scanResult.safe) {
5536
+ console.warn(formatSecurityWarning(scanResult));
5537
+ shouldBlock = true;
5538
+ }
5539
+ }
5540
+ if (shouldBlock && secConfig?.block_commits) {
5541
+ await showToast("Blocked commit due to sensitive data", "error", "CliKit Security");
5542
+ blockToolExecution("Sensitive data detected in commit");
5543
+ } else if (shouldBlock) {
5544
+ await showToast("Potential sensitive data detected in staged changes", "warning", "CliKit Security");
4952
5545
  }
4953
5546
  }
4954
- if (shouldBlock && secConfig?.block_commits) {
4955
- await showToast("Blocked commit due to sensitive data", "error", "CliKit Security");
4956
- blockToolExecution("Sensitive data detected in commit");
5547
+ } catch (error) {
5548
+ if (isBlockedToolExecutionError(error)) {
5549
+ throw error;
4957
5550
  }
5551
+ logHookError("security-check", error, { tool: toolName, command });
4958
5552
  }
4959
5553
  }
4960
5554
  }
4961
5555
  if (pluginConfig.hooks?.swarm_enforcer?.enabled !== false) {
4962
- const editTools = ["edit", "Edit", "write", "Write", "bash", "Bash"];
4963
- if (editTools.includes(toolName)) {
5556
+ const editTools = ["edit", "write", "bash"];
5557
+ if (editTools.some((name) => isToolNamed(toolName, name))) {
4964
5558
  const targetFile = extractFileFromToolInput(toolName, toolInput);
4965
- if (targetFile) {
4966
- const taskScope = toolInput.taskScope || input.__taskScope;
4967
- const enforcement = checkEditPermission(targetFile, taskScope, pluginConfig.hooks?.swarm_enforcer);
4968
- if (!enforcement.allowed) {
4969
- console.warn(formatEnforcementWarning(enforcement));
4970
- if (pluginConfig.hooks?.swarm_enforcer?.block_unreserved_edits) {
4971
- await showToast(enforcement.reason || "Edit blocked outside task scope", "warning", "CliKit Swarm");
4972
- blockToolExecution(enforcement.reason || "Edit outside reserved task scope");
5559
+ try {
5560
+ if (targetFile) {
5561
+ const taskScope = toolInput.taskScope || input.__taskScope;
5562
+ const enforcement = checkEditPermission(targetFile, taskScope, pluginConfig.hooks?.swarm_enforcer);
5563
+ if (!enforcement.allowed) {
5564
+ console.warn(formatEnforcementWarning(enforcement));
5565
+ if (pluginConfig.hooks?.swarm_enforcer?.block_unreserved_edits) {
5566
+ await showToast(enforcement.reason || "Edit blocked outside task scope", "warning", "CliKit Swarm");
5567
+ blockToolExecution(enforcement.reason || "Edit outside reserved task scope");
5568
+ }
5569
+ } else if (pluginConfig.hooks?.swarm_enforcer?.log === true) {
5570
+ console.log(`[CliKit:swarm-enforcer] Allowed edit: ${targetFile}`);
4973
5571
  }
4974
- } else if (pluginConfig.hooks?.swarm_enforcer?.log === true) {
4975
- console.log(`[CliKit:swarm-enforcer] Allowed edit: ${targetFile}`);
4976
5572
  }
5573
+ } catch (error) {
5574
+ if (isBlockedToolExecutionError(error)) {
5575
+ throw error;
5576
+ }
5577
+ logHookError("swarm-enforcer", error, { tool: toolName, targetFile });
4977
5578
  }
4978
5579
  }
4979
5580
  }
4980
5581
  if (pluginConfig.hooks?.subagent_question_blocker?.enabled !== false) {
4981
5582
  if (isSubagentTool(toolName)) {
4982
5583
  const prompt = toolInput.prompt;
4983
- if (prompt && containsQuestion(prompt)) {
4984
- console.warn(formatBlockerWarning());
4985
- await showToast("Subagent prompt blocked: avoid direct questions", "warning", "CliKit Guard");
4986
- blockToolExecution("Subagents should not ask questions");
5584
+ try {
5585
+ if (prompt && containsQuestion(prompt)) {
5586
+ console.warn(formatBlockerWarning());
5587
+ await showToast("Subagent prompt blocked: avoid direct questions", "warning", "CliKit Guard");
5588
+ blockToolExecution("Subagents should not ask questions");
5589
+ }
5590
+ } catch (error) {
5591
+ if (isBlockedToolExecutionError(error)) {
5592
+ throw error;
5593
+ }
5594
+ logHookError("subagent-question-blocker", error, { tool: toolName });
4987
5595
  }
4988
5596
  }
4989
5597
  }
@@ -4997,28 +5605,36 @@ var CliKitPlugin = async (ctx) => {
4997
5605
  }
4998
5606
  const sanitizerConfig = pluginConfig.hooks?.empty_message_sanitizer;
4999
5607
  if (sanitizerConfig?.enabled !== false) {
5000
- if (isEmptyContent(toolOutputContent)) {
5001
- const placeholder = sanitizerConfig?.placeholder || "(No output)";
5002
- if (sanitizerConfig?.log_empty === true) {
5003
- console.log(`[CliKit] Empty output detected for tool: ${toolName}`);
5004
- }
5005
- const sanitized = sanitizeContent(toolOutputContent, placeholder);
5006
- if (typeof sanitized === "string") {
5007
- toolOutputContent = sanitized;
5008
- output.output = sanitized;
5608
+ try {
5609
+ if (isEmptyContent(toolOutputContent)) {
5610
+ const placeholder = sanitizerConfig?.placeholder || "(No output)";
5611
+ if (sanitizerConfig?.log_empty === true) {
5612
+ console.log(`[CliKit] Empty output detected for tool: ${toolName}`);
5613
+ }
5614
+ const sanitized = sanitizeContent(toolOutputContent, placeholder);
5615
+ if (typeof sanitized === "string") {
5616
+ toolOutputContent = sanitized;
5617
+ output.output = sanitized;
5618
+ }
5009
5619
  }
5620
+ } catch (error) {
5621
+ logHookError("empty-message-sanitizer", error, { tool: toolName });
5010
5622
  }
5011
5623
  }
5012
5624
  if (pluginConfig.hooks?.truncator?.enabled !== false) {
5013
- if (shouldTruncate(toolOutputContent, pluginConfig.hooks?.truncator)) {
5014
- const result = truncateOutput(toolOutputContent, pluginConfig.hooks?.truncator);
5015
- if (result.truncated) {
5016
- toolOutputContent = result.content;
5017
- output.output = result.content;
5018
- if (pluginConfig.hooks?.truncator?.log === true) {
5019
- console.log(formatTruncationLog(result));
5625
+ try {
5626
+ if (shouldTruncate(toolOutputContent, pluginConfig.hooks?.truncator)) {
5627
+ const result = truncateOutput(toolOutputContent, pluginConfig.hooks?.truncator);
5628
+ if (result.truncated) {
5629
+ toolOutputContent = result.content;
5630
+ output.output = result.content;
5631
+ if (pluginConfig.hooks?.truncator?.log === true) {
5632
+ console.log(formatTruncationLog(result));
5633
+ }
5020
5634
  }
5021
5635
  }
5636
+ } catch (error) {
5637
+ logHookError("truncator", error, { tool: toolName });
5022
5638
  }
5023
5639
  }
5024
5640
  }