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