ctxloom-pro 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -3460,8 +3460,8 @@ var CoChangeIndex = class _CoChangeIndex {
|
|
|
3460
3460
|
if (event.isBulk || event.isMerge) return;
|
|
3461
3461
|
const paths = event.files.map((f) => f.path);
|
|
3462
3462
|
if (paths.length === 0) return;
|
|
3463
|
-
for (const
|
|
3464
|
-
this.nodeCounts.set(
|
|
3463
|
+
for (const path37 of paths) {
|
|
3464
|
+
this.nodeCounts.set(path37, (this.nodeCounts.get(path37) ?? 0) + 1);
|
|
3465
3465
|
}
|
|
3466
3466
|
for (let i = 0; i < paths.length; i++) {
|
|
3467
3467
|
for (let j = i + 1; j < paths.length; j++) {
|
|
@@ -3608,8 +3608,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3608
3608
|
*/
|
|
3609
3609
|
snapshot() {
|
|
3610
3610
|
const nodes = {};
|
|
3611
|
-
for (const [
|
|
3612
|
-
nodes[
|
|
3611
|
+
for (const [path37, raw] of this.nodes) {
|
|
3612
|
+
nodes[path37] = {
|
|
3613
3613
|
commits: raw.commits,
|
|
3614
3614
|
churnLines: raw.churnLines,
|
|
3615
3615
|
bugCommits: raw.bugCommits,
|
|
@@ -3624,8 +3624,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3624
3624
|
*/
|
|
3625
3625
|
static load(s) {
|
|
3626
3626
|
const idx = new _ChurnIndex();
|
|
3627
|
-
for (const [
|
|
3628
|
-
idx.nodes.set(
|
|
3627
|
+
for (const [path37, raw] of Object.entries(s.nodes)) {
|
|
3628
|
+
idx.nodes.set(path37, {
|
|
3629
3629
|
commits: raw.commits,
|
|
3630
3630
|
churnLines: raw.churnLines,
|
|
3631
3631
|
bugCommits: raw.bugCommits,
|
|
@@ -3638,8 +3638,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3638
3638
|
// -------------------------------------------------------------------------
|
|
3639
3639
|
// Private helpers
|
|
3640
3640
|
// -------------------------------------------------------------------------
|
|
3641
|
-
getOrCreate(
|
|
3642
|
-
const existing = this.nodes.get(
|
|
3641
|
+
getOrCreate(path37) {
|
|
3642
|
+
const existing = this.nodes.get(path37);
|
|
3643
3643
|
if (existing !== void 0) return existing;
|
|
3644
3644
|
const fresh = {
|
|
3645
3645
|
commits: 0,
|
|
@@ -3648,7 +3648,7 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3648
3648
|
authorCounts: {},
|
|
3649
3649
|
lastTouch: 0
|
|
3650
3650
|
};
|
|
3651
|
-
this.nodes.set(
|
|
3651
|
+
this.nodes.set(path37, fresh);
|
|
3652
3652
|
return fresh;
|
|
3653
3653
|
}
|
|
3654
3654
|
};
|
|
@@ -3729,12 +3729,12 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3729
3729
|
*/
|
|
3730
3730
|
snapshot() {
|
|
3731
3731
|
const nodes = {};
|
|
3732
|
-
for (const [
|
|
3732
|
+
for (const [path37, raw] of this.nodes) {
|
|
3733
3733
|
const authorWeights = {};
|
|
3734
3734
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3735
3735
|
authorWeights[email] = { ...entry };
|
|
3736
3736
|
}
|
|
3737
|
-
nodes[
|
|
3737
|
+
nodes[path37] = { authorWeights, lastTouch: raw.lastTouch };
|
|
3738
3738
|
}
|
|
3739
3739
|
return { version: 1, nodes };
|
|
3740
3740
|
}
|
|
@@ -3743,23 +3743,23 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3743
3743
|
*/
|
|
3744
3744
|
static load(s) {
|
|
3745
3745
|
const idx = new _OwnershipIndex();
|
|
3746
|
-
for (const [
|
|
3746
|
+
for (const [path37, raw] of Object.entries(s.nodes)) {
|
|
3747
3747
|
const authorWeights = {};
|
|
3748
3748
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3749
3749
|
authorWeights[email] = { ...entry };
|
|
3750
3750
|
}
|
|
3751
|
-
idx.nodes.set(
|
|
3751
|
+
idx.nodes.set(path37, { authorWeights, lastTouch: raw.lastTouch });
|
|
3752
3752
|
}
|
|
3753
3753
|
return idx;
|
|
3754
3754
|
}
|
|
3755
3755
|
// -------------------------------------------------------------------------
|
|
3756
3756
|
// Private helpers
|
|
3757
3757
|
// -------------------------------------------------------------------------
|
|
3758
|
-
getOrCreate(
|
|
3759
|
-
const existing = this.nodes.get(
|
|
3758
|
+
getOrCreate(path37) {
|
|
3759
|
+
const existing = this.nodes.get(path37);
|
|
3760
3760
|
if (existing !== void 0) return existing;
|
|
3761
3761
|
const fresh = { authorWeights: {}, lastTouch: 0 };
|
|
3762
|
-
this.nodes.set(
|
|
3762
|
+
this.nodes.set(path37, fresh);
|
|
3763
3763
|
return fresh;
|
|
3764
3764
|
}
|
|
3765
3765
|
};
|
|
@@ -4565,7 +4565,7 @@ function renderStatusXml(input) {
|
|
|
4565
4565
|
return lines.join("\n");
|
|
4566
4566
|
}
|
|
4567
4567
|
function registerStatusTool(registry, ctx) {
|
|
4568
|
-
const
|
|
4568
|
+
const Schema31 = z2.object({ project_root: ProjectRootField });
|
|
4569
4569
|
registry.register(
|
|
4570
4570
|
"ctx_status",
|
|
4571
4571
|
{
|
|
@@ -4579,7 +4579,7 @@ function registerStatusTool(registry, ctx) {
|
|
|
4579
4579
|
}
|
|
4580
4580
|
},
|
|
4581
4581
|
async (args) => {
|
|
4582
|
-
const { project_root } =
|
|
4582
|
+
const { project_root } = Schema31.parse(args ?? {});
|
|
4583
4583
|
void project_root;
|
|
4584
4584
|
return renderStatusXml({
|
|
4585
4585
|
defaultRoot: ctx.noDefaultMode ? null : ctx.projectRoot,
|
|
@@ -4614,6 +4614,325 @@ var ToolRegistry = class {
|
|
|
4614
4614
|
// packages/core/src/tools/search.ts
|
|
4615
4615
|
import { z as z3 } from "zod";
|
|
4616
4616
|
|
|
4617
|
+
// packages/core/src/budget/nextToolSuggestions.ts
|
|
4618
|
+
var STATIC_RULES = {
|
|
4619
|
+
// ─── Source-returning / file-shaped tools ────────────────────────
|
|
4620
|
+
ctx_get_file: [
|
|
4621
|
+
{
|
|
4622
|
+
tool: "ctx_get_call_graph",
|
|
4623
|
+
why: "Check who depends on this file before modifying.",
|
|
4624
|
+
estimated_tokens: 800
|
|
4625
|
+
},
|
|
4626
|
+
{
|
|
4627
|
+
tool: "ctx_get_definition",
|
|
4628
|
+
why: "Cheaper view if you need a specific symbol, not the whole file.",
|
|
4629
|
+
estimated_tokens: 2e3
|
|
4630
|
+
},
|
|
4631
|
+
{
|
|
4632
|
+
tool: "ctx_blast_radius",
|
|
4633
|
+
why: "Transitive impact analysis before a write.",
|
|
4634
|
+
estimated_tokens: 1500
|
|
4635
|
+
}
|
|
4636
|
+
],
|
|
4637
|
+
ctx_get_definition: [
|
|
4638
|
+
{
|
|
4639
|
+
tool: "ctx_get_call_graph",
|
|
4640
|
+
why: "Who calls this symbol? Almost always your next step.",
|
|
4641
|
+
estimated_tokens: 800
|
|
4642
|
+
},
|
|
4643
|
+
{
|
|
4644
|
+
tool: "ctx_blast_radius",
|
|
4645
|
+
why: "What would break if this signature changes?",
|
|
4646
|
+
estimated_tokens: 1500
|
|
4647
|
+
}
|
|
4648
|
+
],
|
|
4649
|
+
ctx_get_context_packet: [
|
|
4650
|
+
{
|
|
4651
|
+
tool: "ctx_get_call_graph",
|
|
4652
|
+
why: "Surface external callers not visible inside the packet.",
|
|
4653
|
+
estimated_tokens: 800
|
|
4654
|
+
},
|
|
4655
|
+
{
|
|
4656
|
+
tool: "ctx_get_affected_flows",
|
|
4657
|
+
why: "Execution-flow coverage of the packet files.",
|
|
4658
|
+
estimated_tokens: 2e3
|
|
4659
|
+
}
|
|
4660
|
+
],
|
|
4661
|
+
ctx_search: [
|
|
4662
|
+
{
|
|
4663
|
+
tool: "ctx_get_definition",
|
|
4664
|
+
why: "Pull the canonical definition of a top result.",
|
|
4665
|
+
estimated_tokens: 2e3
|
|
4666
|
+
},
|
|
4667
|
+
{
|
|
4668
|
+
tool: "ctx_similar_files",
|
|
4669
|
+
why: "Find related files outside the keyword/vector hit set.",
|
|
4670
|
+
estimated_tokens: 1e3
|
|
4671
|
+
}
|
|
4672
|
+
],
|
|
4673
|
+
ctx_full_text_search: [
|
|
4674
|
+
{
|
|
4675
|
+
tool: "ctx_get_file",
|
|
4676
|
+
why: "Inspect a specific match in context.",
|
|
4677
|
+
estimated_tokens: 8e3
|
|
4678
|
+
},
|
|
4679
|
+
{
|
|
4680
|
+
tool: "ctx_get_call_graph",
|
|
4681
|
+
why: "Caller graph for matched symbols.",
|
|
4682
|
+
estimated_tokens: 800
|
|
4683
|
+
}
|
|
4684
|
+
],
|
|
4685
|
+
ctx_similar_files: [
|
|
4686
|
+
{
|
|
4687
|
+
tool: "ctx_get_context_packet",
|
|
4688
|
+
why: "Bundle the cluster into a single packet for review.",
|
|
4689
|
+
estimated_tokens: 6e3
|
|
4690
|
+
}
|
|
4691
|
+
],
|
|
4692
|
+
// ─── Graph / structural queries ──────────────────────────────────
|
|
4693
|
+
// ctx_get_call_graph is the canonical "find callers" tool — pass
|
|
4694
|
+
// direction: 'callers' or 'callees' in args. The follow-ups below
|
|
4695
|
+
// assume the caller is investigating impact / coverage on the
|
|
4696
|
+
// returned caller set.
|
|
4697
|
+
ctx_get_call_graph: [
|
|
4698
|
+
{
|
|
4699
|
+
tool: "ctx_blast_radius",
|
|
4700
|
+
why: "Transitive dependents \u2014 callers of the callers you just found.",
|
|
4701
|
+
estimated_tokens: 1500
|
|
4702
|
+
},
|
|
4703
|
+
{
|
|
4704
|
+
tool: "ctx_get_affected_flows",
|
|
4705
|
+
why: "Which execution paths break if any caller is removed?",
|
|
4706
|
+
estimated_tokens: 2e3
|
|
4707
|
+
},
|
|
4708
|
+
{
|
|
4709
|
+
tool: "ctx_execution_flow",
|
|
4710
|
+
why: "Linearize the caller set into ordered execution sequences.",
|
|
4711
|
+
estimated_tokens: 4e3
|
|
4712
|
+
}
|
|
4713
|
+
],
|
|
4714
|
+
ctx_blast_radius: [
|
|
4715
|
+
{
|
|
4716
|
+
tool: "ctx_get_affected_flows",
|
|
4717
|
+
why: "Execution-flow impact on the affected files.",
|
|
4718
|
+
estimated_tokens: 2e3
|
|
4719
|
+
},
|
|
4720
|
+
{ tool: "ctx_knowledge_gaps", why: "Identify test-coverage gaps on affected files.", estimated_tokens: 1200 }
|
|
4721
|
+
],
|
|
4722
|
+
ctx_get_affected_flows: [
|
|
4723
|
+
{
|
|
4724
|
+
tool: "ctx_execution_flow",
|
|
4725
|
+
why: "Drill into a specific affected flow.",
|
|
4726
|
+
estimated_tokens: 4e3
|
|
4727
|
+
},
|
|
4728
|
+
{
|
|
4729
|
+
tool: "ctx_blast_radius",
|
|
4730
|
+
why: "Reverse direction \u2014 what affects this flow?",
|
|
4731
|
+
estimated_tokens: 1500
|
|
4732
|
+
}
|
|
4733
|
+
],
|
|
4734
|
+
ctx_execution_flow: [
|
|
4735
|
+
{
|
|
4736
|
+
tool: "ctx_get_call_graph",
|
|
4737
|
+
why: "External callers of the flow entry-point.",
|
|
4738
|
+
estimated_tokens: 800
|
|
4739
|
+
}
|
|
4740
|
+
],
|
|
4741
|
+
// ─── Architecture / overview tools ───────────────────────────────
|
|
4742
|
+
ctx_architecture_overview: [
|
|
4743
|
+
{
|
|
4744
|
+
tool: "ctx_community_list",
|
|
4745
|
+
why: "Drill into a specific community.",
|
|
4746
|
+
estimated_tokens: 1e3
|
|
4747
|
+
},
|
|
4748
|
+
{
|
|
4749
|
+
tool: "ctx_hub_nodes",
|
|
4750
|
+
why: "Top fan-in/out nodes deserving deeper inspection.",
|
|
4751
|
+
estimated_tokens: 1200
|
|
4752
|
+
},
|
|
4753
|
+
{
|
|
4754
|
+
tool: "ctx_bridge_nodes",
|
|
4755
|
+
why: "Cross-community bridges (high architectural leverage).",
|
|
4756
|
+
estimated_tokens: 1e3
|
|
4757
|
+
}
|
|
4758
|
+
],
|
|
4759
|
+
ctx_community_list: [
|
|
4760
|
+
{
|
|
4761
|
+
tool: "ctx_get_context_packet",
|
|
4762
|
+
why: "Bundle a community into a single review packet.",
|
|
4763
|
+
estimated_tokens: 6e3
|
|
4764
|
+
}
|
|
4765
|
+
],
|
|
4766
|
+
ctx_hub_nodes: [
|
|
4767
|
+
{
|
|
4768
|
+
tool: "ctx_get_call_graph",
|
|
4769
|
+
why: "Who depends on the top hub?",
|
|
4770
|
+
estimated_tokens: 800
|
|
4771
|
+
},
|
|
4772
|
+
{
|
|
4773
|
+
tool: "ctx_blast_radius",
|
|
4774
|
+
why: "Hub change-impact analysis.",
|
|
4775
|
+
estimated_tokens: 1500
|
|
4776
|
+
}
|
|
4777
|
+
],
|
|
4778
|
+
ctx_bridge_nodes: [
|
|
4779
|
+
{
|
|
4780
|
+
tool: "ctx_get_call_graph",
|
|
4781
|
+
why: "Callers across the bridge.",
|
|
4782
|
+
estimated_tokens: 800
|
|
4783
|
+
}
|
|
4784
|
+
],
|
|
4785
|
+
ctx_surprising_connections: [
|
|
4786
|
+
{
|
|
4787
|
+
tool: "ctx_blast_radius",
|
|
4788
|
+
why: "Impact analysis on a surprising-connection target.",
|
|
4789
|
+
estimated_tokens: 1500
|
|
4790
|
+
}
|
|
4791
|
+
],
|
|
4792
|
+
// ─── Review / diff tools ─────────────────────────────────────────
|
|
4793
|
+
ctx_detect_changes: [
|
|
4794
|
+
{
|
|
4795
|
+
tool: "ctx_get_file",
|
|
4796
|
+
why: "Inspect a specific risky file.",
|
|
4797
|
+
estimated_tokens: 8e3
|
|
4798
|
+
},
|
|
4799
|
+
{
|
|
4800
|
+
tool: "ctx_get_affected_flows",
|
|
4801
|
+
why: "Which execution paths the change touches.",
|
|
4802
|
+
estimated_tokens: 2e3
|
|
4803
|
+
},
|
|
4804
|
+
{
|
|
4805
|
+
tool: "ctx_git_diff_review",
|
|
4806
|
+
why: "Full diff packet for the changeset.",
|
|
4807
|
+
estimated_tokens: 8e3
|
|
4808
|
+
}
|
|
4809
|
+
],
|
|
4810
|
+
ctx_git_diff_review: [
|
|
4811
|
+
{
|
|
4812
|
+
tool: "ctx_risk_overlay",
|
|
4813
|
+
why: "Score the changed files by historical churn + coupling.",
|
|
4814
|
+
estimated_tokens: 1500
|
|
4815
|
+
},
|
|
4816
|
+
{
|
|
4817
|
+
tool: "ctx_get_call_graph",
|
|
4818
|
+
why: "Caller-side impact of the changes.",
|
|
4819
|
+
estimated_tokens: 800
|
|
4820
|
+
}
|
|
4821
|
+
],
|
|
4822
|
+
ctx_risk_overlay: [
|
|
4823
|
+
{
|
|
4824
|
+
tool: "ctx_get_file",
|
|
4825
|
+
why: "Inspect the highest-risk file in detail.",
|
|
4826
|
+
estimated_tokens: 8e3
|
|
4827
|
+
}
|
|
4828
|
+
],
|
|
4829
|
+
ctx_git_coupling: [
|
|
4830
|
+
{
|
|
4831
|
+
tool: "ctx_blast_radius",
|
|
4832
|
+
why: "Static impact analysis to complement co-change signal.",
|
|
4833
|
+
estimated_tokens: 1500
|
|
4834
|
+
}
|
|
4835
|
+
],
|
|
4836
|
+
// ─── Refactor tools ──────────────────────────────────────────────
|
|
4837
|
+
ctx_refactor_preview: [
|
|
4838
|
+
{
|
|
4839
|
+
tool: "ctx_apply_refactor",
|
|
4840
|
+
why: "Commit the preview after you reviewed the rename.",
|
|
4841
|
+
estimated_tokens: 2e3
|
|
4842
|
+
}
|
|
4843
|
+
],
|
|
4844
|
+
ctx_apply_refactor: [
|
|
4845
|
+
{
|
|
4846
|
+
tool: "ctx_detect_changes",
|
|
4847
|
+
why: "Verify the refactor produced the expected risk profile.",
|
|
4848
|
+
estimated_tokens: 1500
|
|
4849
|
+
}
|
|
4850
|
+
],
|
|
4851
|
+
// ─── Knowledge / coverage tools ──────────────────────────────────
|
|
4852
|
+
ctx_knowledge_gaps: [
|
|
4853
|
+
{ tool: "ctx_knowledge_gaps", why: "Identify test-coverage gaps on affected files.", estimated_tokens: 1200 },
|
|
4854
|
+
{
|
|
4855
|
+
tool: "ctx_get_call_graph",
|
|
4856
|
+
why: "Caller frequency on untested symbols (impact ranking).",
|
|
4857
|
+
estimated_tokens: 800
|
|
4858
|
+
}
|
|
4859
|
+
],
|
|
4860
|
+
ctx_find_large_functions: [
|
|
4861
|
+
{
|
|
4862
|
+
tool: "ctx_get_definition",
|
|
4863
|
+
why: "Inspect the largest function.",
|
|
4864
|
+
estimated_tokens: 2e3
|
|
4865
|
+
}
|
|
4866
|
+
],
|
|
4867
|
+
// ─── Metadata / status ───────────────────────────────────────────
|
|
4868
|
+
ctx_get_minimal_context: [
|
|
4869
|
+
// intentionally empty — the suggested_first_tool field IS the
|
|
4870
|
+
// next-step suggestion. Adding rules here would be redundant.
|
|
4871
|
+
],
|
|
4872
|
+
ctx_status: [],
|
|
4873
|
+
ctx_get_rules: [
|
|
4874
|
+
{
|
|
4875
|
+
tool: "ctx_rules_check",
|
|
4876
|
+
why: "Validate code against rules.",
|
|
4877
|
+
estimated_tokens: 1200
|
|
4878
|
+
}
|
|
4879
|
+
],
|
|
4880
|
+
ctx_rules_check: [],
|
|
4881
|
+
ctx_get_workflow: [],
|
|
4882
|
+
ctx_suggested_questions: [],
|
|
4883
|
+
// ─── Wiki / export ───────────────────────────────────────────────
|
|
4884
|
+
ctx_wiki_generate: [
|
|
4885
|
+
{
|
|
4886
|
+
tool: "ctx_architecture_overview",
|
|
4887
|
+
why: "Confirm the wiki structure against the live overview.",
|
|
4888
|
+
estimated_tokens: 2e3
|
|
4889
|
+
}
|
|
4890
|
+
],
|
|
4891
|
+
ctx_graph_export: [],
|
|
4892
|
+
ctx_graph_snapshot: [
|
|
4893
|
+
{
|
|
4894
|
+
tool: "ctx_graph_diff",
|
|
4895
|
+
why: "Compare against a later snapshot.",
|
|
4896
|
+
estimated_tokens: 2e3
|
|
4897
|
+
}
|
|
4898
|
+
],
|
|
4899
|
+
ctx_graph_diff: [
|
|
4900
|
+
{
|
|
4901
|
+
tool: "ctx_detect_changes",
|
|
4902
|
+
why: "Risk-scored view of the graph delta.",
|
|
4903
|
+
estimated_tokens: 1500
|
|
4904
|
+
}
|
|
4905
|
+
],
|
|
4906
|
+
// ─── Cross-repo ──────────────────────────────────────────────────
|
|
4907
|
+
ctx_cross_repo_search: [
|
|
4908
|
+
{
|
|
4909
|
+
tool: "ctx_get_file",
|
|
4910
|
+
why: "Inspect a hit in a specific repo.",
|
|
4911
|
+
estimated_tokens: 8e3
|
|
4912
|
+
}
|
|
4913
|
+
]
|
|
4914
|
+
// ─── Query primitive ─────────────────────────────────────────────
|
|
4915
|
+
// ─── Affected flows (sibling to get_affected_flows but worth pinning) ─
|
|
4916
|
+
// ─── Definition aliases / structurals not listed above are deliberate
|
|
4917
|
+
// — the test enforces the full registered-tool list is covered.
|
|
4918
|
+
};
|
|
4919
|
+
var TOKEN_ESTIMATE_MIN = 0;
|
|
4920
|
+
var TOKEN_ESTIMATE_MAX = 1e5;
|
|
4921
|
+
function clampEstimate(n) {
|
|
4922
|
+
if (!Number.isFinite(n)) return 0;
|
|
4923
|
+
return Math.max(TOKEN_ESTIMATE_MIN, Math.min(TOKEN_ESTIMATE_MAX, Math.round(n)));
|
|
4924
|
+
}
|
|
4925
|
+
function suggestNext(fromTool, registeredTools) {
|
|
4926
|
+
const raw = STATIC_RULES[fromTool] ?? [];
|
|
4927
|
+
const filtered = registeredTools ? raw.filter((s) => registeredTools.has(s.tool)) : raw;
|
|
4928
|
+
return filtered.slice(0, 3).map((s) => ({
|
|
4929
|
+
tool: s.tool,
|
|
4930
|
+
args: s.args,
|
|
4931
|
+
why: s.why,
|
|
4932
|
+
estimated_tokens: clampEstimate(s.estimated_tokens)
|
|
4933
|
+
}));
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4617
4936
|
// packages/core/src/budget/budget.ts
|
|
4618
4937
|
var defaultTokenEstimator = (text) => Math.ceil(text.length / 4);
|
|
4619
4938
|
function hasBudgetArgs(args) {
|
|
@@ -4649,16 +4968,21 @@ async function enforceBudget(opts) {
|
|
|
4649
4968
|
const { full, args, toolName, defaultMaxTokens, skeletonProducer } = opts;
|
|
4650
4969
|
const estimate = opts.estimator ?? defaultTokenEstimator;
|
|
4651
4970
|
const sink = opts.sink ?? opts.ctx?.telemetrySink ?? diskSink;
|
|
4971
|
+
const nextToolSuggestions = suggestNext(toolName);
|
|
4972
|
+
const withSuggestions = (base) => {
|
|
4973
|
+
if (nextToolSuggestions.length === 0) return base;
|
|
4974
|
+
return { ...base, next_tool_suggestions: nextToolSuggestions };
|
|
4975
|
+
};
|
|
4652
4976
|
const originalTokens = estimate(full);
|
|
4653
4977
|
if (isBudgetDisabled()) {
|
|
4654
4978
|
return {
|
|
4655
4979
|
text: full,
|
|
4656
|
-
meta: {
|
|
4980
|
+
meta: withSuggestions({
|
|
4657
4981
|
format: "full",
|
|
4658
4982
|
original_tokens_est: originalTokens,
|
|
4659
4983
|
returned_tokens_est: originalTokens,
|
|
4660
4984
|
fallback_reason: null
|
|
4661
|
-
}
|
|
4985
|
+
})
|
|
4662
4986
|
};
|
|
4663
4987
|
}
|
|
4664
4988
|
if (args.response_format === "skeleton" && skeletonProducer) {
|
|
@@ -4667,45 +4991,45 @@ async function enforceBudget(opts) {
|
|
|
4667
4991
|
const skTokens = estimate(skeleton2);
|
|
4668
4992
|
return {
|
|
4669
4993
|
text: skeleton2,
|
|
4670
|
-
meta: {
|
|
4994
|
+
meta: withSuggestions({
|
|
4671
4995
|
format: "skeleton",
|
|
4672
4996
|
original_tokens_est: originalTokens,
|
|
4673
4997
|
returned_tokens_est: skTokens,
|
|
4674
4998
|
fallback_reason: null
|
|
4675
|
-
}
|
|
4999
|
+
})
|
|
4676
5000
|
};
|
|
4677
5001
|
}
|
|
4678
5002
|
return {
|
|
4679
5003
|
text: full,
|
|
4680
|
-
meta: {
|
|
5004
|
+
meta: withSuggestions({
|
|
4681
5005
|
format: "full",
|
|
4682
5006
|
original_tokens_est: originalTokens,
|
|
4683
5007
|
returned_tokens_est: originalTokens,
|
|
4684
5008
|
fallback_reason: "skeleton_failed"
|
|
4685
|
-
}
|
|
5009
|
+
})
|
|
4686
5010
|
};
|
|
4687
5011
|
}
|
|
4688
5012
|
const budget = args.max_response_tokens ?? defaultMaxTokens;
|
|
4689
5013
|
if (budget === void 0) {
|
|
4690
5014
|
return {
|
|
4691
5015
|
text: full,
|
|
4692
|
-
meta: {
|
|
5016
|
+
meta: withSuggestions({
|
|
4693
5017
|
format: "full",
|
|
4694
5018
|
original_tokens_est: originalTokens,
|
|
4695
5019
|
returned_tokens_est: originalTokens,
|
|
4696
5020
|
fallback_reason: null
|
|
4697
|
-
}
|
|
5021
|
+
})
|
|
4698
5022
|
};
|
|
4699
5023
|
}
|
|
4700
5024
|
if (originalTokens <= budget) {
|
|
4701
5025
|
return {
|
|
4702
5026
|
text: full,
|
|
4703
|
-
meta: {
|
|
5027
|
+
meta: withSuggestions({
|
|
4704
5028
|
format: "full",
|
|
4705
5029
|
original_tokens_est: originalTokens,
|
|
4706
5030
|
returned_tokens_est: originalTokens,
|
|
4707
5031
|
fallback_reason: null
|
|
4708
|
-
}
|
|
5032
|
+
})
|
|
4709
5033
|
};
|
|
4710
5034
|
}
|
|
4711
5035
|
emitTelemetry({
|
|
@@ -4736,12 +5060,12 @@ async function enforceBudget(opts) {
|
|
|
4736
5060
|
}, sink);
|
|
4737
5061
|
return {
|
|
4738
5062
|
text: sliced2,
|
|
4739
|
-
meta: {
|
|
5063
|
+
meta: withSuggestions({
|
|
4740
5064
|
format: "truncated",
|
|
4741
5065
|
original_tokens_est: originalTokens,
|
|
4742
5066
|
returned_tokens_est: slicedTokens,
|
|
4743
5067
|
fallback_reason: "budget_exceeded"
|
|
4744
|
-
}
|
|
5068
|
+
})
|
|
4745
5069
|
};
|
|
4746
5070
|
}
|
|
4747
5071
|
const skeleton = skeletonProducer ? await safeSkeleton(skeletonProducer, toolName) : null;
|
|
@@ -4756,12 +5080,12 @@ async function enforceBudget(opts) {
|
|
|
4756
5080
|
}, sink);
|
|
4757
5081
|
return {
|
|
4758
5082
|
text: skeleton,
|
|
4759
|
-
meta: {
|
|
5083
|
+
meta: withSuggestions({
|
|
4760
5084
|
format: "skeleton",
|
|
4761
5085
|
original_tokens_est: originalTokens,
|
|
4762
5086
|
returned_tokens_est: skTokens,
|
|
4763
5087
|
fallback_reason: "budget_exceeded"
|
|
4764
|
-
}
|
|
5088
|
+
})
|
|
4765
5089
|
};
|
|
4766
5090
|
}
|
|
4767
5091
|
const slicedSk = skeleton.slice(0, budget * 4);
|
|
@@ -4773,12 +5097,12 @@ async function enforceBudget(opts) {
|
|
|
4773
5097
|
}, sink);
|
|
4774
5098
|
return {
|
|
4775
5099
|
text: slicedSk,
|
|
4776
|
-
meta: {
|
|
5100
|
+
meta: withSuggestions({
|
|
4777
5101
|
format: "truncated",
|
|
4778
5102
|
original_tokens_est: originalTokens,
|
|
4779
5103
|
returned_tokens_est: estimate(slicedSk),
|
|
4780
5104
|
fallback_reason: "budget_exceeded"
|
|
4781
|
-
}
|
|
5105
|
+
})
|
|
4782
5106
|
};
|
|
4783
5107
|
}
|
|
4784
5108
|
const sliced = full.slice(0, budget * 4);
|
|
@@ -4790,12 +5114,12 @@ async function enforceBudget(opts) {
|
|
|
4790
5114
|
}, sink);
|
|
4791
5115
|
return {
|
|
4792
5116
|
text: sliced,
|
|
4793
|
-
meta: {
|
|
5117
|
+
meta: withSuggestions({
|
|
4794
5118
|
format: "truncated",
|
|
4795
5119
|
original_tokens_est: originalTokens,
|
|
4796
5120
|
returned_tokens_est: estimate(sliced),
|
|
4797
5121
|
fallback_reason: "skeleton_failed"
|
|
4798
|
-
}
|
|
5122
|
+
})
|
|
4799
5123
|
};
|
|
4800
5124
|
}
|
|
4801
5125
|
async function safeSkeleton(producer, toolName) {
|
|
@@ -6454,7 +6778,7 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6454
6778
|
skeleton: include_skeletons && i < skeletonLimit ? await trySkeletonize(ctx, file, project_root) : ""
|
|
6455
6779
|
}))
|
|
6456
6780
|
);
|
|
6457
|
-
const
|
|
6781
|
+
const render2 = (withSkeletons, withTransitive) => {
|
|
6458
6782
|
const out = [`<git_diff_review changed_files="${files.length}" depth="${depth}">`];
|
|
6459
6783
|
out.push(` <changed_files count="${files.length}">`);
|
|
6460
6784
|
for (const cd of changedFileData) {
|
|
@@ -6500,8 +6824,8 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
6500
6824
|
out.push("</git_diff_review>");
|
|
6501
6825
|
return out.join("\n");
|
|
6502
6826
|
};
|
|
6503
|
-
const full =
|
|
6504
|
-
return maybeBudget(full, async () =>
|
|
6827
|
+
const full = render2(include_skeletons, true);
|
|
6828
|
+
return maybeBudget(full, async () => render2(false, false));
|
|
6505
6829
|
}
|
|
6506
6830
|
);
|
|
6507
6831
|
}
|
|
@@ -6603,7 +6927,7 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
6603
6927
|
totalOccurrences += occurrences.length;
|
|
6604
6928
|
}
|
|
6605
6929
|
}
|
|
6606
|
-
const
|
|
6930
|
+
const render2 = (includeChanges) => {
|
|
6607
6931
|
const xmlLines = [
|
|
6608
6932
|
`<refactor_preview symbol="${escapeXML17(symbol)}" new_name="${escapeXML17(new_name)}" total_files="${fileChanges.length}" total_occurrences="${totalOccurrences}">`
|
|
6609
6933
|
];
|
|
@@ -6633,7 +6957,7 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
6633
6957
|
xmlLines.push("</refactor_preview>");
|
|
6634
6958
|
return xmlLines.join("\n");
|
|
6635
6959
|
};
|
|
6636
|
-
const full =
|
|
6960
|
+
const full = render2(true);
|
|
6637
6961
|
if (!hasBudgetArgs(args)) return full;
|
|
6638
6962
|
const result = await enforceBudget({
|
|
6639
6963
|
ctx,
|
|
@@ -6641,7 +6965,7 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
6641
6965
|
args: readBudgetArgs(args),
|
|
6642
6966
|
toolName: "ctx_refactor_preview",
|
|
6643
6967
|
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS7,
|
|
6644
|
-
skeletonProducer: async () =>
|
|
6968
|
+
skeletonProducer: async () => render2(false)
|
|
6645
6969
|
});
|
|
6646
6970
|
return wrapResponse(result);
|
|
6647
6971
|
}
|
|
@@ -7004,7 +7328,7 @@ function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
|
|
|
7004
7328
|
);
|
|
7005
7329
|
}
|
|
7006
7330
|
xmlLines.push(" </repos>");
|
|
7007
|
-
const
|
|
7331
|
+
const render2 = (includeContent) => {
|
|
7008
7332
|
const out = [...xmlLines];
|
|
7009
7333
|
out.push(` <results count="${topResults.length}">`);
|
|
7010
7334
|
for (const r of topResults) {
|
|
@@ -7020,7 +7344,7 @@ function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
|
|
|
7020
7344
|
out.push("</cross_repo_search>");
|
|
7021
7345
|
return out.join("\n");
|
|
7022
7346
|
};
|
|
7023
|
-
const full =
|
|
7347
|
+
const full = render2(true);
|
|
7024
7348
|
if (!hasBudgetArgs(args)) return full;
|
|
7025
7349
|
const result = await enforceBudget({
|
|
7026
7350
|
ctx,
|
|
@@ -7028,7 +7352,7 @@ function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
|
|
|
7028
7352
|
args: readBudgetArgs(args),
|
|
7029
7353
|
toolName: "ctx_cross_repo_search",
|
|
7030
7354
|
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS9,
|
|
7031
|
-
skeletonProducer: async () =>
|
|
7355
|
+
skeletonProducer: async () => render2(false)
|
|
7032
7356
|
});
|
|
7033
7357
|
return wrapResponse(result);
|
|
7034
7358
|
}
|
|
@@ -7415,7 +7739,7 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
7415
7739
|
} catch {
|
|
7416
7740
|
}
|
|
7417
7741
|
}
|
|
7418
|
-
const
|
|
7742
|
+
const render2 = (includeSnippets) => {
|
|
7419
7743
|
const xml = [
|
|
7420
7744
|
`<full_text_search query="${escapeXML22(query)}" mode="${mode}" case_sensitive="${case_sensitive}" count="${merged.length}">`
|
|
7421
7745
|
];
|
|
@@ -7434,8 +7758,8 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
7434
7758
|
return xml.join("\n");
|
|
7435
7759
|
};
|
|
7436
7760
|
return maybeBudget(
|
|
7437
|
-
|
|
7438
|
-
async () =>
|
|
7761
|
+
render2(true),
|
|
7762
|
+
async () => render2(false)
|
|
7439
7763
|
);
|
|
7440
7764
|
}
|
|
7441
7765
|
);
|
|
@@ -8468,6 +8792,279 @@ function registerGetAffectedFlowsTool(registry, ctx) {
|
|
|
8468
8792
|
);
|
|
8469
8793
|
}
|
|
8470
8794
|
|
|
8795
|
+
// packages/core/src/tools/minimal-context.ts
|
|
8796
|
+
import { execSync } from "child_process";
|
|
8797
|
+
import { z as z36 } from "zod";
|
|
8798
|
+
var Schema30 = z36.object({
|
|
8799
|
+
task: z36.string().max(200).optional().describe(
|
|
8800
|
+
"Free-text description of what you're about to do (e.g. 'review PR 142', 'rename emitTelemetry'). The tool routes by regex to the most-fitting suggested-first-tool. Capped at 200 chars; control characters stripped."
|
|
8801
|
+
),
|
|
8802
|
+
project_root: ProjectRootField,
|
|
8803
|
+
max_response_tokens: z36.number().int().positive().optional(),
|
|
8804
|
+
on_budget_exceeded: z36.enum(["skeleton", "truncate", "error"]).optional(),
|
|
8805
|
+
response_format: z36.enum(["full", "skeleton", "auto"]).optional()
|
|
8806
|
+
});
|
|
8807
|
+
var DEFAULT_MAX_RESPONSE_TOKENS13 = 250;
|
|
8808
|
+
function routeFirstTool(task, hasDirtyChanges) {
|
|
8809
|
+
const t = (task ?? "").toLowerCase();
|
|
8810
|
+
if (/\b(rename|refactor|move\s+\w+|extract)\b/.test(t)) {
|
|
8811
|
+
return {
|
|
8812
|
+
tool: "ctx_get_call_graph",
|
|
8813
|
+
why: "Renames + refactors need every caller surfaced before the edit. Start here.",
|
|
8814
|
+
estimated_tokens: 800
|
|
8815
|
+
};
|
|
8816
|
+
}
|
|
8817
|
+
if (/\b(blast|impact|breaks?|affects?)\b/.test(t)) {
|
|
8818
|
+
return {
|
|
8819
|
+
tool: "ctx_blast_radius",
|
|
8820
|
+
why: "Blast-radius analysis gives transitive dependents; start here for impact questions.",
|
|
8821
|
+
estimated_tokens: 1500
|
|
8822
|
+
};
|
|
8823
|
+
}
|
|
8824
|
+
if (/\b(architect|overview|explore|onboard|tour)\b/.test(t)) {
|
|
8825
|
+
return {
|
|
8826
|
+
tool: "ctx_architecture_overview",
|
|
8827
|
+
why: "Top-down map of communities + hub nodes; the natural starting point for exploration.",
|
|
8828
|
+
estimated_tokens: 2e3
|
|
8829
|
+
};
|
|
8830
|
+
}
|
|
8831
|
+
if (/\b(test|coverage|tested)\b/.test(t)) {
|
|
8832
|
+
return {
|
|
8833
|
+
tool: "ctx_knowledge_gaps",
|
|
8834
|
+
why: "Knowledge-gap report highlights files lacking test coverage.",
|
|
8835
|
+
estimated_tokens: 1200
|
|
8836
|
+
};
|
|
8837
|
+
}
|
|
8838
|
+
if (/\b(review|audit|check|diff)\b/.test(t)) {
|
|
8839
|
+
return {
|
|
8840
|
+
tool: "ctx_detect_changes",
|
|
8841
|
+
why: "Risk-scored change analysis is the canonical start for reviews.",
|
|
8842
|
+
estimated_tokens: 1500
|
|
8843
|
+
};
|
|
8844
|
+
}
|
|
8845
|
+
if (hasDirtyChanges) {
|
|
8846
|
+
return {
|
|
8847
|
+
tool: "ctx_detect_changes",
|
|
8848
|
+
why: "Working tree has uncommitted changes \u2014 review-mode is the most-likely intent.",
|
|
8849
|
+
estimated_tokens: 1500
|
|
8850
|
+
};
|
|
8851
|
+
}
|
|
8852
|
+
return {
|
|
8853
|
+
tool: "ctx_architecture_overview",
|
|
8854
|
+
why: "Clean working tree + no task hint \u2014 orientation is the safe default.",
|
|
8855
|
+
estimated_tokens: 2e3
|
|
8856
|
+
};
|
|
8857
|
+
}
|
|
8858
|
+
function classifyStaleness(lastBuildIso) {
|
|
8859
|
+
if (!lastBuildIso) return "unbuilt";
|
|
8860
|
+
const ms = Date.now() - new Date(lastBuildIso).getTime();
|
|
8861
|
+
if (!Number.isFinite(ms) || ms < 0) return "unbuilt";
|
|
8862
|
+
if (ms < 5 * 60 * 1e3) return "fresh";
|
|
8863
|
+
if (ms < 60 * 60 * 1e3) return "stale_minutes";
|
|
8864
|
+
return "stale_hours";
|
|
8865
|
+
}
|
|
8866
|
+
function readRecentChanges(projectRoot) {
|
|
8867
|
+
try {
|
|
8868
|
+
const stdout = execSync("git status --porcelain", {
|
|
8869
|
+
cwd: projectRoot,
|
|
8870
|
+
timeout: 2e3,
|
|
8871
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
8872
|
+
encoding: "utf-8"
|
|
8873
|
+
});
|
|
8874
|
+
const lines = stdout.split("\n").filter((l) => l.trim() !== "");
|
|
8875
|
+
return lines.slice(0, 20).map((line) => {
|
|
8876
|
+
const x = line[0];
|
|
8877
|
+
const y = line[1];
|
|
8878
|
+
const path37 = line.slice(3).trim();
|
|
8879
|
+
let status = "?";
|
|
8880
|
+
const xy = x === " " ? y : x;
|
|
8881
|
+
if (xy === "M" || xy === "A" || xy === "D" || xy === "R") status = xy;
|
|
8882
|
+
return { file: path37, status };
|
|
8883
|
+
});
|
|
8884
|
+
} catch {
|
|
8885
|
+
return [];
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
function computeTopHubs(graph) {
|
|
8889
|
+
const files = graph.allFiles();
|
|
8890
|
+
const scored = files.map((file) => {
|
|
8891
|
+
const inDeg = graph.getImporters(file).length;
|
|
8892
|
+
const outDeg = graph.getImports(file).length;
|
|
8893
|
+
return { file, inDeg, outDeg, total: inDeg + outDeg };
|
|
8894
|
+
}).filter((s) => s.total >= 2).sort((a, b) => b.total - a.total).slice(0, 5);
|
|
8895
|
+
return scored.map((s) => ({
|
|
8896
|
+
name: s.file,
|
|
8897
|
+
reason: s.inDeg > s.outDeg ? "fan_in" : s.outDeg > s.inDeg ? "fan_out" : "bridge"
|
|
8898
|
+
}));
|
|
8899
|
+
}
|
|
8900
|
+
var CACHE_TTL_MS = 1e4;
|
|
8901
|
+
var responseCache = /* @__PURE__ */ new Map();
|
|
8902
|
+
function cacheKey(projectRoot, task) {
|
|
8903
|
+
return `${projectRoot}|${task ?? ""}`;
|
|
8904
|
+
}
|
|
8905
|
+
function cacheGet(key) {
|
|
8906
|
+
const e = responseCache.get(key);
|
|
8907
|
+
if (!e) return null;
|
|
8908
|
+
if (e.expiresAt < Date.now()) {
|
|
8909
|
+
responseCache.delete(key);
|
|
8910
|
+
return null;
|
|
8911
|
+
}
|
|
8912
|
+
return e.body;
|
|
8913
|
+
}
|
|
8914
|
+
function cachePut(key, body) {
|
|
8915
|
+
responseCache.set(key, { expiresAt: Date.now() + CACHE_TTL_MS, body });
|
|
8916
|
+
}
|
|
8917
|
+
function sanitizeTask(raw) {
|
|
8918
|
+
if (raw == null) return void 0;
|
|
8919
|
+
const stripped = raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
8920
|
+
return stripped === "" ? void 0 : stripped;
|
|
8921
|
+
}
|
|
8922
|
+
function escapeXML26(text) {
|
|
8923
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8924
|
+
}
|
|
8925
|
+
function render(input) {
|
|
8926
|
+
const lines = ["<minimal_context>"];
|
|
8927
|
+
lines.push(
|
|
8928
|
+
` <graph ready="${input.graphReady}" nodes="${input.nodes}" edges="${input.edges}" last_build="${escapeXML26(input.lastBuildIso ?? "never")}" staleness="${input.staleness}" />`
|
|
8929
|
+
);
|
|
8930
|
+
if (input.languages.length > 0) {
|
|
8931
|
+
lines.push(` <languages>${input.languages.map(escapeXML26).join(", ")}</languages>`);
|
|
8932
|
+
}
|
|
8933
|
+
if (input.recentChanges.length > 0) {
|
|
8934
|
+
lines.push(` <recent_changes count="${input.recentChanges.length}">`);
|
|
8935
|
+
for (const c of input.recentChanges) {
|
|
8936
|
+
lines.push(` <change status="${c.status}" file="${escapeXML26(c.file)}" />`);
|
|
8937
|
+
}
|
|
8938
|
+
lines.push(" </recent_changes>");
|
|
8939
|
+
} else {
|
|
8940
|
+
lines.push(' <recent_changes count="0" />');
|
|
8941
|
+
}
|
|
8942
|
+
if (input.topHubs.length > 0) {
|
|
8943
|
+
lines.push(` <top_hubs count="${input.topHubs.length}">`);
|
|
8944
|
+
for (const h of input.topHubs) {
|
|
8945
|
+
lines.push(` <hub reason="${h.reason}" name="${escapeXML26(h.name)}" />`);
|
|
8946
|
+
}
|
|
8947
|
+
lines.push(" </top_hubs>");
|
|
8948
|
+
}
|
|
8949
|
+
lines.push(" <suggested_first_tool>");
|
|
8950
|
+
lines.push(` <tool>${escapeXML26(input.suggested.tool)}</tool>`);
|
|
8951
|
+
lines.push(` <why>${escapeXML26(input.suggested.why)}</why>`);
|
|
8952
|
+
lines.push(` <estimated_tokens>${input.suggested.estimated_tokens}</estimated_tokens>`);
|
|
8953
|
+
if (input.suggested.args && Object.keys(input.suggested.args).length > 0) {
|
|
8954
|
+
lines.push(` <args>${escapeXML26(JSON.stringify(input.suggested.args))}</args>`);
|
|
8955
|
+
}
|
|
8956
|
+
lines.push(" </suggested_first_tool>");
|
|
8957
|
+
lines.push("</minimal_context>");
|
|
8958
|
+
return lines.join("\n");
|
|
8959
|
+
}
|
|
8960
|
+
function renderSkeleton(input) {
|
|
8961
|
+
const lines = ['<minimal_context format="skeleton">'];
|
|
8962
|
+
lines.push(
|
|
8963
|
+
` <graph ready="${input.graphReady}" nodes="${input.nodes}" edges="${input.edges}" staleness="${input.staleness}" />`
|
|
8964
|
+
);
|
|
8965
|
+
lines.push(` <recent_changes count="${input.recentChanges.length}" />`);
|
|
8966
|
+
lines.push(` <top_hubs count="${input.topHubs.length}" />`);
|
|
8967
|
+
lines.push(" <suggested_first_tool>");
|
|
8968
|
+
lines.push(` <tool>${escapeXML26(input.suggested.tool)}</tool>`);
|
|
8969
|
+
lines.push(` <why>${escapeXML26(input.suggested.why)}</why>`);
|
|
8970
|
+
lines.push(` <estimated_tokens>${input.suggested.estimated_tokens}</estimated_tokens>`);
|
|
8971
|
+
lines.push(" </suggested_first_tool>");
|
|
8972
|
+
lines.push("</minimal_context>");
|
|
8973
|
+
return lines.join("\n");
|
|
8974
|
+
}
|
|
8975
|
+
function registerMinimalContextTool(registry, ctx) {
|
|
8976
|
+
registry.register(
|
|
8977
|
+
"ctx_get_minimal_context",
|
|
8978
|
+
{
|
|
8979
|
+
name: "ctx_get_minimal_context",
|
|
8980
|
+
description: 'Orientation anchor \u2014 call this FIRST in any workflow. Returns ~150 tokens covering graph readiness, recent working-tree changes, top hub nodes, and a task-aware suggested-first-tool. Cached for 10s. Pass `task` as a free-text intent description ("review PR 142", "rename X", "check coverage") and the tool routes to the most-fitting follow-up. The agent should call the suggested tool next rather than guessing.',
|
|
8981
|
+
inputSchema: {
|
|
8982
|
+
type: "object",
|
|
8983
|
+
properties: {
|
|
8984
|
+
task: {
|
|
8985
|
+
type: "string",
|
|
8986
|
+
description: "Free-text intent (max 200 chars). Routes the suggested-first-tool by regex. Keywords: review/audit, rename/refactor, blast/impact, architect/explore, test/coverage."
|
|
8987
|
+
},
|
|
8988
|
+
project_root: PROJECT_ROOT_JSON_SCHEMA,
|
|
8989
|
+
max_response_tokens: {
|
|
8990
|
+
type: "number",
|
|
8991
|
+
description: "Optional response token budget. Default 250."
|
|
8992
|
+
},
|
|
8993
|
+
on_budget_exceeded: {
|
|
8994
|
+
type: "string",
|
|
8995
|
+
enum: ["skeleton", "truncate", "error"]
|
|
8996
|
+
},
|
|
8997
|
+
response_format: {
|
|
8998
|
+
type: "string",
|
|
8999
|
+
enum: ["full", "skeleton", "auto"]
|
|
9000
|
+
}
|
|
9001
|
+
}
|
|
9002
|
+
}
|
|
9003
|
+
},
|
|
9004
|
+
async (args) => {
|
|
9005
|
+
const parsed = Schema30.parse(args);
|
|
9006
|
+
const task = sanitizeTask(parsed.task);
|
|
9007
|
+
const projectRoot = parsed.project_root ?? ctx.projectRoot;
|
|
9008
|
+
const key = cacheKey(projectRoot, task);
|
|
9009
|
+
const cached = cacheGet(key);
|
|
9010
|
+
if (cached) {
|
|
9011
|
+
return cached;
|
|
9012
|
+
}
|
|
9013
|
+
const graphReady = ctx.isGraphInitialized();
|
|
9014
|
+
let nodes = 0;
|
|
9015
|
+
let edges = 0;
|
|
9016
|
+
let lastBuildIso = null;
|
|
9017
|
+
const languages = [];
|
|
9018
|
+
let topHubs = [];
|
|
9019
|
+
if (graphReady) {
|
|
9020
|
+
try {
|
|
9021
|
+
const graph = await ctx.getGraph(projectRoot);
|
|
9022
|
+
nodes = graph.allFiles().length;
|
|
9023
|
+
edges = graph.allFiles().reduce((acc, f) => acc + graph.getImports(f).length, 0);
|
|
9024
|
+
const extensions = /* @__PURE__ */ new Set();
|
|
9025
|
+
for (const f of graph.allFiles()) {
|
|
9026
|
+
const ext = f.split(".").pop();
|
|
9027
|
+
if (ext && ext.length <= 4) extensions.add(ext);
|
|
9028
|
+
}
|
|
9029
|
+
languages.push(...Array.from(extensions).sort());
|
|
9030
|
+
topHubs = computeTopHubs(graph);
|
|
9031
|
+
} catch {
|
|
9032
|
+
}
|
|
9033
|
+
}
|
|
9034
|
+
const recentChanges = readRecentChanges(projectRoot);
|
|
9035
|
+
const suggested = routeFirstTool(task, recentChanges.length > 0);
|
|
9036
|
+
const staleness = classifyStaleness(lastBuildIso);
|
|
9037
|
+
const renderInput = {
|
|
9038
|
+
graphReady,
|
|
9039
|
+
nodes,
|
|
9040
|
+
edges,
|
|
9041
|
+
lastBuildIso,
|
|
9042
|
+
staleness,
|
|
9043
|
+
languages,
|
|
9044
|
+
recentChanges,
|
|
9045
|
+
topHubs,
|
|
9046
|
+
suggested
|
|
9047
|
+
};
|
|
9048
|
+
const full = render(renderInput);
|
|
9049
|
+
if (!hasBudgetArgs(parsed)) {
|
|
9050
|
+
cachePut(key, full);
|
|
9051
|
+
return full;
|
|
9052
|
+
}
|
|
9053
|
+
const result = await enforceBudget({
|
|
9054
|
+
ctx,
|
|
9055
|
+
full,
|
|
9056
|
+
args: readBudgetArgs(parsed),
|
|
9057
|
+
toolName: "ctx_get_minimal_context",
|
|
9058
|
+
defaultMaxTokens: DEFAULT_MAX_RESPONSE_TOKENS13,
|
|
9059
|
+
skeletonProducer: async () => renderSkeleton(renderInput)
|
|
9060
|
+
});
|
|
9061
|
+
const body = wrapResponse(result);
|
|
9062
|
+
cachePut(key, body);
|
|
9063
|
+
return body;
|
|
9064
|
+
}
|
|
9065
|
+
);
|
|
9066
|
+
}
|
|
9067
|
+
|
|
8471
9068
|
// packages/core/src/tools/index.ts
|
|
8472
9069
|
function createToolRegistry(ctx) {
|
|
8473
9070
|
const registry = new ToolRegistry();
|
|
@@ -8504,6 +9101,7 @@ function createToolRegistry(ctx) {
|
|
|
8504
9101
|
registerGitCouplingTool(registry, ctx);
|
|
8505
9102
|
registerRiskOverlayTool(registry, ctx);
|
|
8506
9103
|
registerGetAffectedFlowsTool(registry, ctx);
|
|
9104
|
+
registerMinimalContextTool(registry, ctx);
|
|
8507
9105
|
return registry;
|
|
8508
9106
|
}
|
|
8509
9107
|
|
|
@@ -9136,20 +9734,20 @@ import { readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync, existsSy
|
|
|
9136
9734
|
import path29 from "path";
|
|
9137
9735
|
|
|
9138
9736
|
// packages/core/src/license/types.ts
|
|
9139
|
-
import { z as
|
|
9737
|
+
import { z as z37 } from "zod";
|
|
9140
9738
|
var FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/;
|
|
9141
|
-
var LicenseFileSchema =
|
|
9142
|
-
schemaVersion:
|
|
9143
|
-
key:
|
|
9144
|
-
tier:
|
|
9145
|
-
status:
|
|
9146
|
-
fingerprint:
|
|
9147
|
-
seats:
|
|
9148
|
-
issuedAt:
|
|
9149
|
-
expiresAt:
|
|
9150
|
-
lastValidatedAt:
|
|
9151
|
-
licenseId:
|
|
9152
|
-
instanceId:
|
|
9739
|
+
var LicenseFileSchema = z37.object({
|
|
9740
|
+
schemaVersion: z37.literal(1),
|
|
9741
|
+
key: z37.string().min(1),
|
|
9742
|
+
tier: z37.enum(["pro", "team", "enterprise", "trial"]),
|
|
9743
|
+
status: z37.enum(["active", "trialing", "expired"]),
|
|
9744
|
+
fingerprint: z37.string().regex(FINGERPRINT_RE),
|
|
9745
|
+
seats: z37.number().int().positive(),
|
|
9746
|
+
issuedAt: z37.string().datetime(),
|
|
9747
|
+
expiresAt: z37.string().datetime(),
|
|
9748
|
+
lastValidatedAt: z37.string().datetime(),
|
|
9749
|
+
licenseId: z37.string().min(1),
|
|
9750
|
+
instanceId: z37.string().min(1)
|
|
9153
9751
|
});
|
|
9154
9752
|
|
|
9155
9753
|
// packages/core/src/license/LicenseStore.ts
|
|
@@ -9362,7 +9960,7 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
|
|
|
9362
9960
|
function getTelemetryLevel() {
|
|
9363
9961
|
return TELEMETRY_LEVEL;
|
|
9364
9962
|
}
|
|
9365
|
-
var CTXLOOM_VERSION = "1.
|
|
9963
|
+
var CTXLOOM_VERSION = "1.4.0".length > 0 ? "1.4.0" : "dev";
|
|
9366
9964
|
var POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
9367
9965
|
var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
|
|
9368
9966
|
var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
|
|
@@ -9986,6 +10584,732 @@ var EmittedOnceTracker = class {
|
|
|
9986
10584
|
}
|
|
9987
10585
|
};
|
|
9988
10586
|
|
|
10587
|
+
// packages/core/src/install/installer.ts
|
|
10588
|
+
import fs28 from "fs";
|
|
10589
|
+
import path36 from "path";
|
|
10590
|
+
|
|
10591
|
+
// packages/core/src/install/templates.ts
|
|
10592
|
+
var RULES_BLOCK_NAME = "CTXLOOM-RULES";
|
|
10593
|
+
var RULES_BLOCK_CONTENT = `## MCP Tools: ctxloom
|
|
10594
|
+
|
|
10595
|
+
**IMPORTANT: This project has a knowledge graph. ALWAYS use the
|
|
10596
|
+
ctxloom MCP tools BEFORE Grep/Glob/Read to explore the codebase.**
|
|
10597
|
+
The graph is faster, cheaper (fewer tokens), and gives you
|
|
10598
|
+
structural context (callers, dependents, test coverage) that file
|
|
10599
|
+
scanning cannot.
|
|
10600
|
+
|
|
10601
|
+
### Start every workflow with \`ctx_get_minimal_context\`
|
|
10602
|
+
|
|
10603
|
+
The first MCP call into ctxloom should always be
|
|
10604
|
+
\`ctx_get_minimal_context(task="<what you're about to do>")\`. It
|
|
10605
|
+
returns ~150 tokens of orientation plus a task-aware
|
|
10606
|
+
\`suggested_first_tool\` you should call next instead of guessing.
|
|
10607
|
+
|
|
10608
|
+
### When to use graph tools FIRST
|
|
10609
|
+
|
|
10610
|
+
- **Code review**: \`ctx_detect_changes\` + \`ctx_get_review_context\`
|
|
10611
|
+
instead of reading whole files
|
|
10612
|
+
- **Understanding impact**: \`ctx_blast_radius\` + \`ctx_get_affected_flows\`
|
|
10613
|
+
instead of manually tracing imports
|
|
10614
|
+
- **Refactor planning**: \`ctx_get_call_graph(direction: 'callers')\`
|
|
10615
|
+
+ \`ctx_refactor_preview\` before any rename
|
|
10616
|
+
- **Architecture questions**: \`ctx_architecture_overview\`,
|
|
10617
|
+
\`ctx_community_list\`, \`ctx_hub_nodes\`
|
|
10618
|
+
- **Finding code**: \`ctx_search\` or \`ctx_full_text_search\` instead
|
|
10619
|
+
of \`Grep\`
|
|
10620
|
+
|
|
10621
|
+
Fall back to Grep/Glob/Read **only** when the graph doesn't cover
|
|
10622
|
+
what you need.
|
|
10623
|
+
|
|
10624
|
+
### Follow the \`next_tool_suggestions\` in every response
|
|
10625
|
+
|
|
10626
|
+
Every budget-wrapped ctxloom response includes
|
|
10627
|
+
\`meta.next_tool_suggestions\` \u2014 author-curated follow-ups with
|
|
10628
|
+
\`why\` reasoning and \`estimated_tokens\` per entry. Pick from
|
|
10629
|
+
those instead of guessing.
|
|
10630
|
+
|
|
10631
|
+
### Token-budget protocol
|
|
10632
|
+
|
|
10633
|
+
- Target: \u22648 tool calls per task, \u22642000 total tokens of graph context
|
|
10634
|
+
- Pass \`max_response_tokens\` on calls that might return large
|
|
10635
|
+
responses; the budget surface returns skeletons instead of dumping
|
|
10636
|
+
- Use \`response_format: 'skeleton'\` when you know you only need
|
|
10637
|
+
signatures, not bodies
|
|
10638
|
+
|
|
10639
|
+
### Key tools at a glance
|
|
10640
|
+
|
|
10641
|
+
| Tool | Use when |
|
|
10642
|
+
|------|----------|
|
|
10643
|
+
| \`ctx_get_minimal_context\` | START HERE \u2014 orientation anchor |
|
|
10644
|
+
| \`ctx_detect_changes\` | Reviewing code changes; risk-scored |
|
|
10645
|
+
| \`ctx_get_review_context\` | Token-efficient review snippets |
|
|
10646
|
+
| \`ctx_blast_radius\` | Blast radius of a change |
|
|
10647
|
+
| \`ctx_get_affected_flows\` | Execution paths impacted |
|
|
10648
|
+
| \`ctx_get_call_graph\` | Callers / callees of a symbol |
|
|
10649
|
+
| \`ctx_search\` / \`ctx_full_text_search\` | Find code |
|
|
10650
|
+
| \`ctx_architecture_overview\` | High-level codebase map |
|
|
10651
|
+
| \`ctx_refactor_preview\` / \`ctx_apply_refactor\` | Plan a rename |
|
|
10652
|
+
|
|
10653
|
+
### Hooks keep the graph fresh
|
|
10654
|
+
|
|
10655
|
+
\`ctxloom init\` installed a PostToolUse hook on \`Write|Edit\` that
|
|
10656
|
+
runs \`ctxloom update --incremental --quiet\` \u2014 so the graph is
|
|
10657
|
+
always up to date when you query it. No "did the index update yet?"
|
|
10658
|
+
guessing.`;
|
|
10659
|
+
var SESSION_START_HEADER = `#!/usr/bin/env bash
|
|
10660
|
+
# ctxloom \u2014 agent-harness session-start hook
|
|
10661
|
+
# Generated by \`ctxloom init\`. Re-run \`ctxloom init\` to update.
|
|
10662
|
+
# Manual edits will be overwritten on the next install.
|
|
10663
|
+
|
|
10664
|
+
set -e
|
|
10665
|
+
`;
|
|
10666
|
+
var SESSION_START_BODY = `DB=".ctxloom/graph.db"
|
|
10667
|
+
|
|
10668
|
+
if [ -f "$DB" ]; then
|
|
10669
|
+
# \`ctxloom status --json\` is cached + sub-100ms \u2014 keeps the hook
|
|
10670
|
+
# under its 2s timeout even on cold disk.
|
|
10671
|
+
STATS=$(ctxloom status --json 2>/dev/null || echo '{"nodes":0,"edges":0}')
|
|
10672
|
+
NODES=$(echo "$STATS" | grep -oE '"nodes":\\s*[0-9]+' | grep -oE '[0-9]+' || echo "?")
|
|
10673
|
+
EDGES=$(echo "$STATS" | grep -oE '"edges":\\s*[0-9]+' | grep -oE '[0-9]+' || echo "?")
|
|
10674
|
+
|
|
10675
|
+
cat <<EOF
|
|
10676
|
+
[ctxloom] Knowledge graph ready (\${NODES} nodes, \${EDGES} edges).
|
|
10677
|
+
|
|
10678
|
+
Start every workflow with \\\`ctx_get_minimal_context(task="...")\\\`.
|
|
10679
|
+
It returns ~150 tokens of orientation + a task-aware
|
|
10680
|
+
suggested_first_tool. Follow the meta.next_tool_suggestions on
|
|
10681
|
+
every response.
|
|
10682
|
+
|
|
10683
|
+
Prefer ctxloom MCP tools over Grep/Glob/Read:
|
|
10684
|
+
- ctx_detect_changes for code review
|
|
10685
|
+
- ctx_blast_radius / ctx_get_call_graph before refactoring
|
|
10686
|
+
- ctx_architecture_overview for orientation
|
|
10687
|
+
EOF
|
|
10688
|
+
else
|
|
10689
|
+
cat <<EOF
|
|
10690
|
+
[ctxloom] No knowledge graph found here.
|
|
10691
|
+
Run: ctxloom build
|
|
10692
|
+
|
|
10693
|
+
Then restart this session to enable graph-powered queries.
|
|
10694
|
+
EOF
|
|
10695
|
+
fi
|
|
10696
|
+
`;
|
|
10697
|
+
var SESSION_START_FULL = SESSION_START_HEADER + "\n" + SESSION_START_BODY;
|
|
10698
|
+
var CTXLOOM_HOOK_ENTRIES = {
|
|
10699
|
+
SessionStart: {
|
|
10700
|
+
matcher: "",
|
|
10701
|
+
hooks: [{ type: "command", command: ".claude/hooks/session-start.sh", timeout: 2 }]
|
|
10702
|
+
},
|
|
10703
|
+
PostToolUse: {
|
|
10704
|
+
matcher: "Write|Edit",
|
|
10705
|
+
hooks: [
|
|
10706
|
+
{
|
|
10707
|
+
type: "command",
|
|
10708
|
+
command: "ctxloom update --incremental --quiet",
|
|
10709
|
+
timeout: 30
|
|
10710
|
+
}
|
|
10711
|
+
]
|
|
10712
|
+
}
|
|
10713
|
+
};
|
|
10714
|
+
|
|
10715
|
+
// packages/core/src/install/hmacBlock.ts
|
|
10716
|
+
import crypto6 from "crypto";
|
|
10717
|
+
var DEFAULT_HMAC_KEY = "ctxloom-agent-harness-v1-published";
|
|
10718
|
+
function resolveHmacKey() {
|
|
10719
|
+
return process.env.CTXLOOM_INSTALL_KEY ?? DEFAULT_HMAC_KEY;
|
|
10720
|
+
}
|
|
10721
|
+
function computeBlockHmac(content, key = resolveHmacKey()) {
|
|
10722
|
+
return crypto6.createHmac("sha256", key).update(content, "utf-8").digest("hex");
|
|
10723
|
+
}
|
|
10724
|
+
function wrapBlock(name, content) {
|
|
10725
|
+
const hmac = computeBlockHmac(content);
|
|
10726
|
+
return [
|
|
10727
|
+
`<!-- BEGIN ${name} v:1 hmac:sha256:${hmac} -->`,
|
|
10728
|
+
content,
|
|
10729
|
+
`<!-- END ${name} -->`
|
|
10730
|
+
].join("\n");
|
|
10731
|
+
}
|
|
10732
|
+
var START_RE_TEMPLATE = (name) => new RegExp(`<!-- BEGIN ${escapeRegex(name)} v:(\\d+) hmac:sha256:([0-9a-f]{64}) -->`);
|
|
10733
|
+
var END_RE_TEMPLATE = (name) => new RegExp(`<!-- END ${escapeRegex(name)} -->`);
|
|
10734
|
+
function escapeRegex(s) {
|
|
10735
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10736
|
+
}
|
|
10737
|
+
function extractBlock(fileContent, name) {
|
|
10738
|
+
const startRe = START_RE_TEMPLATE(name);
|
|
10739
|
+
const endRe = END_RE_TEMPLATE(name);
|
|
10740
|
+
const startMatch = startRe.exec(fileContent);
|
|
10741
|
+
if (!startMatch) return null;
|
|
10742
|
+
const startIdx = startMatch.index;
|
|
10743
|
+
const afterStart = startIdx + startMatch[0].length;
|
|
10744
|
+
endRe.lastIndex = afterStart;
|
|
10745
|
+
const endMatch = endRe.exec(fileContent.slice(afterStart));
|
|
10746
|
+
if (!endMatch) return null;
|
|
10747
|
+
const endIdx = afterStart + endMatch.index + endMatch[0].length;
|
|
10748
|
+
let inner = fileContent.slice(afterStart, afterStart + endMatch.index);
|
|
10749
|
+
if (inner.startsWith("\n")) inner = inner.slice(1);
|
|
10750
|
+
if (inner.endsWith("\n")) inner = inner.slice(0, -1);
|
|
10751
|
+
return {
|
|
10752
|
+
content: inner,
|
|
10753
|
+
declaredHmac: startMatch[2],
|
|
10754
|
+
version: Number(startMatch[1]),
|
|
10755
|
+
start: startIdx,
|
|
10756
|
+
end: endIdx
|
|
10757
|
+
};
|
|
10758
|
+
}
|
|
10759
|
+
function verifyBlock(block) {
|
|
10760
|
+
return computeBlockHmac(block.content) === block.declaredHmac;
|
|
10761
|
+
}
|
|
10762
|
+
function upsertBlock(fileContent, name, newContent) {
|
|
10763
|
+
const wrapped = wrapBlock(name, newContent);
|
|
10764
|
+
const existing = extractBlock(fileContent, name);
|
|
10765
|
+
if (existing) {
|
|
10766
|
+
return fileContent.slice(0, existing.start) + wrapped + fileContent.slice(existing.end);
|
|
10767
|
+
}
|
|
10768
|
+
const sep = fileContent.length === 0 || fileContent.endsWith("\n\n") ? "" : fileContent.endsWith("\n") ? "\n" : "\n\n";
|
|
10769
|
+
return fileContent + sep + wrapped + "\n";
|
|
10770
|
+
}
|
|
10771
|
+
|
|
10772
|
+
// packages/core/src/install/skillTemplates.ts
|
|
10773
|
+
var EXPLORE_CONTENT = `---
|
|
10774
|
+
name: ctxloom-explore
|
|
10775
|
+
description: Orient yourself to an unfamiliar codebase using ctxloom's structural graph. Architecture overview + communities + top hubs in \u22645 tool calls.
|
|
10776
|
+
---
|
|
10777
|
+
|
|
10778
|
+
# Explore Codebase
|
|
10779
|
+
|
|
10780
|
+
Use this when you need to understand a codebase you haven't worked in
|
|
10781
|
+
before, or when re-orienting after time away.
|
|
10782
|
+
|
|
10783
|
+
## Steps
|
|
10784
|
+
|
|
10785
|
+
1. **Orientation anchor**: call \`ctx_get_minimal_context(task="explore this codebase")\`.
|
|
10786
|
+
The response includes graph stats, top hubs, and a
|
|
10787
|
+
\`suggested_first_tool\` \u2014 follow it (likely
|
|
10788
|
+
\`ctx_architecture_overview\`).
|
|
10789
|
+
|
|
10790
|
+
2. **Architecture overview**: call \`ctx_architecture_overview(max_response_tokens=2000)\`.
|
|
10791
|
+
Returns the community structure + hub nodes + cross-community bridges.
|
|
10792
|
+
|
|
10793
|
+
3. **Drill into the biggest communities**: from the overview's
|
|
10794
|
+
\`meta.next_tool_suggestions\`, call \`ctx_community_list\` for the
|
|
10795
|
+
top 1\u20132 communities. Skip communities labeled "tests" or
|
|
10796
|
+
"config" \u2014 they're usually peripheral.
|
|
10797
|
+
|
|
10798
|
+
4. **Investigate the architectural bridges**: call \`ctx_bridge_nodes\`.
|
|
10799
|
+
Bridge nodes are high-leverage \u2014 changing one affects multiple
|
|
10800
|
+
communities. Read these first to understand the codebase's
|
|
10801
|
+
coupling story.
|
|
10802
|
+
|
|
10803
|
+
5. **Tour the hubs**: call \`ctx_hub_nodes(limit=5, detail_level="minimal")\`.
|
|
10804
|
+
Top 5 most-depended-upon files. These are usually the heart of
|
|
10805
|
+
the codebase.
|
|
10806
|
+
|
|
10807
|
+
## Budget
|
|
10808
|
+
|
|
10809
|
+
- \u22645 ctxloom tool calls
|
|
10810
|
+
- \u22642000 tokens total response budget
|
|
10811
|
+
- Don't \`ctx_get_file\` anything during exploration \u2014 signatures are
|
|
10812
|
+
enough. Drop to file reads only if a specific symbol needs
|
|
10813
|
+
inspection (and only via \`ctx_get_definition\`, not raw read).
|
|
10814
|
+
|
|
10815
|
+
## Output
|
|
10816
|
+
|
|
10817
|
+
Summarize for the user:
|
|
10818
|
+
- Main communities (3\u20135 named clusters)
|
|
10819
|
+
- Top hubs by fan-in (the load-bearing files)
|
|
10820
|
+
- Top bridges (the architectural seams)
|
|
10821
|
+
- Recommended deep-dive starting points
|
|
10822
|
+
`;
|
|
10823
|
+
var BLAST_CONTENT = `---
|
|
10824
|
+
name: ctxloom-blast
|
|
10825
|
+
description: Compute blast radius + affected execution flows for a symbol or file before changing it. Pinpoints what will break.
|
|
10826
|
+
argument-hint: "<symbol-name | file-path>"
|
|
10827
|
+
---
|
|
10828
|
+
|
|
10829
|
+
# Blast Radius
|
|
10830
|
+
|
|
10831
|
+
Use this before any change to a public function, type, or file
|
|
10832
|
+
where you're not sure who depends on it.
|
|
10833
|
+
|
|
10834
|
+
## Inputs
|
|
10835
|
+
|
|
10836
|
+
- \`$ARGUMENTS\` \u2014 the symbol name (e.g. \`emitTelemetry\`) or file
|
|
10837
|
+
path (e.g. \`src/server.ts\`) you're about to modify.
|
|
10838
|
+
|
|
10839
|
+
## Steps
|
|
10840
|
+
|
|
10841
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="blast radius for $ARGUMENTS")\`.
|
|
10842
|
+
|
|
10843
|
+
2. **Blast radius**: call \`ctx_blast_radius(target="$ARGUMENTS", max_response_tokens=1500)\`.
|
|
10844
|
+
Returns transitive dependents \u2014 every file (and indirectly,
|
|
10845
|
+
every flow) that would be affected by a breaking change.
|
|
10846
|
+
|
|
10847
|
+
3. **Caller graph**: call \`ctx_get_call_graph(symbol="$ARGUMENTS", direction="callers", depth=2)\`.
|
|
10848
|
+
Direct + grandparent callers. Pair with the blast radius
|
|
10849
|
+
to distinguish "many transitive deps" from "load-bearing
|
|
10850
|
+
direct API."
|
|
10851
|
+
|
|
10852
|
+
4. **Affected execution flows**: call \`ctx_get_affected_flows(target="$ARGUMENTS")\`.
|
|
10853
|
+
Maps the change to ordered execution sequences \u2014 useful for
|
|
10854
|
+
debugging "what user-facing path breaks?"
|
|
10855
|
+
|
|
10856
|
+
5. **Test coverage check**: call \`ctx_knowledge_gaps(scope="$ARGUMENTS")\`.
|
|
10857
|
+
Highlights affected files lacking test coverage \u2014 those are the
|
|
10858
|
+
real risk surface.
|
|
10859
|
+
|
|
10860
|
+
## Budget
|
|
10861
|
+
|
|
10862
|
+
- \u22645 ctxloom tool calls
|
|
10863
|
+
- \u22642000 tokens total
|
|
10864
|
+
|
|
10865
|
+
## Output
|
|
10866
|
+
|
|
10867
|
+
Report to the user:
|
|
10868
|
+
- Total transitive dependents (number + top 5 by depth)
|
|
10869
|
+
- Direct callers (the API consumers)
|
|
10870
|
+
- Affected execution flows (named user-facing paths)
|
|
10871
|
+
- Coverage gaps on the affected files (the risk surface)
|
|
10872
|
+
- Recommendation: "safe to change" / "review carefully" / "needs migration plan"
|
|
10873
|
+
`;
|
|
10874
|
+
var REFACTOR_CONTENT = `---
|
|
10875
|
+
name: ctxloom-refactor-safely
|
|
10876
|
+
description: Plan and execute a rename or signature change with full caller-aware safety. Preview before applying.
|
|
10877
|
+
argument-hint: "<old-name> <new-name>"
|
|
10878
|
+
---
|
|
10879
|
+
|
|
10880
|
+
# Refactor Safely
|
|
10881
|
+
|
|
10882
|
+
Use this for renames, signature changes, or function moves. The
|
|
10883
|
+
skill enforces preview-before-apply.
|
|
10884
|
+
|
|
10885
|
+
## Inputs
|
|
10886
|
+
|
|
10887
|
+
- \`$1\` \u2014 current symbol name (e.g. \`emitTelemetry\`)
|
|
10888
|
+
- \`$2\` \u2014 target name (e.g. \`emitTelemetryEvent\`)
|
|
10889
|
+
|
|
10890
|
+
## Steps
|
|
10891
|
+
|
|
10892
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="refactor $1 to $2")\`.
|
|
10893
|
+
|
|
10894
|
+
2. **Surface every caller**: call \`ctx_get_call_graph(symbol="$1", direction="callers", depth=1)\`.
|
|
10895
|
+
The exhaustive caller list. Without this, a rename can break
|
|
10896
|
+
files you didn't notice depended on the symbol.
|
|
10897
|
+
|
|
10898
|
+
3. **Blast radius**: call \`ctx_blast_radius(target="$1")\`.
|
|
10899
|
+
Transitive impact \u2014 useful to gauge whether this should be a
|
|
10900
|
+
single PR or split with a deprecation period.
|
|
10901
|
+
|
|
10902
|
+
4. **Generate the refactor preview**: call \`ctx_refactor_preview(symbol="$1", new_name="$2")\`.
|
|
10903
|
+
Returns a diff preview WITHOUT writing anything. Inspect it.
|
|
10904
|
+
|
|
10905
|
+
5. **Confirm with the user**: show the preview summary
|
|
10906
|
+
(N files changed, M call sites updated). **DO NOT proceed to
|
|
10907
|
+
step 6 without explicit user confirmation.**
|
|
10908
|
+
|
|
10909
|
+
6. **Apply the refactor**: call \`ctx_apply_refactor(symbol="$1", new_name="$2")\`.
|
|
10910
|
+
This writes the changes to disk. Irreversible without a git
|
|
10911
|
+
reset.
|
|
10912
|
+
|
|
10913
|
+
7. **Verify**: call \`ctx_detect_changes\` and confirm the diff
|
|
10914
|
+
matches what the preview said. Surface any unexpected changes.
|
|
10915
|
+
|
|
10916
|
+
## Safety rails
|
|
10917
|
+
|
|
10918
|
+
- ALWAYS run step 4 (preview) before step 6 (apply)
|
|
10919
|
+
- ALWAYS ask the user for confirmation between preview and apply
|
|
10920
|
+
- If preview shows >50 files affected, recommend splitting into
|
|
10921
|
+
a deprecation-style migration instead of a single rename
|
|
10922
|
+
- If the symbol is exported from a public API package, refuse to
|
|
10923
|
+
proceed and recommend the user open a tracked migration plan
|
|
10924
|
+
|
|
10925
|
+
## Budget
|
|
10926
|
+
|
|
10927
|
+
- \u22647 ctxloom tool calls
|
|
10928
|
+
- \u22643000 tokens total (preview output can be larger than other skills)
|
|
10929
|
+
`;
|
|
10930
|
+
var COVERAGE_GAP_CONTENT = `---
|
|
10931
|
+
name: ctxloom-coverage-gap
|
|
10932
|
+
description: Identify code that lacks test coverage, prioritized by caller frequency and risk.
|
|
10933
|
+
---
|
|
10934
|
+
|
|
10935
|
+
# Coverage Gap Analysis
|
|
10936
|
+
|
|
10937
|
+
Use this to find untested code that genuinely matters \u2014 the
|
|
10938
|
+
intersection of "no tests" + "many callers" + "high risk score."
|
|
10939
|
+
|
|
10940
|
+
## Steps
|
|
10941
|
+
|
|
10942
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="check test coverage")\`.
|
|
10943
|
+
|
|
10944
|
+
2. **Knowledge gaps**: call \`ctx_knowledge_gaps(max_response_tokens=1200)\`.
|
|
10945
|
+
Lists every file lacking a \`tests_for\` graph edge. Raw list
|
|
10946
|
+
without prioritization.
|
|
10947
|
+
|
|
10948
|
+
3. **Score by impact**: for each gap, call
|
|
10949
|
+
\`ctx_get_call_graph(symbol=<gap_symbol>, direction="callers")\`
|
|
10950
|
+
to count callers. High caller-count + no tests = high priority.
|
|
10951
|
+
|
|
10952
|
+
4. **Cross-reference with churn**: call
|
|
10953
|
+
\`ctx_git_coupling(file=<gap_file>)\` for the top 5 gaps.
|
|
10954
|
+
Files churning often without tests are the urgent ones.
|
|
10955
|
+
|
|
10956
|
+
5. **Risk overlay**: call \`ctx_risk_overlay(scope=<top_gap_files>)\`.
|
|
10957
|
+
Combines churn + coupling into a single risk score.
|
|
10958
|
+
|
|
10959
|
+
## Output
|
|
10960
|
+
|
|
10961
|
+
Tabular report:
|
|
10962
|
+
|
|
10963
|
+
\`\`\`
|
|
10964
|
+
| File / Symbol | Callers | Churn | Risk | Recommendation |
|
|
10965
|
+
|---|---|---|---|---|
|
|
10966
|
+
| ... | ... | ... | ... | ... |
|
|
10967
|
+
\`\`\`
|
|
10968
|
+
|
|
10969
|
+
Recommendations should distinguish:
|
|
10970
|
+
- "Add tests now" (high caller count + high churn)
|
|
10971
|
+
- "Add tests during next change" (high caller count, low churn)
|
|
10972
|
+
- "Acceptable gap" (low caller count, low churn)
|
|
10973
|
+
|
|
10974
|
+
## Budget
|
|
10975
|
+
|
|
10976
|
+
- \u22646 ctxloom tool calls
|
|
10977
|
+
- \u22642500 tokens total
|
|
10978
|
+
`;
|
|
10979
|
+
var REVIEW_PR_CONTENT = `---
|
|
10980
|
+
name: ctxloom-review-pr
|
|
10981
|
+
description: Multi-tier code review of a PR using ctxloom's structural graph. Risk-scored, blast-radius-aware, coverage-conscious.
|
|
10982
|
+
argument-hint: "<PR number | branch name>"
|
|
10983
|
+
---
|
|
10984
|
+
|
|
10985
|
+
# Review PR
|
|
10986
|
+
|
|
10987
|
+
Comprehensive PR review using ctxloom's graph. Mirrors the
|
|
10988
|
+
multi-agent review the ctxloom-bot posts automatically \u2014 useful
|
|
10989
|
+
when reviewing manually or when the bot isn't wired up.
|
|
10990
|
+
|
|
10991
|
+
## Inputs
|
|
10992
|
+
|
|
10993
|
+
- \`$ARGUMENTS\` \u2014 PR number (e.g. \`142\`) or branch name (e.g. \`feat/foo\`).
|
|
10994
|
+
Defaults to the current branch if unset.
|
|
10995
|
+
|
|
10996
|
+
## Steps
|
|
10997
|
+
|
|
10998
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="review PR $ARGUMENTS")\`.
|
|
10999
|
+
|
|
11000
|
+
2. **Detect changes**: call \`ctx_detect_changes(base="main")\`.
|
|
11001
|
+
Risk-scored per-file analysis. Take the top 5 highest-risk files.
|
|
11002
|
+
|
|
11003
|
+
3. **Pull source for the risky files**: call \`ctx_git_diff_review(base="main", max_response_tokens=4000)\`.
|
|
11004
|
+
Token-efficient diff packet covering the changed files.
|
|
11005
|
+
|
|
11006
|
+
4. **Blast radius per high-risk file**: for the top 3 risky files,
|
|
11007
|
+
call \`ctx_blast_radius(target=<file>)\`. Surfaces files that the
|
|
11008
|
+
change indirectly affects but don't appear in the diff.
|
|
11009
|
+
|
|
11010
|
+
5. **Affected flows**: call \`ctx_get_affected_flows(base="main")\`.
|
|
11011
|
+
Which execution paths the PR touches. Use to identify what
|
|
11012
|
+
integration tests should pass.
|
|
11013
|
+
|
|
11014
|
+
6. **Coverage check**: call \`ctx_knowledge_gaps(scope=<changed_files>)\`.
|
|
11015
|
+
Surface changed files lacking tests.
|
|
11016
|
+
|
|
11017
|
+
7. **Generate the review**: structured output with:
|
|
11018
|
+
- Risk summary (low/medium/high overall)
|
|
11019
|
+
- File-by-file findings (severity-ranked)
|
|
11020
|
+
- Coverage gaps that need addressing
|
|
11021
|
+
- Blast-radius observations the diff doesn't show
|
|
11022
|
+
|
|
11023
|
+
## Tier discipline
|
|
11024
|
+
|
|
11025
|
+
This skill is the agent-driven equivalent of the bot's
|
|
11026
|
+
multi-specialist review. Use the same tier ladder:
|
|
11027
|
+
|
|
11028
|
+
- **T0 (structural)**: use the tools listed above \u2014 they're
|
|
11029
|
+
pre-fetched and cheap
|
|
11030
|
+
- **T1 (skeleton)**: \`ctx_get_definition\` for individual symbols
|
|
11031
|
+
- **T2 (full body)**: \`ctx_get_file\` only for files where the
|
|
11032
|
+
skeleton view is insufficient
|
|
11033
|
+
- **T3 (raw read)**: avoid; if the graph can't answer the question,
|
|
11034
|
+
prefer \`ctx_git_diff_review\` (token-efficient diff packet) over
|
|
11035
|
+
raw \`Read\`
|
|
11036
|
+
|
|
11037
|
+
## Budget
|
|
11038
|
+
|
|
11039
|
+
- \u22648 ctxloom tool calls
|
|
11040
|
+
- \u22645000 tokens total (review needs more headroom than other skills)
|
|
11041
|
+
|
|
11042
|
+
## Output format
|
|
11043
|
+
|
|
11044
|
+
\`\`\`
|
|
11045
|
+
## PR Review: <title>
|
|
11046
|
+
|
|
11047
|
+
### Summary
|
|
11048
|
+
<1\u20133 sentence overview>
|
|
11049
|
+
|
|
11050
|
+
### Risk Assessment
|
|
11051
|
+
- Overall: Low / Medium / High
|
|
11052
|
+
- Blast radius: X files, Y flows impacted
|
|
11053
|
+
- Coverage: N changed symbols covered / M total
|
|
11054
|
+
|
|
11055
|
+
### Findings
|
|
11056
|
+
|
|
11057
|
+
#### <file_path>
|
|
11058
|
+
- **Severity**: ...
|
|
11059
|
+
- **Issue**: ...
|
|
11060
|
+
- **Suggested fix**: ...
|
|
11061
|
+
|
|
11062
|
+
### Coverage Gaps
|
|
11063
|
+
|
|
11064
|
+
<table>
|
|
11065
|
+
|
|
11066
|
+
### Suggested follow-ups
|
|
11067
|
+
|
|
11068
|
+
<list>
|
|
11069
|
+
\`\`\`
|
|
11070
|
+
`;
|
|
11071
|
+
var BUDGET_STATS_CONTENT = `---
|
|
11072
|
+
name: ctxloom-budget-stats
|
|
11073
|
+
description: Inspect ctxloom's per-tool budget telemetry \u2014 fallback distribution + original-token p50/p75/p95 \u2014 to tune defaults from real usage.
|
|
11074
|
+
---
|
|
11075
|
+
|
|
11076
|
+
# Budget Stats
|
|
11077
|
+
|
|
11078
|
+
Wrapper around \`ctxloom budget-stats\` for inline use inside a
|
|
11079
|
+
Claude Code session. Useful when:
|
|
11080
|
+
|
|
11081
|
+
- Tuning per-tool \`DEFAULT_MAX_RESPONSE_TOKENS\` from real usage
|
|
11082
|
+
(the Phase B follow-up)
|
|
11083
|
+
- Diagnosing why a tool keeps falling back to skeleton mode
|
|
11084
|
+
- Understanding which tools dominate the user's token budget
|
|
11085
|
+
|
|
11086
|
+
## Steps
|
|
11087
|
+
|
|
11088
|
+
1. **Orientation**: call \`ctx_get_minimal_context(task="inspect budget telemetry")\`.
|
|
11089
|
+
Cheap (~150 tokens). Confirms the graph is wired up \u2014 if it's
|
|
11090
|
+
not, the user's MCP server probably can't emit budget events
|
|
11091
|
+
either, and the stats will be empty.
|
|
11092
|
+
|
|
11093
|
+
3. **Window selection**: ask the user how far back to look
|
|
11094
|
+
(default: 14d). Accept \`1d\`, \`7d\`, \`14d\`, \`30d\`.
|
|
11095
|
+
|
|
11096
|
+
4. **Optional tool filter**: ask if they want stats for a specific
|
|
11097
|
+
tool (e.g. \`ctx_get_file\`) or all tools.
|
|
11098
|
+
|
|
11099
|
+
5. **Run the CLI**: \`Bash\`-tool execute:
|
|
11100
|
+
\`ctxloom budget-stats --window=<N>d [--tool=<name>]\`
|
|
11101
|
+
|
|
11102
|
+
6. **Parse + summarize the output**:
|
|
11103
|
+
- Top 3 tools by breach count \u2192 these are the candidates for
|
|
11104
|
+
budget tuning
|
|
11105
|
+
- For each top tool, the p75 column is the suggested next
|
|
11106
|
+
\`DEFAULT_MAX_RESPONSE_TOKENS\` value (rationale: 75% of
|
|
11107
|
+
real-world calls fit under p75; the rest fall back gracefully
|
|
11108
|
+
to skeletons)
|
|
11109
|
+
|
|
11110
|
+
7. **Suggest concrete edits**: for each top tool, point to the
|
|
11111
|
+
source file (\`packages/core/src/tools/<tool>.ts\`) and the
|
|
11112
|
+
current constant. Don't apply edits without user confirmation.
|
|
11113
|
+
|
|
11114
|
+
## Budget
|
|
11115
|
+
|
|
11116
|
+
- \u22642 ctxloom tool calls (this skill is mostly Bash + parsing)
|
|
11117
|
+
- \u22641500 tokens response total
|
|
11118
|
+
|
|
11119
|
+
## Output
|
|
11120
|
+
|
|
11121
|
+
\`\`\`
|
|
11122
|
+
## Budget stats \u2014 <window>
|
|
11123
|
+
|
|
11124
|
+
### Top tools by breach count
|
|
11125
|
+
1. <tool>: N breaches, skeleton%, p75=<tokens>
|
|
11126
|
+
2. ...
|
|
11127
|
+
|
|
11128
|
+
### Suggested DEFAULT_MAX_RESPONSE_TOKENS tuning
|
|
11129
|
+
- packages/core/src/tools/<tool>.ts: <current> \u2192 <suggested-p75>
|
|
11130
|
+
(rationale: ...)
|
|
11131
|
+
\`\`\`
|
|
11132
|
+
`;
|
|
11133
|
+
var CTXLOOM_SKILLS = [
|
|
11134
|
+
{ name: "ctxloom-explore", content: EXPLORE_CONTENT },
|
|
11135
|
+
{ name: "ctxloom-blast", content: BLAST_CONTENT },
|
|
11136
|
+
{ name: "ctxloom-refactor-safely", content: REFACTOR_CONTENT },
|
|
11137
|
+
{ name: "ctxloom-coverage-gap", content: COVERAGE_GAP_CONTENT },
|
|
11138
|
+
{ name: "ctxloom-review-pr", content: REVIEW_PR_CONTENT },
|
|
11139
|
+
{ name: "ctxloom-budget-stats", content: BUDGET_STATS_CONTENT }
|
|
11140
|
+
];
|
|
11141
|
+
function skillFilePath(name) {
|
|
11142
|
+
return `.claude/skills/${name}/SKILL.md`;
|
|
11143
|
+
}
|
|
11144
|
+
|
|
11145
|
+
// packages/core/src/install/installer.ts
|
|
11146
|
+
function installHarness(opts = {}) {
|
|
11147
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
11148
|
+
const projectRoot = path36.resolve(cwd);
|
|
11149
|
+
const stat = fs28.statSync(projectRoot);
|
|
11150
|
+
if (!stat.isDirectory()) {
|
|
11151
|
+
throw new Error(`installHarness: ${projectRoot} is not a directory`);
|
|
11152
|
+
}
|
|
11153
|
+
const dryRun = opts.dryRun === true;
|
|
11154
|
+
const force = opts.force === true;
|
|
11155
|
+
const warnings = [];
|
|
11156
|
+
const claudeMd = writeRulesBlock(projectRoot, "CLAUDE.md", { dryRun, force, warnings });
|
|
11157
|
+
const agentsMd = writeRulesBlock(projectRoot, "AGENTS.md", { dryRun, force, warnings });
|
|
11158
|
+
const geminiMd = writeRulesBlock(projectRoot, "GEMINI.md", { dryRun, force, warnings });
|
|
11159
|
+
const hooksJson = writeHooksJson(projectRoot, { dryRun, warnings });
|
|
11160
|
+
const sessionStartSh = writeSessionStartScript(projectRoot, { dryRun });
|
|
11161
|
+
const skills = CTXLOOM_SKILLS.map((s) => writeSkill(projectRoot, s, { dryRun }));
|
|
11162
|
+
return {
|
|
11163
|
+
projectRoot,
|
|
11164
|
+
claudeMd,
|
|
11165
|
+
agentsMd,
|
|
11166
|
+
geminiMd,
|
|
11167
|
+
hooksJson,
|
|
11168
|
+
sessionStartSh,
|
|
11169
|
+
skills,
|
|
11170
|
+
warnings
|
|
11171
|
+
};
|
|
11172
|
+
}
|
|
11173
|
+
function safeJoin(root, name) {
|
|
11174
|
+
const target = path36.resolve(root, name);
|
|
11175
|
+
const rootResolved = path36.resolve(root);
|
|
11176
|
+
if (!target.startsWith(rootResolved + path36.sep) && target !== rootResolved) {
|
|
11177
|
+
throw new Error(`installHarness: refusing to write outside project root: ${target}`);
|
|
11178
|
+
}
|
|
11179
|
+
return target;
|
|
11180
|
+
}
|
|
11181
|
+
function writeRulesBlock(projectRoot, filename, opts) {
|
|
11182
|
+
const filePath = safeJoin(projectRoot, filename);
|
|
11183
|
+
const existed = fs28.existsSync(filePath);
|
|
11184
|
+
const existing = existed ? fs28.readFileSync(filePath, "utf-8") : "";
|
|
11185
|
+
if (existed) {
|
|
11186
|
+
const block = extractBlock(existing, RULES_BLOCK_NAME);
|
|
11187
|
+
if (block) {
|
|
11188
|
+
const intact = verifyBlock(block);
|
|
11189
|
+
if (intact && block.content === RULES_BLOCK_CONTENT) {
|
|
11190
|
+
return { path: filePath, created: false, updated: false, alreadyCorrect: true, dryRun: opts.dryRun };
|
|
11191
|
+
}
|
|
11192
|
+
if (!intact && !opts.force) {
|
|
11193
|
+
opts.warnings.push(
|
|
11194
|
+
`Drift detected in ${filename}: the CTXLOOM-RULES block has been hand-edited. Re-run \`ctxloom init --force\` to overwrite, or commit your changes and re-run.`
|
|
11195
|
+
);
|
|
11196
|
+
return { path: filePath, created: false, updated: false, alreadyCorrect: false, dryRun: opts.dryRun };
|
|
11197
|
+
}
|
|
11198
|
+
}
|
|
11199
|
+
}
|
|
11200
|
+
const next = upsertBlock(existing, RULES_BLOCK_NAME, RULES_BLOCK_CONTENT);
|
|
11201
|
+
if (!opts.dryRun) {
|
|
11202
|
+
fs28.writeFileSync(filePath, next, "utf-8");
|
|
11203
|
+
}
|
|
11204
|
+
return {
|
|
11205
|
+
path: filePath,
|
|
11206
|
+
created: !existed,
|
|
11207
|
+
updated: existed,
|
|
11208
|
+
alreadyCorrect: false,
|
|
11209
|
+
dryRun: opts.dryRun
|
|
11210
|
+
};
|
|
11211
|
+
}
|
|
11212
|
+
function writeHooksJson(projectRoot, opts) {
|
|
11213
|
+
const dir = safeJoin(projectRoot, ".claude");
|
|
11214
|
+
const filePath = safeJoin(projectRoot, ".claude/hooks.json");
|
|
11215
|
+
const existed = fs28.existsSync(filePath);
|
|
11216
|
+
let current = {};
|
|
11217
|
+
if (existed) {
|
|
11218
|
+
try {
|
|
11219
|
+
const text = fs28.readFileSync(filePath, "utf-8");
|
|
11220
|
+
current = JSON.parse(text);
|
|
11221
|
+
} catch (err) {
|
|
11222
|
+
opts.warnings.push(
|
|
11223
|
+
`Could not parse existing ${path36.relative(projectRoot, filePath)}; treating as empty. (${err instanceof Error ? err.message : String(err)})`
|
|
11224
|
+
);
|
|
11225
|
+
current = {};
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
const merged = { ...current };
|
|
11229
|
+
for (const event of ["SessionStart", "PostToolUse"]) {
|
|
11230
|
+
const incoming = CTXLOOM_HOOK_ENTRIES[event];
|
|
11231
|
+
const existingArr = Array.isArray(merged[event]) ? merged[event] : [];
|
|
11232
|
+
const filtered = existingArr.filter(
|
|
11233
|
+
(entry) => !isCtxloomEntry(entry, incoming.matcher)
|
|
11234
|
+
);
|
|
11235
|
+
merged[event] = [...filtered, incoming];
|
|
11236
|
+
}
|
|
11237
|
+
const nextJson = JSON.stringify(merged, null, 2) + "\n";
|
|
11238
|
+
let alreadyCorrect = false;
|
|
11239
|
+
if (existed) {
|
|
11240
|
+
const currentText = fs28.readFileSync(filePath, "utf-8");
|
|
11241
|
+
if (currentText === nextJson) alreadyCorrect = true;
|
|
11242
|
+
}
|
|
11243
|
+
if (!opts.dryRun && !alreadyCorrect) {
|
|
11244
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
11245
|
+
fs28.writeFileSync(filePath, nextJson, "utf-8");
|
|
11246
|
+
}
|
|
11247
|
+
return {
|
|
11248
|
+
path: filePath,
|
|
11249
|
+
created: !existed,
|
|
11250
|
+
updated: existed && !alreadyCorrect,
|
|
11251
|
+
alreadyCorrect,
|
|
11252
|
+
dryRun: opts.dryRun
|
|
11253
|
+
};
|
|
11254
|
+
}
|
|
11255
|
+
function isCtxloomEntry(entry, expectedMatcher) {
|
|
11256
|
+
if (entry.matcher !== expectedMatcher) return false;
|
|
11257
|
+
if (!Array.isArray(entry.hooks)) return false;
|
|
11258
|
+
return entry.hooks.some((h) => {
|
|
11259
|
+
if (!h || typeof h !== "object") return false;
|
|
11260
|
+
const cmd = h.command;
|
|
11261
|
+
if (typeof cmd !== "string") return false;
|
|
11262
|
+
return cmd.includes("ctxloom") || cmd.includes(".claude/hooks/session-start.sh");
|
|
11263
|
+
});
|
|
11264
|
+
}
|
|
11265
|
+
function writeSkill(projectRoot, skill, opts) {
|
|
11266
|
+
const dir = safeJoin(projectRoot, `.claude/skills/${skill.name}`);
|
|
11267
|
+
const filePath = safeJoin(projectRoot, skillFilePath(skill.name));
|
|
11268
|
+
const existed = fs28.existsSync(filePath);
|
|
11269
|
+
let alreadyCorrect = false;
|
|
11270
|
+
if (existed) {
|
|
11271
|
+
if (fs28.readFileSync(filePath, "utf-8") === skill.content) {
|
|
11272
|
+
alreadyCorrect = true;
|
|
11273
|
+
}
|
|
11274
|
+
}
|
|
11275
|
+
if (!opts.dryRun && !alreadyCorrect) {
|
|
11276
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
11277
|
+
fs28.writeFileSync(filePath, skill.content, "utf-8");
|
|
11278
|
+
}
|
|
11279
|
+
return {
|
|
11280
|
+
path: filePath,
|
|
11281
|
+
created: !existed,
|
|
11282
|
+
updated: existed && !alreadyCorrect,
|
|
11283
|
+
alreadyCorrect,
|
|
11284
|
+
dryRun: opts.dryRun
|
|
11285
|
+
};
|
|
11286
|
+
}
|
|
11287
|
+
function writeSessionStartScript(projectRoot, opts) {
|
|
11288
|
+
const dir = safeJoin(projectRoot, ".claude/hooks");
|
|
11289
|
+
const filePath = safeJoin(projectRoot, ".claude/hooks/session-start.sh");
|
|
11290
|
+
const existed = fs28.existsSync(filePath);
|
|
11291
|
+
let alreadyCorrect = false;
|
|
11292
|
+
if (existed) {
|
|
11293
|
+
const current = fs28.readFileSync(filePath, "utf-8");
|
|
11294
|
+
if (current === SESSION_START_FULL) alreadyCorrect = true;
|
|
11295
|
+
}
|
|
11296
|
+
if (!opts.dryRun && !alreadyCorrect) {
|
|
11297
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
11298
|
+
fs28.writeFileSync(filePath, SESSION_START_FULL, "utf-8");
|
|
11299
|
+
try {
|
|
11300
|
+
fs28.chmodSync(filePath, 493);
|
|
11301
|
+
} catch {
|
|
11302
|
+
}
|
|
11303
|
+
}
|
|
11304
|
+
return {
|
|
11305
|
+
path: filePath,
|
|
11306
|
+
created: !existed,
|
|
11307
|
+
updated: existed && !alreadyCorrect,
|
|
11308
|
+
alreadyCorrect,
|
|
11309
|
+
dryRun: opts.dryRun
|
|
11310
|
+
};
|
|
11311
|
+
}
|
|
11312
|
+
|
|
9989
11313
|
export {
|
|
9990
11314
|
GRAMMAR_MANIFEST,
|
|
9991
11315
|
findGrammar,
|
|
@@ -10090,6 +11414,20 @@ export {
|
|
|
10090
11414
|
noParseableSourcesWarning,
|
|
10091
11415
|
wrapWithIndexingEnvelope,
|
|
10092
11416
|
FirstTouchTracker,
|
|
10093
|
-
EmittedOnceTracker
|
|
11417
|
+
EmittedOnceTracker,
|
|
11418
|
+
RULES_BLOCK_NAME,
|
|
11419
|
+
RULES_BLOCK_CONTENT,
|
|
11420
|
+
SESSION_START_FULL,
|
|
11421
|
+
CTXLOOM_HOOK_ENTRIES,
|
|
11422
|
+
DEFAULT_HMAC_KEY,
|
|
11423
|
+
resolveHmacKey,
|
|
11424
|
+
computeBlockHmac,
|
|
11425
|
+
wrapBlock,
|
|
11426
|
+
extractBlock,
|
|
11427
|
+
verifyBlock,
|
|
11428
|
+
upsertBlock,
|
|
11429
|
+
CTXLOOM_SKILLS,
|
|
11430
|
+
skillFilePath,
|
|
11431
|
+
installHarness
|
|
10094
11432
|
};
|
|
10095
|
-
//# sourceMappingURL=chunk-
|
|
11433
|
+
//# sourceMappingURL=chunk-7GZVGIQL.js.map
|