clikit-plugin 0.2.28 → 0.2.30

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 (72) hide show
  1. package/AGENTS.md +30 -32
  2. package/README.md +45 -26
  3. package/command/create.md +37 -122
  4. package/command/handoff.md +45 -69
  5. package/command/init.md +125 -48
  6. package/command/plan.md +101 -159
  7. package/command/research.md +1 -1
  8. package/command/resume.md +34 -55
  9. package/command/vision.md +132 -64
  10. package/dist/.tsbuildinfo +1 -0
  11. package/dist/agents/index.d.ts.map +1 -1
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +95 -11
  14. package/dist/clikit.schema.json +245 -0
  15. package/dist/commands/index.d.ts.map +1 -1
  16. package/dist/config.d.ts +43 -43
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/hooks/git-guard.test.d.ts +2 -0
  19. package/dist/hooks/git-guard.test.d.ts.map +1 -0
  20. package/dist/hooks/index.d.ts +3 -14
  21. package/dist/hooks/index.d.ts.map +1 -1
  22. package/dist/hooks/memory-digest.d.ts +27 -0
  23. package/dist/hooks/memory-digest.d.ts.map +1 -0
  24. package/dist/hooks/security-check.test.d.ts +2 -0
  25. package/dist/hooks/security-check.test.d.ts.map +1 -0
  26. package/dist/hooks/todo-beads-sync.d.ts +23 -0
  27. package/dist/hooks/todo-beads-sync.d.ts.map +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +740 -909
  30. package/dist/skills/index.d.ts.map +1 -1
  31. package/dist/tools/beads-memory-sync.d.ts.map +1 -1
  32. package/dist/tools/context-summary.d.ts.map +1 -1
  33. package/dist/tools/memory-db.d.ts +12 -0
  34. package/dist/tools/memory-db.d.ts.map +1 -0
  35. package/dist/tools/memory.d.ts.map +1 -1
  36. package/dist/tools/observation.d.ts.map +1 -1
  37. package/memory/_templates/plan.md +18 -0
  38. package/package.json +7 -4
  39. package/src/agents/AGENTS.md +11 -39
  40. package/src/agents/build.md +152 -94
  41. package/src/agents/index.ts +31 -5
  42. package/src/agents/looker.md +5 -0
  43. package/src/agents/oracle.md +2 -0
  44. package/src/agents/plan.md +247 -44
  45. package/src/agents/review.md +1 -0
  46. package/src/agents/vision.md +251 -115
  47. package/dist/cli.test.d.ts +0 -2
  48. package/dist/cli.test.d.ts.map +0 -1
  49. package/dist/hooks/auto-format.d.ts +0 -30
  50. package/dist/hooks/auto-format.d.ts.map +0 -1
  51. package/dist/hooks/comment-checker.d.ts +0 -17
  52. package/dist/hooks/comment-checker.d.ts.map +0 -1
  53. package/dist/hooks/compaction.d.ts +0 -60
  54. package/dist/hooks/compaction.d.ts.map +0 -1
  55. package/dist/hooks/env-context.d.ts +0 -43
  56. package/dist/hooks/env-context.d.ts.map +0 -1
  57. package/dist/hooks/ritual-enforcer.d.ts +0 -29
  58. package/dist/hooks/ritual-enforcer.d.ts.map +0 -1
  59. package/dist/hooks/session-notification.d.ts +0 -23
  60. package/dist/hooks/session-notification.d.ts.map +0 -1
  61. package/dist/hooks/session-notification.test.d.ts +0 -2
  62. package/dist/hooks/session-notification.test.d.ts.map +0 -1
  63. package/dist/hooks/typecheck-gate.d.ts +0 -31
  64. package/dist/hooks/typecheck-gate.d.ts.map +0 -1
  65. package/memory/handoffs/2026-02-15-complete-audit.md +0 -136
  66. package/memory/handoffs/2026-02-15-complete-fix.md +0 -140
  67. package/memory/handoffs/2026-02-15-importmeta-fix.md +0 -121
  68. package/memory/handoffs/2026-02-15-installing.md +0 -90
  69. package/memory/handoffs/2026-02-15-plugin-install-fix.md +0 -140
  70. package/memory/handoffs/2026-02-15-runtime-fixes.md +0 -80
  71. package/memory/plans/2026-02-16-plugin-install.md +0 -195
  72. package/memory/research/2026-02-16-opencode-plugin-alignment.md +0 -128
package/dist/index.js CHANGED
@@ -3491,11 +3491,26 @@ var require_gray_matter = __commonJS((exports, module) => {
3491
3491
  module.exports = matter;
3492
3492
  });
3493
3493
 
3494
+ // src/index.ts
3495
+ import { execFile } from "child_process";
3496
+ import { promisify } from "util";
3497
+
3494
3498
  // src/agents/index.ts
3495
3499
  var import_gray_matter = __toESM(require_gray_matter(), 1);
3496
3500
  import * as fs from "fs";
3497
3501
  import * as path from "path";
3498
- var AGENTS_DIR = path.join(import.meta.dir, "../src/agents");
3502
+ var AGENTS_DIR_CANDIDATES = [
3503
+ import.meta.dir,
3504
+ path.join(import.meta.dir, "../../src/agents")
3505
+ ];
3506
+ function resolveAgentsDir() {
3507
+ for (const dir of AGENTS_DIR_CANDIDATES) {
3508
+ if (fs.existsSync(dir)) {
3509
+ return dir;
3510
+ }
3511
+ }
3512
+ return AGENTS_DIR_CANDIDATES[0];
3513
+ }
3499
3514
  function parseAgentMarkdown(filePath) {
3500
3515
  try {
3501
3516
  const content = fs.readFileSync(filePath, "utf-8");
@@ -3526,13 +3541,14 @@ function parseAgentMarkdown(filePath) {
3526
3541
  }
3527
3542
  function loadAgents() {
3528
3543
  const agents = {};
3529
- if (!fs.existsSync(AGENTS_DIR)) {
3544
+ const agentsDir = resolveAgentsDir();
3545
+ if (!fs.existsSync(agentsDir)) {
3530
3546
  return agents;
3531
3547
  }
3532
- const files = fs.readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".md"));
3548
+ const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md") && f !== "AGENTS.md").sort();
3533
3549
  for (const file of files) {
3534
3550
  const agentName = path.basename(file, ".md");
3535
- const agentPath = path.join(AGENTS_DIR, file);
3551
+ const agentPath = path.join(agentsDir, file);
3536
3552
  const agent = parseAgentMarkdown(agentPath);
3537
3553
  if (agent) {
3538
3554
  agents[agentName] = agent;
@@ -3540,15 +3556,37 @@ function loadAgents() {
3540
3556
  }
3541
3557
  return agents;
3542
3558
  }
3559
+ var _cachedAgents = null;
3560
+ var _cachedAgentsMtime = 0;
3543
3561
  function getBuiltinAgents() {
3544
- return loadAgents();
3562
+ try {
3563
+ const mtime = fs.statSync(resolveAgentsDir()).mtimeMs;
3564
+ if (_cachedAgents && _cachedAgentsMtime === mtime)
3565
+ return _cachedAgents;
3566
+ _cachedAgents = loadAgents();
3567
+ _cachedAgentsMtime = mtime;
3568
+ return _cachedAgents;
3569
+ } catch {
3570
+ return _cachedAgents ?? loadAgents();
3571
+ }
3545
3572
  }
3546
3573
 
3547
3574
  // src/commands/index.ts
3548
3575
  var import_gray_matter2 = __toESM(require_gray_matter(), 1);
3549
3576
  import * as fs2 from "fs";
3550
3577
  import * as path2 from "path";
3551
- var COMMANDS_DIR = path2.join(import.meta.dir, "../command");
3578
+ var COMMANDS_DIR_CANDIDATES = [
3579
+ path2.join(import.meta.dir, "../../command"),
3580
+ path2.join(import.meta.dir, "../../../command")
3581
+ ];
3582
+ function resolveCommandsDir() {
3583
+ for (const dir of COMMANDS_DIR_CANDIDATES) {
3584
+ if (fs2.existsSync(dir)) {
3585
+ return dir;
3586
+ }
3587
+ }
3588
+ return COMMANDS_DIR_CANDIDATES[0];
3589
+ }
3552
3590
  function parseCommandMarkdown(filePath) {
3553
3591
  try {
3554
3592
  const content = fs2.readFileSync(filePath, "utf-8");
@@ -3574,13 +3612,14 @@ function parseCommandMarkdown(filePath) {
3574
3612
  }
3575
3613
  function loadCommands() {
3576
3614
  const commands = {};
3577
- if (!fs2.existsSync(COMMANDS_DIR)) {
3615
+ const commandsDir = resolveCommandsDir();
3616
+ if (!fs2.existsSync(commandsDir)) {
3578
3617
  return commands;
3579
3618
  }
3580
- const files = fs2.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith(".md"));
3619
+ const files = fs2.readdirSync(commandsDir).filter((f) => f.endsWith(".md")).sort();
3581
3620
  for (const file of files) {
3582
3621
  const commandName = path2.basename(file, ".md");
3583
- const commandPath = path2.join(COMMANDS_DIR, file);
3622
+ const commandPath = path2.join(commandsDir, file);
3584
3623
  const command = parseCommandMarkdown(commandPath);
3585
3624
  if (command) {
3586
3625
  commands[commandName] = command;
@@ -3588,22 +3627,85 @@ function loadCommands() {
3588
3627
  }
3589
3628
  return commands;
3590
3629
  }
3630
+ var _cachedCommands = null;
3631
+ var _cachedCommandsMtime = 0;
3591
3632
  function getBuiltinCommands() {
3592
- return loadCommands();
3633
+ try {
3634
+ const mtime = fs2.statSync(resolveCommandsDir()).mtimeMs;
3635
+ if (_cachedCommands && _cachedCommandsMtime === mtime)
3636
+ return _cachedCommands;
3637
+ _cachedCommands = loadCommands();
3638
+ _cachedCommandsMtime = mtime;
3639
+ return _cachedCommands;
3640
+ } catch {
3641
+ return _cachedCommands ?? loadCommands();
3642
+ }
3593
3643
  }
3594
3644
 
3595
- // src/config.ts
3645
+ // src/skills/index.ts
3646
+ var import_gray_matter3 = __toESM(require_gray_matter(), 1);
3596
3647
  import * as fs3 from "fs";
3597
3648
  import * as path3 from "path";
3649
+ var SKILLS_DIR_CANDIDATES = [
3650
+ path3.join(import.meta.dir, "../../skill"),
3651
+ path3.join(import.meta.dir, "../../../skill")
3652
+ ];
3653
+ function resolveSkillsDir() {
3654
+ for (const dir of SKILLS_DIR_CANDIDATES) {
3655
+ if (fs3.existsSync(dir)) {
3656
+ return dir;
3657
+ }
3658
+ }
3659
+ return SKILLS_DIR_CANDIDATES[0];
3660
+ }
3661
+ function getBuiltinSkills() {
3662
+ const skills = {};
3663
+ const skillsDir = resolveSkillsDir();
3664
+ if (!fs3.existsSync(skillsDir)) {
3665
+ console.warn("[CliKit] Skills directory not found:", skillsDir);
3666
+ return skills;
3667
+ }
3668
+ const skillDirs = fs3.readdirSync(skillsDir, { withFileTypes: true });
3669
+ for (const dirent of skillDirs) {
3670
+ if (!dirent.isDirectory())
3671
+ continue;
3672
+ const skillName = dirent.name;
3673
+ const skillPath = path3.join(skillsDir, skillName);
3674
+ const skillMdPath = path3.join(skillPath, "SKILL.md");
3675
+ if (!fs3.existsSync(skillMdPath)) {
3676
+ console.warn(`[CliKit] Missing SKILL.md for skill: ${skillName}`);
3677
+ continue;
3678
+ }
3679
+ try {
3680
+ const fileContent = fs3.readFileSync(skillMdPath, "utf-8");
3681
+ const { data, content } = import_gray_matter3.default(fileContent);
3682
+ skills[skillName] = {
3683
+ name: data.name || skillName,
3684
+ description: data.description || "",
3685
+ content: content.trim(),
3686
+ location: skillPath
3687
+ };
3688
+ } catch (err) {
3689
+ console.warn(`[CliKit] Failed to parse skill ${skillName}:`, err);
3690
+ }
3691
+ }
3692
+ return skills;
3693
+ }
3694
+
3695
+ // src/config.ts
3696
+ import * as fs4 from "fs";
3697
+ import * as path4 from "path";
3598
3698
  import * as os from "os";
3599
3699
  var DEFAULT_CONFIG = {
3600
3700
  disabled_agents: [],
3601
3701
  disabled_commands: [],
3702
+ disabled_skills: [],
3602
3703
  agents: {},
3603
3704
  commands: {},
3705
+ skills: {},
3604
3706
  lsp: {},
3605
3707
  hooks: {
3606
- session_logging: true,
3708
+ session_logging: false,
3607
3709
  tool_logging: false,
3608
3710
  todo_enforcer: {
3609
3711
  enabled: true,
@@ -3611,7 +3713,7 @@ var DEFAULT_CONFIG = {
3611
3713
  },
3612
3714
  empty_message_sanitizer: {
3613
3715
  enabled: true,
3614
- log_empty: true,
3716
+ log_empty: false,
3615
3717
  placeholder: "(No output)"
3616
3718
  },
3617
3719
  git_guard: {
@@ -3625,68 +3727,112 @@ var DEFAULT_CONFIG = {
3625
3727
  subagent_question_blocker: {
3626
3728
  enabled: true
3627
3729
  },
3628
- comment_checker: {
3629
- enabled: true,
3630
- threshold: 0.3
3631
- },
3632
- env_context: {
3633
- enabled: true,
3634
- include_git: true,
3635
- include_package: true,
3636
- include_structure: true,
3637
- max_depth: 2
3638
- },
3639
- auto_format: {
3640
- enabled: false,
3641
- log: true
3642
- },
3643
- typecheck_gate: {
3644
- enabled: false,
3645
- log: true,
3646
- block_on_error: false
3647
- },
3648
- session_notification: {
3649
- enabled: true,
3650
- on_idle: true,
3651
- on_error: true,
3652
- title_prefix: "OpenCode"
3653
- },
3654
3730
  truncator: {
3655
3731
  enabled: true,
3656
3732
  max_output_chars: 30000,
3657
3733
  max_output_lines: 500,
3658
3734
  preserve_head_lines: 50,
3659
3735
  preserve_tail_lines: 50,
3660
- log: true
3661
- },
3662
- compaction: {
3663
- enabled: true,
3664
- include_beads_state: true,
3665
- include_memory_refs: true,
3666
- include_todo_state: true,
3667
- max_state_chars: 5000
3736
+ log: false
3668
3737
  },
3669
3738
  swarm_enforcer: {
3670
3739
  enabled: true,
3671
3740
  strict_file_locking: true,
3672
3741
  block_unreserved_edits: false,
3673
- log: true
3742
+ log: false
3743
+ },
3744
+ memory_digest: {
3745
+ enabled: true,
3746
+ max_per_type: 10,
3747
+ include_types: ["decision", "learning", "blocker", "progress", "handoff"],
3748
+ log: false
3749
+ },
3750
+ todo_beads_sync: {
3751
+ enabled: true,
3752
+ close_missing: true,
3753
+ log: false
3674
3754
  }
3675
3755
  }
3676
3756
  };
3677
3757
  function getUserConfigDir() {
3678
3758
  if (process.platform === "win32") {
3679
- return process.env.APPDATA || path3.join(os.homedir(), "AppData", "Roaming");
3759
+ return process.env.APPDATA || path4.join(os.homedir(), "AppData", "Roaming");
3760
+ }
3761
+ return process.env.XDG_CONFIG_HOME || path4.join(os.homedir(), ".config");
3762
+ }
3763
+ function getOpenCodeConfigDir() {
3764
+ if (process.env.OPENCODE_CONFIG_DIR) {
3765
+ return process.env.OPENCODE_CONFIG_DIR;
3680
3766
  }
3681
- return process.env.XDG_CONFIG_HOME || path3.join(os.homedir(), ".config");
3767
+ return path4.join(getUserConfigDir(), "opencode");
3768
+ }
3769
+ function stripJsonComments(content) {
3770
+ let result = "";
3771
+ let inString = false;
3772
+ let inSingleLineComment = false;
3773
+ let inMultiLineComment = false;
3774
+ let escaped = false;
3775
+ for (let i = 0;i < content.length; i += 1) {
3776
+ const char = content[i];
3777
+ const nextChar = content[i + 1];
3778
+ if (inSingleLineComment) {
3779
+ if (char === `
3780
+ `) {
3781
+ inSingleLineComment = false;
3782
+ result += char;
3783
+ }
3784
+ continue;
3785
+ }
3786
+ if (inMultiLineComment) {
3787
+ if (char === "*" && nextChar === "/") {
3788
+ inMultiLineComment = false;
3789
+ i += 1;
3790
+ }
3791
+ continue;
3792
+ }
3793
+ if (inString) {
3794
+ result += char;
3795
+ if (escaped) {
3796
+ escaped = false;
3797
+ } else if (char === "\\") {
3798
+ escaped = true;
3799
+ } else if (char === '"') {
3800
+ inString = false;
3801
+ }
3802
+ continue;
3803
+ }
3804
+ if (char === '"') {
3805
+ inString = true;
3806
+ result += char;
3807
+ continue;
3808
+ }
3809
+ if (char === "/" && nextChar === "/") {
3810
+ inSingleLineComment = true;
3811
+ i += 1;
3812
+ continue;
3813
+ }
3814
+ if (char === "/" && nextChar === "*") {
3815
+ inMultiLineComment = true;
3816
+ i += 1;
3817
+ continue;
3818
+ }
3819
+ result += char;
3820
+ }
3821
+ return result;
3682
3822
  }
3683
3823
  function loadJsonFile(filePath) {
3684
3824
  try {
3685
- if (!fs3.existsSync(filePath)) {
3825
+ if (!fs4.existsSync(filePath)) {
3686
3826
  return null;
3687
3827
  }
3688
- const content = fs3.readFileSync(filePath, "utf-8");
3689
- return JSON.parse(content);
3828
+ const content = fs4.readFileSync(filePath, "utf-8");
3829
+ try {
3830
+ return JSON.parse(content);
3831
+ } catch {
3832
+ const withoutComments = stripJsonComments(content);
3833
+ const withoutTrailingCommas = withoutComments.replace(/,\s*([}\]])/g, "$1");
3834
+ return JSON.parse(withoutTrailingCommas);
3835
+ }
3690
3836
  } catch (error) {
3691
3837
  console.warn(`[CliKit] Failed to load config from ${filePath}:`, error);
3692
3838
  return null;
@@ -3707,18 +3853,25 @@ function deepMerge(base, override) {
3707
3853
  }
3708
3854
  function loadCliKitConfig(projectDirectory) {
3709
3855
  const safeDir = typeof projectDirectory === "string" && projectDirectory ? projectDirectory : process.cwd();
3710
- const userConfigPath = path3.join(getUserConfigDir(), "opencode", "clikit.config.json");
3711
- const projectConfigPath = path3.join(safeDir, ".opencode", "clikit.config.json");
3856
+ const userBaseDir = getOpenCodeConfigDir();
3857
+ const projectBaseDir = path4.join(safeDir, ".opencode");
3858
+ const configCandidates = ["clikit.jsonc", "clikit.json", "clikit.config.json"];
3712
3859
  let config = { ...DEFAULT_CONFIG };
3713
- const userConfig = loadJsonFile(userConfigPath);
3714
- if (userConfig) {
3715
- config = deepMerge(config, userConfig);
3716
- console.log(`[CliKit] Loaded user config from ${userConfigPath}`);
3860
+ for (const candidate of configCandidates) {
3861
+ const userConfigPath = path4.join(userBaseDir, candidate);
3862
+ const userConfig = loadJsonFile(userConfigPath);
3863
+ if (userConfig) {
3864
+ config = deepMerge(config, userConfig);
3865
+ break;
3866
+ }
3717
3867
  }
3718
- const projectConfig = loadJsonFile(projectConfigPath);
3719
- if (projectConfig) {
3720
- config = deepMerge(config, projectConfig);
3721
- console.log(`[CliKit] Loaded project config from ${projectConfigPath}`);
3868
+ for (const candidate of configCandidates) {
3869
+ const projectConfigPath = path4.join(projectBaseDir, candidate);
3870
+ const projectConfig = loadJsonFile(projectConfigPath);
3871
+ if (projectConfig) {
3872
+ config = deepMerge(config, projectConfig);
3873
+ break;
3874
+ }
3722
3875
  }
3723
3876
  return config;
3724
3877
  }
@@ -3763,6 +3916,53 @@ function filterCommands(commands, config) {
3763
3916
  }
3764
3917
  return filtered;
3765
3918
  }
3919
+ function isSkillsConfigObject(value) {
3920
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3921
+ }
3922
+ function filterSkills(skills, config) {
3923
+ if (!config?.skills) {
3924
+ return skills;
3925
+ }
3926
+ const skillsConfig = config.skills;
3927
+ if (Array.isArray(skillsConfig)) {
3928
+ return Object.fromEntries(Object.entries(skills).filter(([name]) => skillsConfig.includes(name)));
3929
+ }
3930
+ let enabledSet = null;
3931
+ let disabledSet = new Set(config.disabled_skills || []);
3932
+ let overrides = {};
3933
+ if (isSkillsConfigObject(skillsConfig)) {
3934
+ if (Array.isArray(skillsConfig.enable) && skillsConfig.enable.length > 0) {
3935
+ enabledSet = new Set(skillsConfig.enable);
3936
+ }
3937
+ if (Array.isArray(skillsConfig.disable) && skillsConfig.disable.length > 0) {
3938
+ disabledSet = new Set(skillsConfig.disable);
3939
+ }
3940
+ const { sources: _sources, enable: _enable, disable: _disable, ...rest } = skillsConfig;
3941
+ overrides = rest;
3942
+ }
3943
+ const filtered = {};
3944
+ for (const [name, skill] of Object.entries(skills)) {
3945
+ if (enabledSet && !enabledSet.has(name)) {
3946
+ continue;
3947
+ }
3948
+ if (disabledSet.has(name)) {
3949
+ continue;
3950
+ }
3951
+ const override = overrides[name];
3952
+ if (override === false) {
3953
+ continue;
3954
+ }
3955
+ if (override && typeof override === "object") {
3956
+ filtered[name] = {
3957
+ ...skill,
3958
+ ...override.description ? { description: override.description } : {}
3959
+ };
3960
+ continue;
3961
+ }
3962
+ filtered[name] = skill;
3963
+ }
3964
+ return filtered;
3965
+ }
3766
3966
 
3767
3967
  // src/hooks/todo-enforcer.ts
3768
3968
  var EMPTY_RESULT = {
@@ -4044,520 +4244,6 @@ function isSubagentTool(toolName) {
4044
4244
  function formatBlockerWarning() {
4045
4245
  return `[CliKit:subagent-blocker] Subagent attempted to ask clarifying questions. Subagents should execute autonomously.`;
4046
4246
  }
4047
- // src/hooks/comment-checker.ts
4048
- var EXCESSIVE_COMMENT_PATTERNS = [
4049
- /\/\/\s*TODO:?\s*$/i,
4050
- /\/\/\s*This (?:function|method|class|variable|constant)/i,
4051
- /\/\/\s*(?:Initialize|Create|Set up|Configure|Define|Declare) the/i,
4052
- /\/\/\s*(?:Import|Export|Return|Handle|Process|Get|Set|Update|Delete|Add|Remove) /i,
4053
- /\/\*\*?\s*\n\s*\*\s*(?:This|The|A|An) (?:function|method|class|component)/i,
4054
- /#\s*(?:This|The|A|An) (?:function|method|class|script)/i
4055
- ];
4056
- function checkCommentDensity(content, threshold = 0.3) {
4057
- if (typeof content !== "string") {
4058
- return { excessive: false, count: 0, totalLines: 0, ratio: 0 };
4059
- }
4060
- const lines = content.split(`
4061
- `);
4062
- const totalLines = lines.filter((l) => l.trim().length > 0).length;
4063
- if (totalLines === 0) {
4064
- return { excessive: false, count: 0, totalLines: 0, ratio: 0 };
4065
- }
4066
- let commentLines = 0;
4067
- let inBlockComment = false;
4068
- for (const line of lines) {
4069
- const trimmed = line.trim();
4070
- if (inBlockComment) {
4071
- commentLines++;
4072
- if (trimmed.includes("*/")) {
4073
- inBlockComment = false;
4074
- }
4075
- continue;
4076
- }
4077
- if (trimmed.startsWith("/*")) {
4078
- commentLines++;
4079
- if (!trimmed.includes("*/")) {
4080
- inBlockComment = true;
4081
- }
4082
- continue;
4083
- }
4084
- if (trimmed.startsWith("//")) {
4085
- commentLines++;
4086
- } else if (trimmed.startsWith("#") && !trimmed.startsWith("#!") && !trimmed.startsWith("# ")) {
4087
- const rest = trimmed.slice(1).trim();
4088
- if (rest.length > 0 && !rest.includes(":") && !rest.startsWith(" ") === false) {
4089
- commentLines++;
4090
- }
4091
- }
4092
- }
4093
- const ratio = commentLines / totalLines;
4094
- return {
4095
- excessive: ratio > threshold,
4096
- count: commentLines,
4097
- totalLines,
4098
- ratio
4099
- };
4100
- }
4101
- function hasExcessiveAIComments(content) {
4102
- if (typeof content !== "string") {
4103
- return false;
4104
- }
4105
- let matches = 0;
4106
- for (const pattern of EXCESSIVE_COMMENT_PATTERNS) {
4107
- const found = content.match(new RegExp(pattern, "gm"));
4108
- if (found) {
4109
- matches += found.length;
4110
- }
4111
- }
4112
- return matches >= 5;
4113
- }
4114
- function formatCommentWarning(result) {
4115
- const pct = (result.ratio * 100).toFixed(0);
4116
- return `[CliKit:comment-checker] ${result.count}/${result.totalLines} lines are comments (${pct}%). Reduce unnecessary comments \u2014 code should be self-documenting.`;
4117
- }
4118
- // src/hooks/env-context.ts
4119
- import * as fs4 from "fs";
4120
- import * as path4 from "path";
4121
- import { execSync } from "child_process";
4122
- function isRecord(value) {
4123
- return !!value && typeof value === "object" && !Array.isArray(value);
4124
- }
4125
- function runSilent(cmd, cwd) {
4126
- try {
4127
- return execSync(cmd, { cwd, encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
4128
- } catch {
4129
- return null;
4130
- }
4131
- }
4132
- function getGitInfo(cwd) {
4133
- const branch = runSilent("git rev-parse --abbrev-ref HEAD", cwd);
4134
- if (!branch)
4135
- return;
4136
- const status = runSilent("git status --porcelain", cwd);
4137
- const remoteUrl = runSilent("git remote get-url origin", cwd);
4138
- const lastCommit = runSilent("git log -1 --format=%s", cwd);
4139
- return {
4140
- branch,
4141
- hasChanges: !!status && status.length > 0,
4142
- remoteUrl: remoteUrl || undefined,
4143
- lastCommit: lastCommit || undefined
4144
- };
4145
- }
4146
- function getPackageInfo(cwd) {
4147
- const pkgPath = path4.join(cwd, "package.json");
4148
- if (!fs4.existsSync(pkgPath))
4149
- return;
4150
- try {
4151
- const parsed = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
4152
- const pkg = isRecord(parsed) ? parsed : {};
4153
- const scriptsObj = isRecord(pkg.scripts) ? pkg.scripts : {};
4154
- const scripts = Object.keys(scriptsObj);
4155
- let packageManager;
4156
- if (fs4.existsSync(path4.join(cwd, "bun.lockb")) || fs4.existsSync(path4.join(cwd, "bun.lock"))) {
4157
- packageManager = "bun";
4158
- } else if (fs4.existsSync(path4.join(cwd, "pnpm-lock.yaml"))) {
4159
- packageManager = "pnpm";
4160
- } else if (fs4.existsSync(path4.join(cwd, "yarn.lock"))) {
4161
- packageManager = "yarn";
4162
- } else if (fs4.existsSync(path4.join(cwd, "package-lock.json"))) {
4163
- packageManager = "npm";
4164
- }
4165
- let framework;
4166
- const deps = isRecord(pkg.dependencies) ? pkg.dependencies : {};
4167
- const devDeps = isRecord(pkg.devDependencies) ? pkg.devDependencies : {};
4168
- const allDeps = { ...deps, ...devDeps };
4169
- if (allDeps["next"])
4170
- framework = "Next.js";
4171
- else if (allDeps["nuxt"])
4172
- framework = "Nuxt";
4173
- else if (allDeps["@angular/core"])
4174
- framework = "Angular";
4175
- else if (allDeps["svelte"])
4176
- framework = "Svelte";
4177
- else if (allDeps["vue"])
4178
- framework = "Vue";
4179
- else if (allDeps["react"])
4180
- framework = "React";
4181
- else if (allDeps["express"])
4182
- framework = "Express";
4183
- else if (allDeps["fastify"])
4184
- framework = "Fastify";
4185
- else if (allDeps["hono"])
4186
- framework = "Hono";
4187
- return {
4188
- name: typeof pkg.name === "string" ? pkg.name : undefined,
4189
- version: typeof pkg.version === "string" ? pkg.version : undefined,
4190
- packageManager,
4191
- scripts,
4192
- framework
4193
- };
4194
- } catch {
4195
- return;
4196
- }
4197
- }
4198
- function getTopLevelStructure(cwd, maxDepth = 2) {
4199
- const entries = [];
4200
- function walk(dir, depth, prefix) {
4201
- if (depth > maxDepth)
4202
- return;
4203
- try {
4204
- const items = fs4.readdirSync(dir, { withFileTypes: true });
4205
- const filtered = items.filter((i) => !i.name.startsWith(".") && i.name !== "node_modules" && i.name !== "dist" && i.name !== "__pycache__").sort((a, b) => {
4206
- if (a.isDirectory() && !b.isDirectory())
4207
- return -1;
4208
- if (!a.isDirectory() && b.isDirectory())
4209
- return 1;
4210
- return a.name.localeCompare(b.name);
4211
- });
4212
- for (const item of filtered.slice(0, 20)) {
4213
- const suffix = item.isDirectory() ? "/" : "";
4214
- entries.push(`${prefix}${item.name}${suffix}`);
4215
- if (item.isDirectory() && depth < maxDepth) {
4216
- walk(path4.join(dir, item.name), depth + 1, prefix + " ");
4217
- }
4218
- }
4219
- } catch {}
4220
- }
4221
- walk(cwd, 1, "");
4222
- return entries;
4223
- }
4224
- function collectEnvInfo(cwd, config) {
4225
- const safeCwd = typeof cwd === "string" && cwd ? cwd : process.cwd();
4226
- const info = {
4227
- platform: process.platform,
4228
- nodeVersion: process.version,
4229
- cwd: safeCwd
4230
- };
4231
- if (config?.include_git !== false) {
4232
- info.git = getGitInfo(safeCwd);
4233
- }
4234
- if (config?.include_package !== false) {
4235
- info.package = getPackageInfo(safeCwd);
4236
- }
4237
- if (config?.include_structure !== false) {
4238
- info.structure = getTopLevelStructure(safeCwd, config?.max_depth ?? 2);
4239
- }
4240
- return info;
4241
- }
4242
- function buildEnvBlock(info) {
4243
- const lines = ["<env-context>"];
4244
- lines.push(`Platform: ${info.platform}`);
4245
- lines.push(`Node: ${info.nodeVersion}`);
4246
- lines.push(`CWD: ${info.cwd}`);
4247
- if (info.git) {
4248
- lines.push(`
4249
- Git:`);
4250
- lines.push(` Branch: ${info.git.branch}`);
4251
- lines.push(` Dirty: ${info.git.hasChanges}`);
4252
- if (info.git.remoteUrl)
4253
- lines.push(` Remote: ${info.git.remoteUrl}`);
4254
- if (info.git.lastCommit)
4255
- lines.push(` Last commit: ${info.git.lastCommit}`);
4256
- }
4257
- if (info.package) {
4258
- lines.push(`
4259
- Package:`);
4260
- if (info.package.name)
4261
- lines.push(` Name: ${info.package.name}`);
4262
- if (info.package.version)
4263
- lines.push(` Version: ${info.package.version}`);
4264
- if (info.package.packageManager)
4265
- lines.push(` Package manager: ${info.package.packageManager}`);
4266
- if (info.package.framework)
4267
- lines.push(` Framework: ${info.package.framework}`);
4268
- if (info.package.scripts?.length) {
4269
- lines.push(` Scripts: ${info.package.scripts.join(", ")}`);
4270
- }
4271
- }
4272
- if (info.structure?.length) {
4273
- lines.push(`
4274
- Project structure:`);
4275
- for (const entry of info.structure) {
4276
- lines.push(` ${entry}`);
4277
- }
4278
- }
4279
- lines.push("</env-context>");
4280
- return lines.join(`
4281
- `);
4282
- }
4283
- function formatEnvSummary(info) {
4284
- const parts = [`${info.platform}/${info.nodeVersion}`];
4285
- if (info.git)
4286
- parts.push(`branch:${info.git.branch}`);
4287
- if (info.package?.framework)
4288
- parts.push(info.package.framework);
4289
- if (info.package?.packageManager)
4290
- parts.push(info.package.packageManager);
4291
- return `[CliKit:env-context] ${parts.join(", ")}`;
4292
- }
4293
- // src/hooks/auto-format.ts
4294
- import * as fs5 from "fs";
4295
- import * as path5 from "path";
4296
- import { execSync as execSync2 } from "child_process";
4297
- var FORMATTERS = [
4298
- {
4299
- name: "prettier",
4300
- configFiles: [".prettierrc", ".prettierrc.json", ".prettierrc.js", ".prettierrc.cjs", ".prettierrc.yaml", ".prettierrc.yml", "prettier.config.js", "prettier.config.cjs"],
4301
- command: (file) => `npx prettier --write "${file}"`
4302
- },
4303
- {
4304
- name: "biome",
4305
- configFiles: ["biome.json", "biome.jsonc"],
4306
- command: (file) => `npx @biomejs/biome format --write "${file}"`
4307
- },
4308
- {
4309
- name: "dprint",
4310
- configFiles: ["dprint.json", ".dprint.json"],
4311
- command: (file) => `dprint fmt "${file}"`
4312
- }
4313
- ];
4314
- var DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".scss", ".html", ".md", ".yaml", ".yml"];
4315
- function detectFormatter(projectDir) {
4316
- const pkgPath = path5.join(projectDir, "package.json");
4317
- let pkgDeps = {};
4318
- try {
4319
- if (fs5.existsSync(pkgPath)) {
4320
- const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
4321
- pkgDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4322
- }
4323
- } catch {}
4324
- for (const formatter of FORMATTERS) {
4325
- for (const configFile of formatter.configFiles) {
4326
- if (fs5.existsSync(path5.join(projectDir, configFile))) {
4327
- return formatter;
4328
- }
4329
- }
4330
- if (pkgDeps[formatter.name] || pkgDeps[`@biomejs/${formatter.name}`]) {
4331
- return formatter;
4332
- }
4333
- }
4334
- return;
4335
- }
4336
- function shouldFormat(filePath, extensions) {
4337
- if (typeof filePath !== "string")
4338
- return false;
4339
- const ext = path5.extname(filePath).toLowerCase();
4340
- const allowedExts = extensions || DEFAULT_EXTENSIONS;
4341
- return allowedExts.includes(ext);
4342
- }
4343
- function runFormatter(filePath, projectDir, formatterOverride) {
4344
- const safePath = typeof filePath === "string" && filePath ? filePath : "";
4345
- const safeDir = typeof projectDir === "string" && projectDir ? projectDir : process.cwd();
4346
- const formatter = formatterOverride ? FORMATTERS.find((f) => f.name === formatterOverride) : detectFormatter(safeDir);
4347
- if (!formatter || !safePath) {
4348
- return {
4349
- formatted: false,
4350
- file: safePath,
4351
- formatter: "none",
4352
- error: formatter ? "No file path provided" : "No formatter detected"
4353
- };
4354
- }
4355
- try {
4356
- const cmd = formatter.command(safePath);
4357
- execSync2(cmd, {
4358
- cwd: safeDir,
4359
- timeout: 1e4,
4360
- stdio: ["pipe", "pipe", "pipe"]
4361
- });
4362
- return {
4363
- formatted: true,
4364
- file: safePath,
4365
- formatter: formatter.name
4366
- };
4367
- } catch (err) {
4368
- return {
4369
- formatted: false,
4370
- file: safePath,
4371
- formatter: formatter.name,
4372
- error: err instanceof Error ? err.message : String(err)
4373
- };
4374
- }
4375
- }
4376
- function formatAutoFormatLog(result) {
4377
- if (result.formatted) {
4378
- return `[CliKit:auto-format] Formatted ${result.file} with ${result.formatter}`;
4379
- }
4380
- return `[CliKit:auto-format] Failed to format ${result.file}: ${result.error}`;
4381
- }
4382
- // src/hooks/typecheck-gate.ts
4383
- import * as fs6 from "fs";
4384
- import * as path6 from "path";
4385
- import { execSync as execSync3 } from "child_process";
4386
- function normalizeTypeCheckResult(result) {
4387
- if (!result || typeof result !== "object") {
4388
- return { clean: true, errors: [], checkedFile: "" };
4389
- }
4390
- const raw = result;
4391
- const errors = Array.isArray(raw.errors) ? raw.errors.filter((item) => !!item && typeof item === "object").map((item) => ({
4392
- file: typeof item.file === "string" ? item.file : "",
4393
- line: typeof item.line === "number" ? item.line : 0,
4394
- column: typeof item.column === "number" ? item.column : 0,
4395
- code: typeof item.code === "string" ? item.code : "TS0000",
4396
- message: typeof item.message === "string" ? item.message : "Unknown typecheck error"
4397
- })) : [];
4398
- const checkedFile = typeof raw.checkedFile === "string" ? raw.checkedFile : "";
4399
- const clean = typeof raw.clean === "boolean" ? raw.clean : errors.length === 0;
4400
- return { clean, errors, checkedFile };
4401
- }
4402
- var TS_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
4403
- function isTypeScriptFile(filePath) {
4404
- if (typeof filePath !== "string")
4405
- return false;
4406
- return TS_EXTENSIONS.includes(path6.extname(filePath).toLowerCase());
4407
- }
4408
- function findTsConfig(projectDir, override) {
4409
- const safeDir = typeof projectDir === "string" && projectDir ? projectDir : process.cwd();
4410
- if (override) {
4411
- const overridePath = path6.resolve(safeDir, override);
4412
- return fs6.existsSync(overridePath) ? overridePath : undefined;
4413
- }
4414
- const candidates = ["tsconfig.json", "tsconfig.build.json"];
4415
- for (const candidate of candidates) {
4416
- const fullPath = path6.join(safeDir, candidate);
4417
- if (fs6.existsSync(fullPath)) {
4418
- return fullPath;
4419
- }
4420
- }
4421
- return;
4422
- }
4423
- function runTypeCheck(filePath, projectDir, config) {
4424
- const safePath = typeof filePath === "string" && filePath ? filePath : "";
4425
- const safeDir = typeof projectDir === "string" && projectDir ? projectDir : process.cwd();
4426
- const tsConfig = findTsConfig(safeDir, config?.tsconfig);
4427
- if (!tsConfig) {
4428
- return { clean: true, errors: [], checkedFile: safePath };
4429
- }
4430
- try {
4431
- const tscCmd = `npx tsc --noEmit --pretty false -p "${tsConfig}"`;
4432
- execSync3(tscCmd, {
4433
- cwd: safeDir,
4434
- timeout: 30000,
4435
- stdio: ["pipe", "pipe", "pipe"],
4436
- encoding: "utf-8"
4437
- });
4438
- return { clean: true, errors: [], checkedFile: safePath };
4439
- } catch (err) {
4440
- const output = err instanceof Error && "stdout" in err ? String(err.stdout) : "";
4441
- const errors = parseTscOutput(output, safePath);
4442
- return {
4443
- clean: errors.length === 0,
4444
- errors,
4445
- checkedFile: safePath
4446
- };
4447
- }
4448
- }
4449
- function parseTscOutput(output, filterFile) {
4450
- if (typeof output !== "string")
4451
- return [];
4452
- const diagnostics = [];
4453
- const lines = output.split(`
4454
- `);
4455
- const pattern = /^(.+?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/;
4456
- for (const line of lines) {
4457
- const match = line.match(pattern);
4458
- if (match) {
4459
- const [, file, lineNum, col, code, message] = match;
4460
- const diagnostic = {
4461
- file: file.trim(),
4462
- line: parseInt(lineNum, 10),
4463
- column: parseInt(col, 10),
4464
- code,
4465
- message: message.trim()
4466
- };
4467
- if (filterFile) {
4468
- const normalizedFilter = path6.resolve(filterFile);
4469
- const normalizedDiag = path6.resolve(diagnostic.file);
4470
- if (normalizedDiag === normalizedFilter) {
4471
- diagnostics.push(diagnostic);
4472
- }
4473
- } else {
4474
- diagnostics.push(diagnostic);
4475
- }
4476
- }
4477
- }
4478
- return diagnostics;
4479
- }
4480
- function formatTypeCheckWarning(result) {
4481
- const safeResult = normalizeTypeCheckResult(result);
4482
- if (safeResult.clean) {
4483
- return `[CliKit:typecheck] ${safeResult.checkedFile} \u2014 no type errors`;
4484
- }
4485
- const lines = [`[CliKit:typecheck] ${safeResult.errors.length} type error(s) in ${safeResult.checkedFile}:`];
4486
- for (const err of safeResult.errors.slice(0, 10)) {
4487
- lines.push(` ${err.file}:${err.line}:${err.column} ${err.code}: ${err.message}`);
4488
- }
4489
- if (safeResult.errors.length > 10) {
4490
- lines.push(` ... and ${safeResult.errors.length - 10} more`);
4491
- }
4492
- return lines.join(`
4493
- `);
4494
- }
4495
- // src/hooks/session-notification.ts
4496
- import { execSync as execSync4 } from "child_process";
4497
- function escapeSingleQuotes(str2) {
4498
- return str2.replace(/'/g, "'\\''");
4499
- }
4500
- function getNotifyCommand(payload) {
4501
- const { title, body, urgency } = payload;
4502
- const safeTitle = typeof title === "string" ? title : "Notification";
4503
- const safeBody = typeof body === "string" ? body : "";
4504
- const escapedTitle = safeTitle.replace(/"/g, "\\\"");
4505
- const escapedBody = safeBody.replace(/"/g, "\\\"");
4506
- switch (process.platform) {
4507
- case "linux":
4508
- return `notify-send "${escapedTitle}" "${escapedBody}" --urgency=${urgency || "normal"}`;
4509
- case "darwin":
4510
- return `osascript -e 'display notification "${escapedBody}" with title "${escapedTitle}"'`;
4511
- case "win32":
4512
- return `powershell -command "[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; $n.ShowBalloonTip(5000, '${escapeSingleQuotes(safeTitle)}', '${escapeSingleQuotes(safeBody)}', [System.Windows.Forms.ToolTipIcon]::Info)"`;
4513
- default:
4514
- return null;
4515
- }
4516
- }
4517
- function sendNotification(payload) {
4518
- const cmd = getNotifyCommand(payload);
4519
- if (!cmd)
4520
- return false;
4521
- try {
4522
- execSync4(cmd, { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
4523
- return true;
4524
- } catch {
4525
- if (process.platform === "linux") {
4526
- try {
4527
- const wslTitle = escapeSingleQuotes(typeof payload.title === "string" ? payload.title : "Notification");
4528
- const wslBody = escapeSingleQuotes(typeof payload.body === "string" ? payload.body : "");
4529
- const wslCmd = `powershell.exe -command "[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; $n.ShowBalloonTip(5000, '${wslTitle}', '${wslBody}', [System.Windows.Forms.ToolTipIcon]::Info)"`;
4530
- execSync4(wslCmd, { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
4531
- return true;
4532
- } catch {
4533
- return false;
4534
- }
4535
- }
4536
- return false;
4537
- }
4538
- }
4539
- function buildIdleNotification(sessionId, prefix) {
4540
- const titlePrefix = prefix || "OpenCode";
4541
- const sid = typeof sessionId === "string" ? sessionId : undefined;
4542
- return {
4543
- title: `${titlePrefix} \u2014 Task Complete`,
4544
- body: sid ? `Session ${sid.substring(0, 8)} is idle and waiting for input.` : "Session is idle and waiting for input.",
4545
- urgency: "normal"
4546
- };
4547
- }
4548
- function buildErrorNotification(error, sessionId, prefix) {
4549
- const titlePrefix = prefix || "OpenCode";
4550
- const errorStr = typeof error === "string" ? error : error instanceof Error ? error.message : String(error);
4551
- const sid = typeof sessionId === "string" ? sessionId : undefined;
4552
- return {
4553
- title: `${titlePrefix} \u2014 Error`,
4554
- body: sid ? `Session ${sid.substring(0, 8)}: ${errorStr.substring(0, 100)}` : errorStr.substring(0, 120),
4555
- urgency: "critical"
4556
- };
4557
- }
4558
- function formatNotificationLog(payload, sent) {
4559
- return sent ? `[CliKit:notification] Sent: "${payload.title}"` : `[CliKit:notification] Failed to send notification (platform: ${process.platform})`;
4560
- }
4561
4247
  // src/hooks/truncator.ts
4562
4248
  var DEFAULT_MAX_CHARS = 30000;
4563
4249
  var DEFAULT_MAX_LINES = 500;
@@ -4665,266 +4351,422 @@ function formatTruncationLog(result) {
4665
4351
  const saved = result.originalLength - result.truncatedLength;
4666
4352
  return `[CliKit:truncator] Truncated output: ${result.originalLines} \u2192 ${result.truncatedLines} lines, saved ${(saved / 1024).toFixed(1)}KB`;
4667
4353
  }
4668
- // src/hooks/compaction.ts
4669
- import * as fs7 from "fs";
4670
- import * as path7 from "path";
4671
- import { execSync as execSync5 } from "child_process";
4672
- function isRecord2(value) {
4673
- return !!value && typeof value === "object" && !Array.isArray(value);
4674
- }
4675
- function isTodoStatus(value) {
4676
- return value === "todo" || value === "in-progress" || value === "in_progress" || value === "completed";
4677
- }
4678
- function normalizeTodoEntries(todos) {
4679
- if (!Array.isArray(todos)) {
4680
- return [];
4354
+ // src/hooks/swarm-enforcer.ts
4355
+ import * as path5 from "path";
4356
+ function isFileInScope(filePath, scope) {
4357
+ if (typeof filePath !== "string")
4358
+ return false;
4359
+ const normalizedPath = path5.resolve(filePath);
4360
+ for (const reserved of scope.reservedFiles) {
4361
+ const normalizedReserved = path5.resolve(reserved);
4362
+ if (normalizedPath === normalizedReserved) {
4363
+ return true;
4364
+ }
4681
4365
  }
4682
- const normalized = [];
4683
- for (const entry of todos) {
4684
- if (!isRecord2(entry) || !isTodoStatus(entry.status)) {
4685
- continue;
4366
+ if (scope.allowedPatterns) {
4367
+ for (const pattern of scope.allowedPatterns) {
4368
+ if (normalizedPath.includes(pattern) || normalizedPath.startsWith(path5.resolve(pattern))) {
4369
+ return true;
4370
+ }
4686
4371
  }
4687
- normalized.push({
4688
- id: typeof entry.id === "string" ? entry.id : "unknown",
4689
- content: typeof entry.content === "string" ? entry.content : "(no content)",
4690
- status: entry.status === "in_progress" ? "in-progress" : entry.status
4691
- });
4692
4372
  }
4693
- return normalized;
4373
+ return false;
4694
4374
  }
4695
- function readBeadsState(projectDir) {
4696
- if (typeof projectDir !== "string" || !projectDir) {
4697
- return;
4375
+ function checkEditPermission(filePath, scope, config) {
4376
+ if (!scope) {
4377
+ return { allowed: true };
4698
4378
  }
4699
- const beadsDir = path7.join(projectDir, ".beads");
4700
- if (!fs7.existsSync(beadsDir))
4379
+ if (config?.strict_file_locking === false) {
4380
+ return { allowed: true };
4381
+ }
4382
+ if (typeof filePath !== "string") {
4383
+ return { allowed: false, reason: "Invalid file path" };
4384
+ }
4385
+ if (isFileInScope(filePath, scope)) {
4386
+ return { allowed: true, file: filePath };
4387
+ }
4388
+ return {
4389
+ allowed: false,
4390
+ file: filePath,
4391
+ reason: `File is not in task scope for task ${scope.taskId}`,
4392
+ suggestion: `Reserve the file first using beads-village reserve, or ask the lead agent to reassign.`
4393
+ };
4394
+ }
4395
+ function extractFileFromToolInput(toolName, input) {
4396
+ if (typeof toolName !== "string")
4701
4397
  return;
4702
- const state = {};
4703
- try {
4704
- const metaPath = path7.join(beadsDir, "metadata.json");
4705
- if (!fs7.existsSync(metaPath))
4398
+ switch (toolName.toLowerCase()) {
4399
+ case "edit":
4400
+ case "write":
4401
+ case "read":
4402
+ return input.filePath;
4403
+ case "bash": {
4404
+ const cmd = input.command;
4405
+ if (!cmd)
4406
+ return;
4407
+ const writePatterns = [
4408
+ />\s*["']?([^\s"'|&;]+)/,
4409
+ /tee\s+["']?([^\s"'|&;]+)/,
4410
+ /mv\s+\S+\s+["']?([^\s"'|&;]+)/,
4411
+ /cp\s+\S+\s+["']?([^\s"'|&;]+)/
4412
+ ];
4413
+ for (const pattern of writePatterns) {
4414
+ const match = cmd.match(pattern);
4415
+ if (match)
4416
+ return match[1];
4417
+ }
4706
4418
  return;
4707
- const meta = JSON.parse(fs7.readFileSync(metaPath, "utf-8"));
4708
- if (!meta.database)
4419
+ }
4420
+ default:
4709
4421
  return;
4422
+ }
4423
+ }
4424
+ function formatEnforcementWarning(result) {
4425
+ if (result.allowed)
4426
+ return "";
4427
+ const lines = [`[CliKit:swarm-enforcer] BLOCKED edit to ${result.file}`];
4428
+ if (result.reason)
4429
+ lines.push(` Reason: ${result.reason}`);
4430
+ if (result.suggestion)
4431
+ lines.push(` Suggestion: ${result.suggestion}`);
4432
+ return lines.join(`
4433
+ `);
4434
+ }
4435
+ // src/hooks/memory-digest.ts
4436
+ import * as fs5 from "fs";
4437
+ import * as path6 from "path";
4438
+ import { Database } from "bun:sqlite";
4439
+ function parseJsonArray(value) {
4440
+ if (typeof value !== "string" || !value.trim())
4441
+ return [];
4442
+ try {
4443
+ const parsed = JSON.parse(value);
4444
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
4710
4445
  } catch {
4711
- return;
4446
+ return [];
4712
4447
  }
4713
- const reservationsDir = path7.join(projectDir, ".reservations");
4714
- if (fs7.existsSync(reservationsDir)) {
4448
+ }
4449
+ function formatDate(iso) {
4450
+ try {
4451
+ return iso.split("T")[0] || iso.substring(0, 10);
4452
+ } catch {
4453
+ return iso;
4454
+ }
4455
+ }
4456
+ function generateMemoryDigest(projectDir, config) {
4457
+ const result = { written: false, path: "", counts: {} };
4458
+ if (typeof projectDir !== "string" || !projectDir)
4459
+ return result;
4460
+ const memoryDir = path6.join(projectDir, ".opencode", "memory");
4461
+ const dbPath = path6.join(memoryDir, "memory.db");
4462
+ if (!fs5.existsSync(dbPath)) {
4463
+ return result;
4464
+ }
4465
+ const maxPerType = config?.max_per_type ?? 10;
4466
+ const includeTypes = config?.include_types ?? [
4467
+ "decision",
4468
+ "learning",
4469
+ "blocker",
4470
+ "progress",
4471
+ "handoff"
4472
+ ];
4473
+ let db;
4474
+ try {
4475
+ db = new Database(dbPath, { readonly: true });
4476
+ } catch {
4477
+ return result;
4478
+ }
4479
+ const sections = [];
4480
+ sections.push("# Memory Digest");
4481
+ sections.push("");
4482
+ sections.push(`> Auto-generated on ${new Date().toISOString().split("T")[0]}. Read-only reference for agents.`);
4483
+ sections.push(`> Source: \`.opencode/memory/memory.db\``);
4484
+ sections.push("");
4485
+ const typeLabels = {
4486
+ decision: { heading: "Past Decisions", emoji: "\uD83D\uDD37" },
4487
+ learning: { heading: "Learnings & Gotchas", emoji: "\uD83D\uDCA1" },
4488
+ blocker: { heading: "Past Blockers", emoji: "\uD83D\uDEA7" },
4489
+ progress: { heading: "Recent Progress", emoji: "\uD83D\uDCC8" },
4490
+ handoff: { heading: "Session Handoffs", emoji: "\uD83D\uDD04" }
4491
+ };
4492
+ let totalCount = 0;
4493
+ for (const type of includeTypes) {
4715
4494
  try {
4716
- const lockFiles = fs7.readdirSync(reservationsDir).filter((f) => f.endsWith(".lock"));
4717
- const reservedFiles = [];
4718
- for (const lockFile of lockFiles) {
4719
- try {
4720
- const raw = fs7.readFileSync(path7.join(reservationsDir, lockFile), "utf-8");
4721
- const lock = JSON.parse(raw);
4722
- if (isRecord2(lock) && typeof lock.path === "string") {
4723
- reservedFiles.push(lock.path);
4724
- }
4725
- if (isRecord2(lock) && typeof lock.agent === "string") {
4726
- state.agentId = lock.agent;
4495
+ const rows = db.prepare(`SELECT * FROM observations WHERE type = ? ORDER BY created_at DESC LIMIT ?`).all(type, maxPerType);
4496
+ if (rows.length === 0)
4497
+ continue;
4498
+ result.counts[type] = rows.length;
4499
+ totalCount += rows.length;
4500
+ const label = typeLabels[type] || { heading: type, emoji: "\uD83D\uDCCC" };
4501
+ sections.push(`## ${label.emoji} ${label.heading}`);
4502
+ sections.push("");
4503
+ for (const row of rows) {
4504
+ const date = formatDate(row.created_at);
4505
+ const facts = parseJsonArray(row.facts);
4506
+ const concepts = parseJsonArray(row.concepts);
4507
+ const filesModified = parseJsonArray(row.files_modified);
4508
+ sections.push(`### ${date} \u2014 ${row.narrative.split(`
4509
+ `)[0]}`);
4510
+ if (row.confidence < 1) {
4511
+ sections.push(`> Confidence: ${(row.confidence * 100).toFixed(0)}%`);
4512
+ }
4513
+ sections.push("");
4514
+ if (row.narrative.includes(`
4515
+ `)) {
4516
+ sections.push(row.narrative);
4517
+ sections.push("");
4518
+ }
4519
+ if (facts.length > 0) {
4520
+ sections.push("**Facts:**");
4521
+ for (const fact of facts) {
4522
+ sections.push(`- ${fact}`);
4727
4523
  }
4728
- } catch {}
4729
- }
4730
- if (reservedFiles.length > 0) {
4731
- state.reservedFiles = reservedFiles;
4524
+ sections.push("");
4525
+ }
4526
+ if (filesModified.length > 0) {
4527
+ sections.push(`**Files:** ${filesModified.map((f) => `\`${f}\``).join(", ")}`);
4528
+ sections.push("");
4529
+ }
4530
+ if (concepts.length > 0) {
4531
+ sections.push(`**Concepts:** ${concepts.join(", ")}`);
4532
+ sections.push("");
4533
+ }
4534
+ if (row.bead_id) {
4535
+ sections.push(`**Bead:** ${row.bead_id}`);
4536
+ sections.push("");
4537
+ }
4538
+ sections.push("---");
4539
+ sections.push("");
4732
4540
  }
4733
4541
  } catch {}
4734
4542
  }
4735
4543
  try {
4736
- const output = execSync5("bd ls --status in_progress --json 2>/dev/null", {
4737
- cwd: projectDir,
4738
- timeout: 5000,
4739
- encoding: "utf-8",
4740
- stdio: ["pipe", "pipe", "pipe"]
4741
- });
4742
- if (output.trim()) {
4743
- const tasks = JSON.parse(output);
4744
- if (Array.isArray(tasks) && tasks.length > 0) {
4745
- const first = tasks[0];
4746
- if (isRecord2(first)) {
4747
- const title = typeof first.title === "string" ? first.title : undefined;
4748
- const shortTitle = typeof first.t === "string" ? first.t : undefined;
4749
- const id = typeof first.id === "string" ? first.id : undefined;
4750
- if (title || shortTitle)
4751
- state.currentTask = title || shortTitle;
4752
- if (id)
4753
- state.taskId = id;
4754
- }
4755
- state.inProgressCount = tasks.length;
4756
- }
4757
- }
4544
+ db.close();
4758
4545
  } catch {}
4546
+ if (totalCount === 0) {
4547
+ sections.push("*No observations found in memory database.*");
4548
+ sections.push("");
4549
+ }
4550
+ const digestPath = path6.join(memoryDir, "_digest.md");
4551
+ const content = sections.join(`
4552
+ `);
4759
4553
  try {
4760
- const output = execSync5("bd ls --status open --json 2>/dev/null", {
4761
- cwd: projectDir,
4762
- timeout: 5000,
4763
- encoding: "utf-8",
4764
- stdio: ["pipe", "pipe", "pipe"]
4765
- });
4766
- if (output.trim()) {
4767
- const tasks = JSON.parse(output);
4768
- if (Array.isArray(tasks)) {
4769
- state.openCount = tasks.length;
4770
- }
4554
+ if (!fs5.existsSync(memoryDir)) {
4555
+ fs5.mkdirSync(memoryDir, { recursive: true });
4771
4556
  }
4557
+ fs5.writeFileSync(digestPath, content, "utf-8");
4558
+ result.written = true;
4559
+ result.path = digestPath;
4772
4560
  } catch {}
4773
- if (state.currentTask || state.reservedFiles?.length || state.agentId || state.inProgressCount || state.openCount) {
4774
- return state;
4561
+ return result;
4562
+ }
4563
+ function formatDigestLog(result) {
4564
+ if (!result.written) {
4565
+ return "[CliKit:memory-digest] No digest generated (no DB or empty)";
4775
4566
  }
4776
- return;
4567
+ const parts = Object.entries(result.counts).map(([type, count]) => `${count} ${type}s`).join(", ");
4568
+ return `[CliKit:memory-digest] Generated digest: ${parts || "empty"}`;
4777
4569
  }
4778
- function readMemoryRefs(projectDir, limit = 10) {
4779
- if (typeof projectDir !== "string" || !projectDir) {
4780
- return [];
4570
+ // src/hooks/todo-beads-sync.ts
4571
+ import * as fs6 from "fs";
4572
+ import * as path7 from "path";
4573
+ import { Database as Database2 } from "bun:sqlite";
4574
+ function mapTodoStatusToIssueStatus(status) {
4575
+ const value = status.toLowerCase();
4576
+ if (value === "completed" || value === "done" || value === "cancelled") {
4577
+ return "closed";
4781
4578
  }
4782
- const memoryDir = path7.join(projectDir, ".opencode", "memory");
4783
- if (!fs7.existsSync(memoryDir))
4784
- return [];
4785
- const MEMORY_SUBDIRS = ["specs", "plans", "research", "reviews", "handoffs", "beads"];
4786
- const refs = [];
4787
- try {
4788
- for (const subdir of MEMORY_SUBDIRS) {
4789
- const subdirPath = path7.join(memoryDir, subdir);
4790
- if (!fs7.existsSync(subdirPath))
4791
- continue;
4792
- const files = fs7.readdirSync(subdirPath).filter((f) => f.endsWith(".md") || f.endsWith(".json")).sort();
4793
- for (const file of files) {
4794
- if (file === ".gitkeep")
4795
- continue;
4796
- try {
4797
- const fullPath = path7.join(subdirPath, file);
4798
- const stat = fs7.statSync(fullPath);
4799
- const raw = fs7.readFileSync(fullPath, "utf-8");
4800
- if (typeof raw !== "string" || !raw.trim())
4801
- continue;
4802
- let summary;
4803
- const ext = path7.extname(file);
4804
- if (ext === ".md") {
4805
- const headingMatch = raw.match(/^#\s+(.+)$/m);
4806
- summary = headingMatch ? headingMatch[1] : raw.substring(0, 100).trim();
4807
- } else if (ext === ".json") {
4808
- const parsed = JSON.parse(raw);
4809
- const safe = isRecord2(parsed) ? parsed : {};
4810
- const content = safe.content;
4811
- const summaryText = typeof safe.summary === "string" ? safe.summary : undefined;
4812
- const titleText = typeof safe.title === "string" ? safe.title : undefined;
4813
- const contentText = typeof content === "string" ? content.substring(0, 100) : "";
4814
- summary = summaryText || titleText || contentText || "";
4815
- } else {
4816
- summary = raw.substring(0, 100).trim();
4817
- }
4818
- refs.push({
4819
- key: path7.basename(file, ext),
4820
- summary,
4821
- timestamp: stat.mtimeMs,
4822
- category: subdir
4823
- });
4824
- } catch {}
4825
- }
4826
- }
4827
- return refs.sort((a, b) => b.timestamp - a.timestamp).slice(0, limit);
4828
- } catch {
4829
- return [];
4579
+ if (value === "in_progress" || value === "in-progress") {
4580
+ return "in_progress";
4830
4581
  }
4582
+ return "open";
4831
4583
  }
4832
- function buildCompactionBlock(payload, maxChars = 5000) {
4833
- const lines = ["<compaction-context>"];
4834
- if (payload.sessionSummary) {
4835
- lines.push(`
4836
- Session Summary: ${payload.sessionSummary}`);
4837
- }
4838
- if (payload.beads) {
4839
- lines.push(`
4840
- Beads State:`);
4841
- if (payload.beads.currentTask)
4842
- lines.push(` Current task: ${payload.beads.currentTask}`);
4843
- if (payload.beads.taskId)
4844
- lines.push(` Task ID: ${payload.beads.taskId}`);
4845
- if (payload.beads.agentId)
4846
- lines.push(` Agent: ${payload.beads.agentId}`);
4847
- if (payload.beads.team)
4848
- lines.push(` Team: ${payload.beads.team}`);
4849
- if (payload.beads.inProgressCount)
4850
- lines.push(` In-progress tasks: ${payload.beads.inProgressCount}`);
4851
- if (payload.beads.openCount)
4852
- lines.push(` Open tasks: ${payload.beads.openCount}`);
4853
- if (payload.beads.reservedFiles?.length) {
4854
- lines.push(` Reserved files: ${payload.beads.reservedFiles.join(", ")}`);
4855
- }
4856
- }
4857
- const normalizedTodos = normalizeTodoEntries(payload.todos);
4858
- if (normalizedTodos.length) {
4859
- lines.push(`
4860
- Todo State:`);
4861
- for (const todo of normalizedTodos) {
4862
- const icon = todo.status === "completed" ? "[x]" : todo.status === "in-progress" ? "[~]" : "[ ]";
4863
- lines.push(` ${icon} ${todo.id}: ${todo.content}`);
4864
- }
4865
- }
4866
- if (payload.memories?.length) {
4867
- lines.push(`
4868
- Recent Memory References:`);
4869
- for (const mem of payload.memories) {
4870
- lines.push(` - [${mem.category}] ${mem.key}: ${mem.summary}`);
4871
- }
4872
- }
4873
- lines.push("</compaction-context>");
4874
- const block = lines.join(`
4875
- `);
4876
- if (typeof block !== "string")
4877
- return `<compaction-context>
4878
- </compaction-context>`;
4879
- if (block.length > maxChars) {
4880
- return block.substring(0, maxChars) + `
4881
- ... [compaction context truncated]
4882
- </compaction-context>`;
4883
- }
4884
- return block;
4584
+ function mapTodoPriorityToIssuePriority(priority) {
4585
+ const value = (priority || "").toLowerCase();
4586
+ if (value === "high")
4587
+ return 1;
4588
+ if (value === "low")
4589
+ return 3;
4590
+ return 2;
4885
4591
  }
4886
- function collectCompactionPayload(projectDir, config) {
4887
- const payload = {};
4888
- if (typeof projectDir !== "string" || !projectDir) {
4889
- return payload;
4890
- }
4891
- if (config?.include_beads_state !== false) {
4892
- payload.beads = readBeadsState(projectDir);
4592
+ function sanitizeId(value) {
4593
+ const normalized = value.replace(/[^a-zA-Z0-9_-]/g, "-");
4594
+ return normalized.length > 0 ? normalized : "unknown";
4595
+ }
4596
+ function buildIssueId(sessionID, todoID) {
4597
+ const sessionPart = sanitizeId(sessionID).slice(0, 16);
4598
+ const todoPart = sanitizeId(todoID).slice(0, 32);
4599
+ return `oc-${sessionPart}-${todoPart}`;
4600
+ }
4601
+ function syncTodosToBeads(projectDirectory, sessionID, todos, config) {
4602
+ const beadsDbPath = path7.join(projectDirectory, ".beads", "beads.db");
4603
+ if (!fs6.existsSync(beadsDbPath)) {
4604
+ return {
4605
+ synced: false,
4606
+ sessionID,
4607
+ totalTodos: todos.length,
4608
+ created: 0,
4609
+ updated: 0,
4610
+ closed: 0,
4611
+ skippedReason: "No .beads/beads.db found"
4612
+ };
4893
4613
  }
4894
- if (config?.include_memory_refs !== false) {
4895
- payload.memories = readMemoryRefs(projectDir);
4614
+ const db = new Database2(beadsDbPath);
4615
+ let created = 0;
4616
+ let updated = 0;
4617
+ let closed = 0;
4618
+ try {
4619
+ const prefix = `opencode:todo:${sessionID}:`;
4620
+ const existingRows = db.query("SELECT external_ref, status FROM issues WHERE external_ref LIKE ?").all(`${prefix}%`);
4621
+ const existingByRef = new Map(existingRows.map((row) => [row.external_ref, row.status]));
4622
+ const activeRefs = new Set;
4623
+ const upsertIssue = db.prepare(`
4624
+ INSERT INTO issues (
4625
+ id, title, description, status, priority, issue_type, external_ref, source_repo, updated_at, closed_at
4626
+ ) VALUES (?, ?, ?, ?, ?, 'task', ?, '.', CURRENT_TIMESTAMP, CASE WHEN ? = 'closed' THEN CURRENT_TIMESTAMP ELSE NULL END)
4627
+ ON CONFLICT(external_ref) DO UPDATE SET
4628
+ title = excluded.title,
4629
+ description = excluded.description,
4630
+ status = excluded.status,
4631
+ priority = excluded.priority,
4632
+ updated_at = CURRENT_TIMESTAMP,
4633
+ closed_at = CASE
4634
+ WHEN excluded.status = 'closed' THEN COALESCE(issues.closed_at, CURRENT_TIMESTAMP)
4635
+ ELSE NULL
4636
+ END
4637
+ `);
4638
+ for (const todo of todos) {
4639
+ const externalRef = `${prefix}${todo.id}`;
4640
+ activeRefs.add(externalRef);
4641
+ const mappedStatus = mapTodoStatusToIssueStatus(todo.status);
4642
+ const issueId = buildIssueId(sessionID, todo.id);
4643
+ const title = (todo.content || "Untitled todo").slice(0, 500);
4644
+ const priority = mapTodoPriorityToIssuePriority(todo.priority);
4645
+ const description = `Synced from OpenCode todo ${todo.id} (session ${sessionID}).`;
4646
+ if (existingByRef.has(externalRef)) {
4647
+ updated += 1;
4648
+ } else {
4649
+ created += 1;
4650
+ }
4651
+ upsertIssue.run(issueId, title, description, mappedStatus, priority, externalRef, mappedStatus);
4652
+ }
4653
+ if (config?.close_missing !== false) {
4654
+ const closeIssue = db.prepare("UPDATE issues SET status = 'closed', closed_at = COALESCE(closed_at, CURRENT_TIMESTAMP), updated_at = CURRENT_TIMESTAMP WHERE external_ref = ? AND status != 'closed'");
4655
+ for (const [externalRef, status] of existingByRef.entries()) {
4656
+ if (!activeRefs.has(externalRef) && status !== "closed") {
4657
+ closeIssue.run(externalRef);
4658
+ closed += 1;
4659
+ }
4660
+ }
4661
+ }
4662
+ return {
4663
+ synced: true,
4664
+ sessionID,
4665
+ totalTodos: todos.length,
4666
+ created,
4667
+ updated,
4668
+ closed
4669
+ };
4670
+ } finally {
4671
+ db.close();
4896
4672
  }
4897
- return payload;
4898
4673
  }
4899
- function formatCompactionLog(payload) {
4900
- const parts = [];
4901
- if (payload.beads)
4902
- parts.push("beads-state");
4903
- if (payload.memories?.length)
4904
- parts.push(`${payload.memories.length} memories`);
4905
- if (payload.todos?.length)
4906
- parts.push(`${payload.todos.length} todos`);
4907
- return `[CliKit:compaction] Injected: ${parts.join(", ") || "nothing"}`;
4674
+ function formatTodoBeadsSyncLog(result) {
4675
+ if (!result.synced) {
4676
+ return `[CliKit:todo-beads-sync] skipped (${result.skippedReason || "unknown reason"})`;
4677
+ }
4678
+ return `[CliKit:todo-beads-sync] session=${result.sessionID} todos=${result.totalTodos} created=${result.created} updated=${result.updated} closed=${result.closed}`;
4908
4679
  }
4909
- // src/hooks/ritual-enforcer.ts
4910
- import * as path8 from "path";
4911
- var RITUAL_FILE = path8.join(process.cwd(), ".opencode", "memory", "ritual-state.json");
4912
4680
  // src/index.ts
4681
+ var execFileAsync = promisify(execFile);
4913
4682
  var CliKitPlugin = async (ctx) => {
4914
- console.log("[CliKit] Plugin initializing...");
4915
- console.log("[CliKit] Context:", JSON.stringify({ directory: ctx?.directory, hasClient: !!ctx?.client }));
4683
+ const todosBySession = new Map;
4684
+ function getToolInput(args) {
4685
+ return args && typeof args === "object" ? args : {};
4686
+ }
4687
+ function blockToolExecution(reason) {
4688
+ throw new Error(`[CliKit] Blocked tool execution: ${reason}`);
4689
+ }
4690
+ async function showToast(message, variant, title = "CliKit") {
4691
+ try {
4692
+ await ctx.client.tui.showToast({
4693
+ body: {
4694
+ title,
4695
+ message,
4696
+ variant,
4697
+ duration: 3500
4698
+ }
4699
+ });
4700
+ } catch {}
4701
+ }
4702
+ function normalizeTodos(rawTodos) {
4703
+ if (!Array.isArray(rawTodos)) {
4704
+ return [];
4705
+ }
4706
+ const normalized = [];
4707
+ for (const entry of rawTodos) {
4708
+ if (!entry || typeof entry !== "object") {
4709
+ continue;
4710
+ }
4711
+ const record = entry;
4712
+ const id = typeof record.id === "string" ? record.id : "";
4713
+ const content = typeof record.content === "string" ? record.content : "";
4714
+ const status = typeof record.status === "string" ? record.status : "todo";
4715
+ const priority = typeof record.priority === "string" ? record.priority : undefined;
4716
+ if (!id || !content) {
4717
+ continue;
4718
+ }
4719
+ normalized.push({ id, content, status, priority });
4720
+ }
4721
+ return normalized;
4722
+ }
4723
+ async function getStagedFiles() {
4724
+ try {
4725
+ const { stdout } = await execFileAsync("git", ["diff", "--cached", "--name-only"], {
4726
+ cwd: ctx.directory,
4727
+ encoding: "utf-8"
4728
+ });
4729
+ return stdout.split(`
4730
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
4731
+ } catch {
4732
+ return [];
4733
+ }
4734
+ }
4735
+ async function getStagedDiff() {
4736
+ try {
4737
+ const { stdout } = await execFileAsync("git", ["diff", "--cached", "--no-color"], {
4738
+ cwd: ctx.directory,
4739
+ encoding: "utf-8"
4740
+ });
4741
+ return stdout;
4742
+ } catch {
4743
+ return "";
4744
+ }
4745
+ }
4916
4746
  const pluginConfig = loadCliKitConfig(ctx.directory) ?? {};
4747
+ const debugLogsEnabled = pluginConfig.hooks?.session_logging === true && process.env.CLIKIT_DEBUG === "1";
4748
+ const toolLogsEnabled = pluginConfig.hooks?.tool_logging === true && process.env.CLIKIT_DEBUG === "1";
4749
+ const DIGEST_THROTTLE_MS = 60000;
4750
+ let lastDigestTime = 0;
4751
+ let lastTodoHash = "";
4917
4752
  const builtinAgents = getBuiltinAgents();
4918
4753
  const builtinCommands = getBuiltinCommands();
4754
+ const builtinSkills = getBuiltinSkills();
4919
4755
  const filteredAgents = filterAgents(builtinAgents, pluginConfig);
4920
4756
  const filteredCommands = filterCommands(builtinCommands, pluginConfig);
4921
- console.log(`[CliKit] Loaded ${Object.keys(filteredAgents).length}/${Object.keys(builtinAgents).length} agents`);
4922
- console.log(`[CliKit] Loaded ${Object.keys(filteredCommands).length}/${Object.keys(builtinCommands).length} commands`);
4923
- if (pluginConfig.disabled_agents?.length) {
4924
- console.log(`[CliKit] Disabled agents: ${pluginConfig.disabled_agents.join(", ")}`);
4925
- }
4926
- if (pluginConfig.disabled_commands?.length) {
4927
- console.log(`[CliKit] Disabled commands: ${pluginConfig.disabled_commands.join(", ")}`);
4757
+ const filteredSkills = filterSkills(builtinSkills, pluginConfig);
4758
+ if (debugLogsEnabled) {
4759
+ console.log("[CliKit] Plugin initializing...");
4760
+ console.log("[CliKit] Context:", JSON.stringify({ directory: ctx?.directory, hasClient: !!ctx?.client }));
4761
+ console.log(`[CliKit] Loaded ${Object.keys(filteredAgents).length}/${Object.keys(builtinAgents).length} agents`);
4762
+ console.log(`[CliKit] Loaded ${Object.keys(filteredCommands).length}/${Object.keys(builtinCommands).length} commands`);
4763
+ console.log(`[CliKit] Loaded ${Object.keys(filteredSkills).length}/${Object.keys(builtinSkills).length} skills`);
4764
+ if (pluginConfig.disabled_agents?.length) {
4765
+ console.log(`[CliKit] Disabled agents: ${pluginConfig.disabled_agents.join(", ")}`);
4766
+ }
4767
+ if (pluginConfig.disabled_commands?.length) {
4768
+ console.log(`[CliKit] Disabled commands: ${pluginConfig.disabled_commands.join(", ")}`);
4769
+ }
4928
4770
  }
4929
4771
  return {
4930
4772
  config: async (config) => {
@@ -4948,7 +4790,9 @@ var CliKitPlugin = async (ctx) => {
4948
4790
  ...enabledLsp,
4949
4791
  ...config.lsp || {}
4950
4792
  };
4951
- console.log(`[CliKit] Injected ${Object.keys(enabledLsp).length} LSP server(s)`);
4793
+ if (debugLogsEnabled) {
4794
+ console.log(`[CliKit] Injected ${Object.keys(enabledLsp).length} LSP server(s)`);
4795
+ }
4952
4796
  }
4953
4797
  }
4954
4798
  },
@@ -4956,68 +4800,78 @@ var CliKitPlugin = async (ctx) => {
4956
4800
  const { event } = input;
4957
4801
  const props = event.properties;
4958
4802
  if (event.type === "session.created") {
4959
- if (pluginConfig.hooks?.session_logging) {
4960
- const info = props?.info;
4803
+ const info = props?.info;
4804
+ if (debugLogsEnabled) {
4961
4805
  console.log(`[CliKit] Session created: ${info?.id || "unknown"}`);
4962
4806
  }
4963
- if (pluginConfig.hooks?.env_context?.enabled !== false) {
4964
- const envConfig = pluginConfig.hooks?.env_context;
4965
- const envInfo = collectEnvInfo(ctx.directory, envConfig);
4966
- const envBlock = buildEnvBlock(envInfo);
4967
- console.log(formatEnvSummary(envInfo));
4968
- input.__envBlock = envBlock;
4807
+ if (pluginConfig.hooks?.memory_digest?.enabled !== false) {
4808
+ const digestResult = generateMemoryDigest(ctx.directory, pluginConfig.hooks?.memory_digest);
4809
+ lastDigestTime = Date.now();
4810
+ if (pluginConfig.hooks?.memory_digest?.log !== false) {
4811
+ console.log(formatDigestLog(digestResult));
4812
+ }
4969
4813
  }
4970
4814
  }
4971
4815
  if (event.type === "session.error") {
4972
4816
  const error = props?.error;
4973
- if (pluginConfig.hooks?.session_logging) {
4817
+ if (debugLogsEnabled) {
4974
4818
  console.error(`[CliKit] Session error:`, error);
4975
4819
  }
4976
- if (pluginConfig.hooks?.session_notification?.enabled !== false && pluginConfig.hooks?.session_notification?.on_error !== false) {
4977
- const notifConfig = pluginConfig.hooks?.session_notification;
4978
- const sessionId = props?.sessionID;
4979
- const payload = buildErrorNotification(error, sessionId, notifConfig?.title_prefix);
4980
- const sent = sendNotification(payload);
4981
- console.log(formatNotificationLog(payload, sent));
4820
+ }
4821
+ if (event.type === "todo.updated") {
4822
+ const sessionID = props?.sessionID;
4823
+ if (typeof sessionID === "string") {
4824
+ const todos = normalizeTodos(props?.todos);
4825
+ todosBySession.set(sessionID, todos);
4826
+ const todoHash = JSON.stringify(todos.map((t) => `${t.id}:${t.status}`));
4827
+ if (todoHash !== lastTodoHash) {
4828
+ lastTodoHash = todoHash;
4829
+ if (pluginConfig.hooks?.todo_beads_sync?.enabled !== false) {
4830
+ const result = syncTodosToBeads(ctx.directory, sessionID, todos, pluginConfig.hooks?.todo_beads_sync);
4831
+ if (pluginConfig.hooks?.todo_beads_sync?.log === true) {
4832
+ console.log(formatTodoBeadsSyncLog(result));
4833
+ }
4834
+ }
4835
+ }
4982
4836
  }
4983
4837
  }
4984
4838
  if (event.type === "session.idle") {
4985
4839
  const sessionID = props?.sessionID;
4986
- if (pluginConfig.hooks?.session_logging) {
4840
+ const sessionTodos = sessionID ? todosBySession.get(sessionID) || [] : [];
4841
+ if (debugLogsEnabled) {
4987
4842
  console.log(`[CliKit] Session idle: ${sessionID || "unknown"}`);
4988
4843
  }
4989
4844
  const todoConfig = pluginConfig.hooks?.todo_enforcer;
4990
4845
  if (todoConfig?.enabled !== false) {
4991
- const todos = props?.todos;
4992
- if (Array.isArray(todos) && todos.length > 0) {
4993
- const result = checkTodoCompletion(todos);
4846
+ const todos = normalizeTodos(props?.todos);
4847
+ const effectiveTodos = todos.length > 0 ? todos : sessionTodos;
4848
+ if (effectiveTodos.length > 0) {
4849
+ const result = checkTodoCompletion(effectiveTodos);
4994
4850
  if (!result.complete && todoConfig?.warn_on_incomplete !== false) {
4995
4851
  console.warn(formatIncompleteWarning(result, sessionID));
4996
4852
  }
4997
4853
  }
4998
4854
  }
4999
- if (pluginConfig.hooks?.session_notification?.enabled !== false && pluginConfig.hooks?.session_notification?.on_idle !== false) {
5000
- const notifConfig = pluginConfig.hooks?.session_notification;
5001
- const payload = buildIdleNotification(sessionID, notifConfig?.title_prefix);
5002
- const sent = sendNotification(payload);
5003
- console.log(formatNotificationLog(payload, sent));
5004
- }
5005
- if (pluginConfig.hooks?.compaction?.enabled !== false) {
5006
- const compConfig = pluginConfig.hooks?.compaction;
5007
- const compPayload = collectCompactionPayload(ctx.directory, compConfig);
5008
- if (compConfig?.include_todo_state !== false && props?.todos) {
5009
- compPayload.todos = props.todos;
4855
+ if (pluginConfig.hooks?.memory_digest?.enabled !== false) {
4856
+ const now = Date.now();
4857
+ if (now - lastDigestTime >= DIGEST_THROTTLE_MS) {
4858
+ generateMemoryDigest(ctx.directory, pluginConfig.hooks?.memory_digest);
4859
+ lastDigestTime = now;
5010
4860
  }
5011
- const block = buildCompactionBlock(compPayload, compConfig?.max_state_chars);
5012
- console.log(formatCompactionLog(compPayload));
5013
- input.__compactionBlock = block;
4861
+ }
4862
+ }
4863
+ if (event.type === "session.deleted") {
4864
+ const info = props?.info;
4865
+ const sessionID = info?.id;
4866
+ if (sessionID) {
4867
+ todosBySession.delete(sessionID);
5014
4868
  }
5015
4869
  }
5016
4870
  },
5017
- "tool.execute.before": async (input, _output) => {
4871
+ "tool.execute.before": async (input, output) => {
5018
4872
  const toolName = input.tool;
5019
- const toolInput = input.input ?? {};
5020
- if (pluginConfig.hooks?.tool_logging) {
4873
+ const toolInput = getToolInput(output.args);
4874
+ if (toolLogsEnabled) {
5021
4875
  console.log(`[CliKit] Tool executing: ${toolName}`);
5022
4876
  }
5023
4877
  if (pluginConfig.hooks?.git_guard?.enabled !== false) {
@@ -5028,8 +4882,8 @@ var CliKitPlugin = async (ctx) => {
5028
4882
  const result = checkDangerousCommand(command, allowForceWithLease);
5029
4883
  if (result.blocked) {
5030
4884
  console.warn(formatBlockedWarning(result));
5031
- input.__blocked = true;
5032
- input.__blockReason = result.reason;
4885
+ await showToast(result.reason || "Blocked dangerous git command", "warning", "CliKit Guard");
4886
+ blockToolExecution(result.reason || "Dangerous git command");
5033
4887
  }
5034
4888
  }
5035
4889
  }
@@ -5040,26 +4894,45 @@ var CliKitPlugin = async (ctx) => {
5040
4894
  if (command && /git\s+(commit|add)/.test(command)) {
5041
4895
  const secConfig = pluginConfig.hooks?.security_check;
5042
4896
  let shouldBlock = false;
5043
- const files = toolInput.files;
5044
- if (files) {
5045
- for (const file of files) {
5046
- if (isSensitiveFile(file)) {
5047
- console.warn(`[CliKit:security] Sensitive file staged: ${file}`);
5048
- shouldBlock = true;
5049
- }
4897
+ const [stagedFiles, stagedDiff] = await Promise.all([
4898
+ getStagedFiles(),
4899
+ getStagedDiff()
4900
+ ]);
4901
+ for (const file of stagedFiles) {
4902
+ if (isSensitiveFile(file)) {
4903
+ console.warn(`[CliKit:security] Sensitive file staged: ${file}`);
4904
+ shouldBlock = true;
5050
4905
  }
5051
4906
  }
5052
- const content = toolInput.content;
5053
- if (content) {
5054
- const scanResult = scanContentForSecrets(content);
4907
+ if (stagedDiff) {
4908
+ const scanResult = scanContentForSecrets(stagedDiff);
5055
4909
  if (!scanResult.safe) {
5056
4910
  console.warn(formatSecurityWarning(scanResult));
5057
4911
  shouldBlock = true;
5058
4912
  }
5059
4913
  }
5060
4914
  if (shouldBlock && secConfig?.block_commits) {
5061
- input.__blocked = true;
5062
- input.__blockReason = "Sensitive data detected in commit";
4915
+ await showToast("Blocked commit due to sensitive data", "error", "CliKit Security");
4916
+ blockToolExecution("Sensitive data detected in commit");
4917
+ }
4918
+ }
4919
+ }
4920
+ }
4921
+ if (pluginConfig.hooks?.swarm_enforcer?.enabled !== false) {
4922
+ const editTools = ["edit", "Edit", "write", "Write", "bash", "Bash"];
4923
+ if (editTools.includes(toolName)) {
4924
+ const targetFile = extractFileFromToolInput(toolName, toolInput);
4925
+ if (targetFile) {
4926
+ const taskScope = toolInput.taskScope || input.__taskScope;
4927
+ const enforcement = checkEditPermission(targetFile, taskScope, pluginConfig.hooks?.swarm_enforcer);
4928
+ if (!enforcement.allowed) {
4929
+ console.warn(formatEnforcementWarning(enforcement));
4930
+ if (pluginConfig.hooks?.swarm_enforcer?.block_unreserved_edits) {
4931
+ await showToast(enforcement.reason || "Edit blocked outside task scope", "warning", "CliKit Swarm");
4932
+ blockToolExecution(enforcement.reason || "Edit outside reserved task scope");
4933
+ }
4934
+ } else if (pluginConfig.hooks?.swarm_enforcer?.log === true) {
4935
+ console.log(`[CliKit:swarm-enforcer] Allowed edit: ${targetFile}`);
5063
4936
  }
5064
4937
  }
5065
4938
  }
@@ -5069,87 +4942,45 @@ var CliKitPlugin = async (ctx) => {
5069
4942
  const prompt = toolInput.prompt;
5070
4943
  if (prompt && containsQuestion(prompt)) {
5071
4944
  console.warn(formatBlockerWarning());
5072
- input.__blocked = true;
5073
- input.__blockReason = "Subagents should not ask questions";
4945
+ await showToast("Subagent prompt blocked: avoid direct questions", "warning", "CliKit Guard");
4946
+ blockToolExecution("Subagents should not ask questions");
5074
4947
  }
5075
4948
  }
5076
4949
  }
5077
4950
  },
5078
4951
  "tool.execute.after": async (input, output) => {
5079
4952
  const toolName = input.tool;
5080
- const toolInput = input.input ?? {};
5081
- const toolOutput = output;
5082
- if (pluginConfig.hooks?.tool_logging) {
4953
+ const toolInput = getToolInput(input.args);
4954
+ let toolOutputContent = output.output;
4955
+ if (toolLogsEnabled) {
5083
4956
  console.log(`[CliKit] Tool completed: ${toolName} -> ${output.title}`);
5084
4957
  }
5085
4958
  const sanitizerConfig = pluginConfig.hooks?.empty_message_sanitizer;
5086
4959
  if (sanitizerConfig?.enabled !== false) {
5087
- if (toolOutput.content !== undefined && isEmptyContent(toolOutput.content)) {
4960
+ if (isEmptyContent(toolOutputContent)) {
5088
4961
  const placeholder = sanitizerConfig?.placeholder || "(No output)";
5089
- if (sanitizerConfig?.log_empty !== false) {
4962
+ if (sanitizerConfig?.log_empty === true) {
5090
4963
  console.log(`[CliKit] Empty output detected for tool: ${toolName}`);
5091
4964
  }
5092
- toolOutput.content = sanitizeContent(toolOutput.content, placeholder);
5093
- }
5094
- }
5095
- if (pluginConfig.hooks?.comment_checker?.enabled !== false) {
5096
- if (toolName === "edit" || toolName === "Edit" || toolName === "write" || toolName === "Write") {
5097
- const content = toolOutput.content;
5098
- if (typeof content === "string" && content.length > 100) {
5099
- const threshold = pluginConfig.hooks?.comment_checker?.threshold ?? 0.3;
5100
- const densityResult = checkCommentDensity(content, threshold);
5101
- if (densityResult.excessive) {
5102
- console.warn(formatCommentWarning(densityResult));
5103
- }
5104
- if (hasExcessiveAIComments(content)) {
5105
- console.warn("[CliKit:comment-checker] Detected AI-style boilerplate comments. Remove unnecessary comments.");
5106
- }
4965
+ const sanitized = sanitizeContent(toolOutputContent, placeholder);
4966
+ if (typeof sanitized === "string") {
4967
+ toolOutputContent = sanitized;
4968
+ output.output = sanitized;
5107
4969
  }
5108
4970
  }
5109
4971
  }
5110
4972
  if (pluginConfig.hooks?.truncator?.enabled !== false) {
5111
- if (typeof toolOutput.content === "string" && shouldTruncate(toolOutput.content, pluginConfig.hooks?.truncator)) {
5112
- const result = truncateOutput(toolOutput.content, pluginConfig.hooks?.truncator);
4973
+ if (shouldTruncate(toolOutputContent, pluginConfig.hooks?.truncator)) {
4974
+ const result = truncateOutput(toolOutputContent, pluginConfig.hooks?.truncator);
5113
4975
  if (result.truncated) {
5114
- toolOutput.content = result.content;
5115
- if (pluginConfig.hooks?.truncator?.log !== false) {
4976
+ toolOutputContent = result.content;
4977
+ output.output = result.content;
4978
+ if (pluginConfig.hooks?.truncator?.log === true) {
5116
4979
  console.log(formatTruncationLog(result));
5117
4980
  }
5118
4981
  }
5119
4982
  }
5120
4983
  }
5121
- if (pluginConfig.hooks?.auto_format?.enabled) {
5122
- if (toolName === "edit" || toolName === "Edit" || toolName === "write" || toolName === "Write") {
5123
- const filePath = toolInput.filePath;
5124
- if (filePath) {
5125
- const fmtConfig = pluginConfig.hooks.auto_format;
5126
- if (shouldFormat(filePath, fmtConfig?.extensions)) {
5127
- const result = runFormatter(filePath, ctx.directory, fmtConfig?.formatter);
5128
- if (fmtConfig?.log !== false) {
5129
- console.log(formatAutoFormatLog(result));
5130
- }
5131
- }
5132
- }
5133
- }
5134
- }
5135
- if (pluginConfig.hooks?.typecheck_gate?.enabled) {
5136
- if (toolName === "edit" || toolName === "Edit" || toolName === "write" || toolName === "Write") {
5137
- const filePath = toolInput.filePath;
5138
- if (filePath && isTypeScriptFile(filePath)) {
5139
- const tcConfig = pluginConfig.hooks.typecheck_gate;
5140
- const result = runTypeCheck(filePath, ctx.directory, tcConfig);
5141
- if (!result.clean) {
5142
- console.warn(formatTypeCheckWarning(result));
5143
- if (tcConfig?.block_on_error) {
5144
- input.__blocked = true;
5145
- input.__blockReason = `Type errors in ${filePath}`;
5146
- }
5147
- } else if (tcConfig?.log !== false) {
5148
- console.log(formatTypeCheckWarning(result));
5149
- }
5150
- }
5151
- }
5152
- }
5153
4984
  }
5154
4985
  };
5155
4986
  };