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.
- package/AGENTS.md +30 -32
- package/README.md +45 -26
- package/command/create.md +37 -122
- package/command/handoff.md +45 -69
- package/command/init.md +125 -48
- package/command/plan.md +101 -159
- package/command/research.md +1 -1
- package/command/resume.md +34 -55
- package/command/vision.md +132 -64
- package/dist/.tsbuildinfo +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +95 -11
- package/dist/clikit.schema.json +245 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/config.d.ts +43 -43
- package/dist/config.d.ts.map +1 -1
- package/dist/hooks/git-guard.test.d.ts +2 -0
- package/dist/hooks/git-guard.test.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +3 -14
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/memory-digest.d.ts +27 -0
- package/dist/hooks/memory-digest.d.ts.map +1 -0
- package/dist/hooks/security-check.test.d.ts +2 -0
- package/dist/hooks/security-check.test.d.ts.map +1 -0
- package/dist/hooks/todo-beads-sync.d.ts +23 -0
- package/dist/hooks/todo-beads-sync.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +740 -909
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/tools/beads-memory-sync.d.ts.map +1 -1
- package/dist/tools/context-summary.d.ts.map +1 -1
- package/dist/tools/memory-db.d.ts +12 -0
- package/dist/tools/memory-db.d.ts.map +1 -0
- package/dist/tools/memory.d.ts.map +1 -1
- package/dist/tools/observation.d.ts.map +1 -1
- package/memory/_templates/plan.md +18 -0
- package/package.json +7 -4
- package/src/agents/AGENTS.md +11 -39
- package/src/agents/build.md +152 -94
- package/src/agents/index.ts +31 -5
- package/src/agents/looker.md +5 -0
- package/src/agents/oracle.md +2 -0
- package/src/agents/plan.md +247 -44
- package/src/agents/review.md +1 -0
- package/src/agents/vision.md +251 -115
- package/dist/cli.test.d.ts +0 -2
- package/dist/cli.test.d.ts.map +0 -1
- package/dist/hooks/auto-format.d.ts +0 -30
- package/dist/hooks/auto-format.d.ts.map +0 -1
- package/dist/hooks/comment-checker.d.ts +0 -17
- package/dist/hooks/comment-checker.d.ts.map +0 -1
- package/dist/hooks/compaction.d.ts +0 -60
- package/dist/hooks/compaction.d.ts.map +0 -1
- package/dist/hooks/env-context.d.ts +0 -43
- package/dist/hooks/env-context.d.ts.map +0 -1
- package/dist/hooks/ritual-enforcer.d.ts +0 -29
- package/dist/hooks/ritual-enforcer.d.ts.map +0 -1
- package/dist/hooks/session-notification.d.ts +0 -23
- package/dist/hooks/session-notification.d.ts.map +0 -1
- package/dist/hooks/session-notification.test.d.ts +0 -2
- package/dist/hooks/session-notification.test.d.ts.map +0 -1
- package/dist/hooks/typecheck-gate.d.ts +0 -31
- package/dist/hooks/typecheck-gate.d.ts.map +0 -1
- package/memory/handoffs/2026-02-15-complete-audit.md +0 -136
- package/memory/handoffs/2026-02-15-complete-fix.md +0 -140
- package/memory/handoffs/2026-02-15-importmeta-fix.md +0 -121
- package/memory/handoffs/2026-02-15-installing.md +0 -90
- package/memory/handoffs/2026-02-15-plugin-install-fix.md +0 -140
- package/memory/handoffs/2026-02-15-runtime-fixes.md +0 -80
- package/memory/plans/2026-02-16-plugin-install.md +0 -195
- 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
|
|
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
|
-
|
|
3544
|
+
const agentsDir = resolveAgentsDir();
|
|
3545
|
+
if (!fs.existsSync(agentsDir)) {
|
|
3530
3546
|
return agents;
|
|
3531
3547
|
}
|
|
3532
|
-
const files = fs.readdirSync(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3615
|
+
const commandsDir = resolveCommandsDir();
|
|
3616
|
+
if (!fs2.existsSync(commandsDir)) {
|
|
3578
3617
|
return commands;
|
|
3579
3618
|
}
|
|
3580
|
-
const files = fs2.readdirSync(
|
|
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(
|
|
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
|
-
|
|
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/
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 ||
|
|
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
|
|
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 (!
|
|
3825
|
+
if (!fs4.existsSync(filePath)) {
|
|
3686
3826
|
return null;
|
|
3687
3827
|
}
|
|
3688
|
-
const content =
|
|
3689
|
-
|
|
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
|
|
3711
|
-
const
|
|
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
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
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
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
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/
|
|
4669
|
-
import * as
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
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
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
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
|
|
4373
|
+
return false;
|
|
4694
4374
|
}
|
|
4695
|
-
function
|
|
4696
|
-
if (
|
|
4697
|
-
return;
|
|
4375
|
+
function checkEditPermission(filePath, scope, config) {
|
|
4376
|
+
if (!scope) {
|
|
4377
|
+
return { allowed: true };
|
|
4698
4378
|
}
|
|
4699
|
-
|
|
4700
|
-
|
|
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
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
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
|
-
|
|
4708
|
-
|
|
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
|
-
|
|
4714
|
-
|
|
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
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
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
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4761
|
-
|
|
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
|
-
|
|
4774
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
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
|
-
|
|
4783
|
-
|
|
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
|
|
4833
|
-
const
|
|
4834
|
-
if (
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
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
|
|
4887
|
-
const
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
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
|
-
|
|
4895
|
-
|
|
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
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
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
|
-
|
|
4915
|
-
|
|
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
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
console.log(
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
console.log(`[CliKit]
|
|
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
|
-
|
|
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
|
-
|
|
4960
|
-
|
|
4803
|
+
const info = props?.info;
|
|
4804
|
+
if (debugLogsEnabled) {
|
|
4961
4805
|
console.log(`[CliKit] Session created: ${info?.id || "unknown"}`);
|
|
4962
4806
|
}
|
|
4963
|
-
if (pluginConfig.hooks?.
|
|
4964
|
-
const
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
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 (
|
|
4817
|
+
if (debugLogsEnabled) {
|
|
4974
4818
|
console.error(`[CliKit] Session error:`, error);
|
|
4975
4819
|
}
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
const
|
|
4981
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4993
|
-
|
|
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?.
|
|
5000
|
-
const
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
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
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
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,
|
|
4871
|
+
"tool.execute.before": async (input, output) => {
|
|
5018
4872
|
const toolName = input.tool;
|
|
5019
|
-
const toolInput =
|
|
5020
|
-
if (
|
|
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
|
-
|
|
5032
|
-
|
|
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
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
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
|
-
|
|
5053
|
-
|
|
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
|
-
|
|
5062
|
-
|
|
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
|
-
|
|
5073
|
-
|
|
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.
|
|
5081
|
-
|
|
5082
|
-
if (
|
|
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 (
|
|
4960
|
+
if (isEmptyContent(toolOutputContent)) {
|
|
5088
4961
|
const placeholder = sanitizerConfig?.placeholder || "(No output)";
|
|
5089
|
-
if (sanitizerConfig?.log_empty
|
|
4962
|
+
if (sanitizerConfig?.log_empty === true) {
|
|
5090
4963
|
console.log(`[CliKit] Empty output detected for tool: ${toolName}`);
|
|
5091
4964
|
}
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
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 (
|
|
5112
|
-
const result = truncateOutput(
|
|
4973
|
+
if (shouldTruncate(toolOutputContent, pluginConfig.hooks?.truncator)) {
|
|
4974
|
+
const result = truncateOutput(toolOutputContent, pluginConfig.hooks?.truncator);
|
|
5113
4975
|
if (result.truncated) {
|
|
5114
|
-
|
|
5115
|
-
|
|
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
|
};
|