loopctl-mcp-server 2.2.0 → 2.23.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.
- package/README.md +46 -12
- package/index.js +1227 -81
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -139,9 +139,12 @@ function toContent(result) {
|
|
|
139
139
|
/**
|
|
140
140
|
* Compact variant for list endpoints — strips acceptance_criteria and
|
|
141
141
|
* description (use get_story for full details). Keeps all other fields.
|
|
142
|
-
*
|
|
142
|
+
* A modest DEFAULT page size keeps unprompted responses small, but an explicit
|
|
143
|
+
* `limit` is honored up to the server's max (500) so story enumeration paginates
|
|
144
|
+
* honestly instead of silently capping at 20 and skipping rows on offset advance.
|
|
143
145
|
*/
|
|
144
|
-
const
|
|
146
|
+
const DEFAULT_STORY_PAGE_SIZE = 20;
|
|
147
|
+
const SERVER_MAX_STORY_PAGE_SIZE = 500;
|
|
145
148
|
|
|
146
149
|
function toContentCompact(result) {
|
|
147
150
|
if (result && result.error === true) return toContent(result);
|
|
@@ -175,8 +178,12 @@ async function getTenant() {
|
|
|
175
178
|
return toContent(result);
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
async function listProjects() {
|
|
179
|
-
const
|
|
181
|
+
async function listProjects({ page, page_size } = {}) {
|
|
182
|
+
const params = new URLSearchParams();
|
|
183
|
+
if (page != null) params.set("page", String(page));
|
|
184
|
+
if (page_size != null) params.set("page_size", String(page_size));
|
|
185
|
+
const query = params.toString() ? `?${params}` : "";
|
|
186
|
+
const result = await apiCall("GET", `/api/v1/projects${query}`);
|
|
180
187
|
return toContent(result);
|
|
181
188
|
}
|
|
182
189
|
|
|
@@ -367,7 +374,10 @@ async function listStories({ project_id, agent_status, verified_status, epic_id,
|
|
|
367
374
|
if (agent_status) params.set("agent_status", agent_status);
|
|
368
375
|
if (verified_status) params.set("verified_status", verified_status);
|
|
369
376
|
if (epic_id) params.set("epic_id", epic_id);
|
|
370
|
-
params.set(
|
|
377
|
+
params.set(
|
|
378
|
+
"limit",
|
|
379
|
+
String(Math.min(limit ?? DEFAULT_STORY_PAGE_SIZE, SERVER_MAX_STORY_PAGE_SIZE)),
|
|
380
|
+
);
|
|
371
381
|
if (offset != null) params.set("offset", String(offset));
|
|
372
382
|
if (include_token_totals) params.set("include_token_totals", "true");
|
|
373
383
|
|
|
@@ -375,9 +385,13 @@ async function listStories({ project_id, agent_status, verified_status, epic_id,
|
|
|
375
385
|
return toContentCompact(result);
|
|
376
386
|
}
|
|
377
387
|
|
|
378
|
-
async function listReadyStories({ project_id,
|
|
388
|
+
async function listReadyStories({ project_id, page, page_size }) {
|
|
389
|
+
// /stories/ready paginates by page/page_size — the old `limit` param was
|
|
390
|
+
// silently ignored by the server, capping callers at the first page.
|
|
379
391
|
const params = new URLSearchParams({ project_id });
|
|
380
|
-
params.set("
|
|
392
|
+
if (page != null) params.set("page", String(page));
|
|
393
|
+
if (page_size != null)
|
|
394
|
+
params.set("page_size", String(Math.min(page_size, SERVER_MAX_STORY_PAGE_SIZE)));
|
|
381
395
|
|
|
382
396
|
const result = await apiCall("GET", `/api/v1/stories/ready?${params}`);
|
|
383
397
|
return toContentCompact(result);
|
|
@@ -550,14 +564,20 @@ async function getCostSummary({ project_id, breakdown }) {
|
|
|
550
564
|
return toContent(result);
|
|
551
565
|
}
|
|
552
566
|
|
|
553
|
-
async function getStoryTokenUsage({ story_id }) {
|
|
554
|
-
const
|
|
567
|
+
async function getStoryTokenUsage({ story_id, page, page_size }) {
|
|
568
|
+
const params = new URLSearchParams();
|
|
569
|
+
if (page != null) params.set("page", String(page));
|
|
570
|
+
if (page_size != null) params.set("page_size", String(page_size));
|
|
571
|
+
const query = params.toString() ? `?${params}` : "";
|
|
572
|
+
const result = await apiCall("GET", `/api/v1/stories/${story_id}/token-usage${query}`);
|
|
555
573
|
return toContent(result);
|
|
556
574
|
}
|
|
557
575
|
|
|
558
|
-
async function getCostAnomalies({ project_id }) {
|
|
576
|
+
async function getCostAnomalies({ project_id, page, page_size }) {
|
|
559
577
|
const params = new URLSearchParams();
|
|
560
578
|
if (project_id) params.set("project_id", project_id);
|
|
579
|
+
if (page != null) params.set("page", String(page));
|
|
580
|
+
if (page_size != null) params.set("page_size", String(page_size));
|
|
561
581
|
|
|
562
582
|
const query = params.toString() ? `?${params}` : "";
|
|
563
583
|
const result = await apiCall("GET", `/api/v1/cost-anomalies${query}`);
|
|
@@ -579,31 +599,214 @@ async function setTokenBudget({ scope_type, scope_id, budget_millicents, alert_t
|
|
|
579
599
|
|
|
580
600
|
// --- Knowledge Wiki Tools (agent key) ---
|
|
581
601
|
|
|
582
|
-
async function knowledgeIndex({ project_id, story_id }) {
|
|
602
|
+
async function knowledgeIndex({ project_id, story_id, category, tags, match, offset, limit, fields }) {
|
|
603
|
+
if (project_id && !UUID_RE.test(project_id)) {
|
|
604
|
+
return {
|
|
605
|
+
content: [{ type: "text", text: "Error: project_id must be a canonical UUID (8-4-4-4-12 hex)." }],
|
|
606
|
+
isError: true,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
583
609
|
const basePath = project_id
|
|
584
610
|
? `/api/v1/projects/${project_id}/knowledge/index`
|
|
585
611
|
: "/api/v1/knowledge/index";
|
|
586
612
|
const params = new URLSearchParams();
|
|
587
613
|
if (story_id) params.set("story_id", story_id);
|
|
614
|
+
if (category) params.set("category", category);
|
|
615
|
+
if (tags) params.set("tags", tags);
|
|
616
|
+
if (match) params.set("match", match);
|
|
617
|
+
if (offset != null) params.set("offset", String(offset));
|
|
618
|
+
if (limit != null) params.set("limit", String(limit));
|
|
619
|
+
if (fields) params.set("fields", Array.isArray(fields) ? fields.join(",") : fields);
|
|
588
620
|
const qs = params.toString();
|
|
589
621
|
const path = qs ? `${basePath}?${qs}` : basePath;
|
|
590
622
|
const result = await apiCall("GET", path, null, process.env.LOOPCTL_AGENT_KEY);
|
|
591
623
|
return toContent(result);
|
|
592
624
|
}
|
|
593
625
|
|
|
594
|
-
async function
|
|
595
|
-
|
|
626
|
+
async function knowledgeStats({ project_id }) {
|
|
627
|
+
if (project_id && !UUID_RE.test(project_id)) {
|
|
628
|
+
return {
|
|
629
|
+
content: [{ type: "text", text: "Error: project_id must be a canonical UUID (8-4-4-4-12 hex)." }],
|
|
630
|
+
isError: true,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
const path = project_id
|
|
634
|
+
? `/api/v1/projects/${project_id}/knowledge/stats`
|
|
635
|
+
: "/api/v1/knowledge/stats";
|
|
636
|
+
const result = await apiCall("GET", path, null, process.env.LOOPCTL_AGENT_KEY);
|
|
637
|
+
return toContent(result);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function knowledgeGraph({ article_id, depth, project_id }) {
|
|
641
|
+
const params = new URLSearchParams();
|
|
642
|
+
params.set("article_id", article_id);
|
|
643
|
+
if (depth != null) params.set("depth", String(depth));
|
|
644
|
+
if (project_id) params.set("project_id", project_id);
|
|
645
|
+
const result = await apiCall(
|
|
646
|
+
"GET",
|
|
647
|
+
`/api/v1/knowledge/graph?${params}`,
|
|
648
|
+
null,
|
|
649
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
650
|
+
);
|
|
651
|
+
return toContent(result);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function knowledgeSuggestLinks({ article_id, limit, threshold }) {
|
|
655
|
+
const params = new URLSearchParams();
|
|
656
|
+
if (limit != null) params.set("limit", String(limit));
|
|
657
|
+
if (threshold != null) params.set("threshold", String(threshold));
|
|
658
|
+
const qs = params.toString();
|
|
659
|
+
const base = `/api/v1/knowledge/articles/${article_id}/suggested_links`;
|
|
660
|
+
const result = await apiCall(
|
|
661
|
+
"GET",
|
|
662
|
+
qs ? `${base}?${qs}` : base,
|
|
663
|
+
null,
|
|
664
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
665
|
+
);
|
|
666
|
+
return toContent(result);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function knowledgeDistantPairs({ min_distance, max_distance, bridge_path, limit, offset }) {
|
|
670
|
+
const params = new URLSearchParams();
|
|
671
|
+
if (min_distance != null) params.set("min_distance", String(min_distance));
|
|
672
|
+
if (max_distance != null) params.set("max_distance", String(max_distance));
|
|
673
|
+
if (bridge_path === true) params.set("bridge_path", "true");
|
|
674
|
+
if (limit != null) params.set("limit", String(limit));
|
|
675
|
+
if (offset != null) params.set("offset", String(offset));
|
|
676
|
+
const qs = params.toString();
|
|
677
|
+
const path = qs ? `/api/v1/knowledge/pairs?${qs}` : "/api/v1/knowledge/pairs";
|
|
678
|
+
const result = await apiCall("GET", path, null, process.env.LOOPCTL_AGENT_KEY);
|
|
679
|
+
return toContent(result);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function knowledgeNovelty({ ideas, texts, prior_tag }) {
|
|
683
|
+
// Accept either shape (#169): `ideas` (strings or objects) or `texts` (strings).
|
|
684
|
+
const body = {};
|
|
685
|
+
if (ideas !== undefined) body.ideas = ideas;
|
|
686
|
+
if (texts !== undefined) body.texts = texts;
|
|
687
|
+
if (prior_tag) body.prior_tag = prior_tag;
|
|
688
|
+
const result = await apiCall("POST", "/api/v1/knowledge/novelty", body, process.env.LOOPCTL_AGENT_KEY);
|
|
689
|
+
return toContent(result);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function knowledgeRandomWalk({ start_id, length }) {
|
|
693
|
+
const params = new URLSearchParams();
|
|
694
|
+
params.set("start_id", start_id);
|
|
695
|
+
if (length != null) params.set("length", String(length));
|
|
696
|
+
const result = await apiCall(
|
|
697
|
+
"GET",
|
|
698
|
+
`/api/v1/knowledge/walk?${params}`,
|
|
699
|
+
null,
|
|
700
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
701
|
+
);
|
|
702
|
+
return toContent(result);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function knowledgeCount({
|
|
706
|
+
project_id,
|
|
707
|
+
category,
|
|
708
|
+
status,
|
|
709
|
+
tags,
|
|
710
|
+
match,
|
|
711
|
+
source_type,
|
|
712
|
+
source_id,
|
|
713
|
+
idempotency_key,
|
|
714
|
+
}) {
|
|
715
|
+
const params = new URLSearchParams();
|
|
716
|
+
if (project_id) params.set("project_id", project_id);
|
|
717
|
+
if (category) params.set("category", category);
|
|
718
|
+
if (status) params.set("status", status);
|
|
719
|
+
if (tags) params.set("tags", tags);
|
|
720
|
+
if (match) params.set("match", match);
|
|
721
|
+
if (source_type) params.set("source_type", source_type);
|
|
722
|
+
if (source_id) params.set("source_id", source_id);
|
|
723
|
+
if (idempotency_key) params.set("idempotency_key", idempotency_key);
|
|
724
|
+
const qs = params.toString();
|
|
725
|
+
const path = qs ? `/api/v1/knowledge/count?${qs}` : "/api/v1/knowledge/count";
|
|
726
|
+
const result = await apiCall("GET", path, null, process.env.LOOPCTL_AGENT_KEY);
|
|
727
|
+
return toContent(result);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function knowledgeFacets({
|
|
731
|
+
project_id,
|
|
732
|
+
category,
|
|
733
|
+
status,
|
|
734
|
+
tags,
|
|
735
|
+
match,
|
|
736
|
+
tag_prefix,
|
|
737
|
+
limit,
|
|
738
|
+
}) {
|
|
739
|
+
const params = new URLSearchParams();
|
|
740
|
+
params.set("group_by", "tag");
|
|
741
|
+
if (project_id) params.set("project_id", project_id);
|
|
742
|
+
if (category) params.set("category", category);
|
|
743
|
+
if (status) params.set("status", status);
|
|
744
|
+
if (tags) params.set("tags", tags);
|
|
745
|
+
if (match) params.set("match", match);
|
|
746
|
+
if (tag_prefix) params.set("tag_prefix", tag_prefix);
|
|
747
|
+
if (limit != null) params.set("limit", String(limit));
|
|
748
|
+
const result = await apiCall(
|
|
749
|
+
"GET",
|
|
750
|
+
`/api/v1/knowledge/facets?${params}`,
|
|
751
|
+
null,
|
|
752
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
753
|
+
);
|
|
754
|
+
return toContent(result);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function knowledgeSearch({ q, project_id, story_id, category, tags, match, mode, limit, offset }) {
|
|
758
|
+
const params = new URLSearchParams();
|
|
759
|
+
if (q != null && q !== "") params.set("q", q);
|
|
596
760
|
if (project_id) params.set("project_id", project_id);
|
|
597
761
|
if (story_id) params.set("story_id", story_id);
|
|
598
762
|
if (category) params.set("category", category);
|
|
599
763
|
if (tags) params.set("tags", tags);
|
|
764
|
+
if (match) params.set("match", match);
|
|
600
765
|
if (mode) params.set("mode", mode);
|
|
601
766
|
if (limit != null) params.set("limit", String(limit));
|
|
767
|
+
if (offset != null) params.set("offset", String(offset));
|
|
602
768
|
|
|
603
769
|
const result = await apiCall("GET", `/api/v1/knowledge/search?${params}`, null, process.env.LOOPCTL_AGENT_KEY);
|
|
604
770
|
return toContent(result);
|
|
605
771
|
}
|
|
606
772
|
|
|
773
|
+
async function knowledgeList({
|
|
774
|
+
project_id,
|
|
775
|
+
category,
|
|
776
|
+
status,
|
|
777
|
+
tags,
|
|
778
|
+
match,
|
|
779
|
+
source_type,
|
|
780
|
+
source_id,
|
|
781
|
+
idempotency_key,
|
|
782
|
+
limit,
|
|
783
|
+
offset,
|
|
784
|
+
include_body,
|
|
785
|
+
}) {
|
|
786
|
+
const params = new URLSearchParams();
|
|
787
|
+
if (project_id) params.set("project_id", project_id);
|
|
788
|
+
if (category) params.set("category", category);
|
|
789
|
+
if (status) params.set("status", status);
|
|
790
|
+
if (tags) params.set("tags", tags);
|
|
791
|
+
if (match) params.set("match", match);
|
|
792
|
+
if (source_type) params.set("source_type", source_type);
|
|
793
|
+
if (source_id) params.set("source_id", source_id);
|
|
794
|
+
if (idempotency_key) params.set("idempotency_key", idempotency_key);
|
|
795
|
+
if (limit != null) params.set("limit", String(limit));
|
|
796
|
+
if (offset != null) params.set("offset", String(offset));
|
|
797
|
+
// Body-less summary by default (safe to enumerate large pages); opt into full
|
|
798
|
+
// bodies (byte-budget bounded server-side) with include_body: true.
|
|
799
|
+
if (include_body === true) params.set("include_body", "true");
|
|
800
|
+
|
|
801
|
+
const result = await apiCall(
|
|
802
|
+
"GET",
|
|
803
|
+
`/api/v1/articles?${params}`,
|
|
804
|
+
null,
|
|
805
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
806
|
+
);
|
|
807
|
+
return toContent(result);
|
|
808
|
+
}
|
|
809
|
+
|
|
607
810
|
async function knowledgeGet({ article_id, project_id, story_id }) {
|
|
608
811
|
const params = new URLSearchParams();
|
|
609
812
|
if (project_id) params.set("project_id", project_id);
|
|
@@ -614,24 +817,63 @@ async function knowledgeGet({ article_id, project_id, story_id }) {
|
|
|
614
817
|
return toContent(result);
|
|
615
818
|
}
|
|
616
819
|
|
|
617
|
-
async function knowledgeContext({
|
|
820
|
+
async function knowledgeContext({
|
|
821
|
+
query,
|
|
822
|
+
project_id,
|
|
823
|
+
story_id,
|
|
824
|
+
limit,
|
|
825
|
+
recency_weight,
|
|
826
|
+
memory_types,
|
|
827
|
+
agents,
|
|
828
|
+
conversation_id,
|
|
829
|
+
}) {
|
|
618
830
|
const params = new URLSearchParams({ query });
|
|
619
831
|
if (project_id) params.set("project_id", project_id);
|
|
620
832
|
if (story_id) params.set("story_id", story_id);
|
|
621
833
|
if (limit != null) params.set("limit", String(limit));
|
|
622
834
|
if (recency_weight != null) params.set("recency_weight", String(recency_weight));
|
|
835
|
+
// Agent-memory scoping (comma-separated lists are OR'd; conversation_id is exact).
|
|
836
|
+
if (memory_types) params.set("memory_types", Array.isArray(memory_types) ? memory_types.join(",") : memory_types);
|
|
837
|
+
if (agents) params.set("agents", Array.isArray(agents) ? agents.join(",") : agents);
|
|
838
|
+
if (conversation_id) params.set("conversation_id", conversation_id);
|
|
623
839
|
|
|
624
840
|
const result = await apiCall("GET", `/api/v1/knowledge/context?${params}`, null, process.env.LOOPCTL_AGENT_KEY);
|
|
625
841
|
return toContent(result);
|
|
626
842
|
}
|
|
627
843
|
|
|
628
|
-
async function knowledgeCreate({
|
|
844
|
+
async function knowledgeCreate({
|
|
845
|
+
title,
|
|
846
|
+
body,
|
|
847
|
+
category,
|
|
848
|
+
tags,
|
|
849
|
+
project_id,
|
|
850
|
+
draft,
|
|
851
|
+
source_type,
|
|
852
|
+
source_id,
|
|
853
|
+
idempotency_key,
|
|
854
|
+
metadata,
|
|
855
|
+
}) {
|
|
629
856
|
const payload = { title, body };
|
|
630
857
|
if (category) payload.category = category;
|
|
631
858
|
if (tags) payload.tags = tags;
|
|
632
859
|
if (project_id) payload.project_id = project_id;
|
|
860
|
+
if (source_type) payload.source_type = source_type;
|
|
861
|
+
if (source_id) payload.source_id = source_id;
|
|
862
|
+
if (idempotency_key) payload.idempotency_key = idempotency_key;
|
|
863
|
+
if (metadata) payload.metadata = metadata;
|
|
864
|
+
|
|
865
|
+
// Articles publish on create by default for every role (including agent), so a
|
|
866
|
+
// plain create routes through the agent key and is immediately visible. Pass
|
|
867
|
+
// draft:true to stage it for later review instead — publish it afterwards with
|
|
868
|
+
// knowledge_publish.
|
|
869
|
+
if (draft) payload.draft = true;
|
|
633
870
|
|
|
634
|
-
const result = await apiCall(
|
|
871
|
+
const result = await apiCall(
|
|
872
|
+
"POST",
|
|
873
|
+
"/api/v1/articles",
|
|
874
|
+
payload,
|
|
875
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
876
|
+
);
|
|
635
877
|
return toContent(result);
|
|
636
878
|
}
|
|
637
879
|
|
|
@@ -649,6 +891,46 @@ async function knowledgeBulkPublish({ article_ids }) {
|
|
|
649
891
|
{ article_ids },
|
|
650
892
|
process.env.LOOPCTL_USER_KEY
|
|
651
893
|
);
|
|
894
|
+
|
|
895
|
+
// Partial success returns 200 even when nothing published. Surface a warning
|
|
896
|
+
// so the agent doesn't treat not_found/errored ids as success.
|
|
897
|
+
const counts = result?.meta?.counts;
|
|
898
|
+
if (counts && (counts.not_found > 0 || counts.errored > 0)) {
|
|
899
|
+
const warning =
|
|
900
|
+
`WARNING: bulk-publish was partial — published ${counts.published}, ` +
|
|
901
|
+
`skipped ${counts.skipped}, not_found ${counts.not_found}, errored ${counts.errored} ` +
|
|
902
|
+
`(of ${counts.requested} requested). Inspect meta.results for per-id outcomes; ` +
|
|
903
|
+
`not_found/errored ids were NOT published.`;
|
|
904
|
+
return {
|
|
905
|
+
content: [{ type: "text", text: warning }, ...toContent(result).content],
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return toContent(result);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function knowledgeBulkUnpublish({ article_ids }) {
|
|
913
|
+
const result = await apiCall(
|
|
914
|
+
"POST",
|
|
915
|
+
"/api/v1/knowledge/bulk-unpublish",
|
|
916
|
+
{ article_ids },
|
|
917
|
+
process.env.LOOPCTL_USER_KEY
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
// Partial success returns 200 even when nothing unpublished. Surface a warning
|
|
921
|
+
// so the agent doesn't treat not_found/errored ids as success.
|
|
922
|
+
const counts = result?.meta?.counts;
|
|
923
|
+
if (counts && (counts.not_found > 0 || counts.errored > 0)) {
|
|
924
|
+
const warning =
|
|
925
|
+
`WARNING: bulk-unpublish was partial — unpublished ${counts.unpublished}, ` +
|
|
926
|
+
`skipped ${counts.skipped}, not_found ${counts.not_found}, errored ${counts.errored} ` +
|
|
927
|
+
`(of ${counts.requested} requested). Inspect meta.results for per-id outcomes; ` +
|
|
928
|
+
`not_found/errored ids were NOT unpublished.`;
|
|
929
|
+
return {
|
|
930
|
+
content: [{ type: "text", text: warning }, ...toContent(result).content],
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
652
934
|
return toContent(result);
|
|
653
935
|
}
|
|
654
936
|
|
|
@@ -682,12 +964,68 @@ async function knowledgeDelete({ article_id }) {
|
|
|
682
964
|
return toContent(result);
|
|
683
965
|
}
|
|
684
966
|
|
|
967
|
+
async function knowledgeBulkDelete({
|
|
968
|
+
article_ids,
|
|
969
|
+
source_type,
|
|
970
|
+
source_id,
|
|
971
|
+
tag,
|
|
972
|
+
confirm,
|
|
973
|
+
dry_run,
|
|
974
|
+
hard,
|
|
975
|
+
token,
|
|
976
|
+
confirm_hash,
|
|
977
|
+
}) {
|
|
978
|
+
const payload = {};
|
|
979
|
+
if (article_ids) payload.article_ids = article_ids;
|
|
980
|
+
if (source_type) payload.source_type = source_type;
|
|
981
|
+
if (source_id) payload.source_id = source_id;
|
|
982
|
+
if (tag) payload.tag = tag;
|
|
983
|
+
if (confirm) payload.confirm = confirm;
|
|
984
|
+
// US-27.12: dry-run preview + irreversible hard delete via a single-use frozen
|
|
985
|
+
// token. dry_run=true mutates nothing (returns meta.would_affect; for the hard
|
|
986
|
+
// path a single-use meta.token); hard=true + token performs the FK-correct
|
|
987
|
+
// IRREVERSIBLE delete over the frozen id-set. Oversized selectors echo
|
|
988
|
+
// meta.confirm_hash for re-confirm-on-drift instead of a token.
|
|
989
|
+
if (dry_run) payload.dry_run = true;
|
|
990
|
+
if (hard) payload.hard = true;
|
|
991
|
+
if (token) payload.token = token;
|
|
992
|
+
if (confirm_hash) payload.confirm_hash = confirm_hash;
|
|
993
|
+
|
|
994
|
+
const result = await apiCall(
|
|
995
|
+
"POST",
|
|
996
|
+
"/api/v1/knowledge/bulk-delete",
|
|
997
|
+
payload,
|
|
998
|
+
process.env.LOOPCTL_USER_KEY,
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
// Partial success returns 200 even when some/none archived — surface a warning
|
|
1002
|
+
// so the agent doesn't treat not_found/errored, or a nothing-archived run, as
|
|
1003
|
+
// success.
|
|
1004
|
+
const counts = result?.meta?.counts;
|
|
1005
|
+
if (
|
|
1006
|
+
counts &&
|
|
1007
|
+
(counts.not_found > 0 ||
|
|
1008
|
+
counts.errored > 0 ||
|
|
1009
|
+
(counts.archived === 0 && counts.requested > 0))
|
|
1010
|
+
) {
|
|
1011
|
+
const warning =
|
|
1012
|
+
`WARNING: bulk-delete was partial — archived ${counts.archived}, ` +
|
|
1013
|
+
`skipped ${counts.skipped}, not_found ${counts.not_found}, errored ${counts.errored} ` +
|
|
1014
|
+
`(of ${counts.requested}). Inspect meta.results; not_found/errored ids were NOT archived.`;
|
|
1015
|
+
return {
|
|
1016
|
+
content: [{ type: "text", text: warning }, ...toContent(result).content],
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return toContent(result);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
685
1023
|
async function knowledgeDrafts({ limit, offset, project_id }) {
|
|
686
1024
|
const params = new URLSearchParams();
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
);
|
|
1025
|
+
// Pass `limit` through verbatim (like knowledge_list/index/search) so the
|
|
1026
|
+
// server honors it up to its max page size and returns 400 above it — rather
|
|
1027
|
+
// than silently clamping client-side, which would truncate draft enumeration.
|
|
1028
|
+
if (limit != null) params.set("limit", String(limit));
|
|
691
1029
|
if (offset != null) params.set("offset", String(offset));
|
|
692
1030
|
if (project_id) params.set("project_id", project_id);
|
|
693
1031
|
const path = `/api/v1/knowledge/drafts?${params.toString()}`;
|
|
@@ -709,24 +1047,28 @@ async function knowledgeLint({ project_id, stale_days, min_coverage, max_per_cat
|
|
|
709
1047
|
return toContent(result);
|
|
710
1048
|
}
|
|
711
1049
|
|
|
712
|
-
async function knowledgeIngest({ url, content, source_type, project_id }) {
|
|
1050
|
+
async function knowledgeIngest({ url, content, source_type, project_id, publish }) {
|
|
713
1051
|
const body = { source_type };
|
|
714
1052
|
if (url) body.url = url;
|
|
715
1053
|
if (content) body.content = content;
|
|
716
1054
|
if (project_id) body.project_id = project_id;
|
|
1055
|
+
if (publish) body.publish = true;
|
|
717
1056
|
const result = await apiCall("POST", "/api/v1/knowledge/ingest", body, process.env.LOOPCTL_ORCH_KEY);
|
|
718
1057
|
return toContent(result);
|
|
719
1058
|
}
|
|
720
1059
|
|
|
721
|
-
async function knowledgeIngestBatch({ items, project_id }) {
|
|
722
|
-
//
|
|
723
|
-
//
|
|
1060
|
+
async function knowledgeIngestBatch({ items, project_id, publish }) {
|
|
1061
|
+
// Apply batch-level project_id / publish as defaults to any item that doesn't
|
|
1062
|
+
// set its own.
|
|
724
1063
|
const resolvedItems = Array.isArray(items)
|
|
725
|
-
? items.map((item) =>
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1064
|
+
? items.map((item) => {
|
|
1065
|
+
if (!item) return item;
|
|
1066
|
+
let resolved = item;
|
|
1067
|
+
if (project_id && resolved.project_id == null)
|
|
1068
|
+
resolved = { ...resolved, project_id };
|
|
1069
|
+
if (publish && resolved.publish == null) resolved = { ...resolved, publish: true };
|
|
1070
|
+
return resolved;
|
|
1071
|
+
})
|
|
730
1072
|
: items;
|
|
731
1073
|
|
|
732
1074
|
const result = await apiCall(
|
|
@@ -738,8 +1080,18 @@ async function knowledgeIngestBatch({ items, project_id }) {
|
|
|
738
1080
|
return toContent(result);
|
|
739
1081
|
}
|
|
740
1082
|
|
|
741
|
-
async function knowledgeIngestionJobs() {
|
|
742
|
-
const
|
|
1083
|
+
async function knowledgeIngestionJobs({ limit, offset, since_days } = {}) {
|
|
1084
|
+
const params = new URLSearchParams();
|
|
1085
|
+
if (limit != null) params.set("limit", String(limit));
|
|
1086
|
+
if (offset != null) params.set("offset", String(offset));
|
|
1087
|
+
if (since_days != null) params.set("since_days", String(since_days));
|
|
1088
|
+
const query = params.toString() ? `?${params}` : "";
|
|
1089
|
+
const result = await apiCall(
|
|
1090
|
+
"GET",
|
|
1091
|
+
`/api/v1/knowledge/ingestion-jobs${query}`,
|
|
1092
|
+
null,
|
|
1093
|
+
process.env.LOOPCTL_ORCH_KEY,
|
|
1094
|
+
);
|
|
743
1095
|
return toContent(result);
|
|
744
1096
|
}
|
|
745
1097
|
|
|
@@ -852,12 +1204,18 @@ async function knowledgeExport({ project_id }) {
|
|
|
852
1204
|
? `/api/v1/projects/${project_id}/knowledge/export`
|
|
853
1205
|
: "/api/v1/knowledge/export";
|
|
854
1206
|
const baseUrl = getBaseUrl();
|
|
855
|
-
|
|
1207
|
+
// The export endpoint is role:user — use LOOPCTL_USER_KEY. (LOOPCTL_ORCH_KEY is
|
|
1208
|
+
// orchestrator role, which is BELOW user in the hierarchy and would 403.) The
|
|
1209
|
+
// endpoint streams a gzipped tar (`application/gzip`), so save it as `.tar.gz`.
|
|
1210
|
+
const downloadCmd = `curl -H "Authorization: Bearer $LOOPCTL_USER_KEY" "${baseUrl}${basePath}" -o knowledge-export.tar.gz`;
|
|
856
1211
|
return {
|
|
857
1212
|
content: [{
|
|
858
1213
|
type: "text",
|
|
859
1214
|
text: JSON.stringify({
|
|
860
|
-
message:
|
|
1215
|
+
message:
|
|
1216
|
+
"Knowledge export streams a bounded-memory gzipped tar (.tar.gz). Use the curl " +
|
|
1217
|
+
"command below to download it directly. Requires a user-role key " +
|
|
1218
|
+
"(LOOPCTL_USER_KEY); extract with `tar -xzf knowledge-export.tar.gz`.",
|
|
861
1219
|
command: downloadCmd,
|
|
862
1220
|
endpoint: `${baseUrl}${basePath}`,
|
|
863
1221
|
}, null, 2),
|
|
@@ -865,6 +1223,136 @@ async function knowledgeExport({ project_id }) {
|
|
|
865
1223
|
};
|
|
866
1224
|
}
|
|
867
1225
|
|
|
1226
|
+
// --- OKF (Open Knowledge Format) interchange ---
|
|
1227
|
+
|
|
1228
|
+
// Guard a user-supplied absolute filesystem path against pseudo-filesystems.
|
|
1229
|
+
function assertSafeAbsolutePath(nodePath, p, label) {
|
|
1230
|
+
if (!nodePath.isAbsolute(p)) {
|
|
1231
|
+
return `${label} must be an absolute path (got '${p}').`;
|
|
1232
|
+
}
|
|
1233
|
+
const blocked = ["/proc", "/dev", "/sys"];
|
|
1234
|
+
if (blocked.some((b) => p === b || p.startsWith(b + "/"))) {
|
|
1235
|
+
return `${label} refused: '${p}' targets a pseudo-filesystem path.`;
|
|
1236
|
+
}
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async function knowledgeOkfExport({ project_id, out_dir }) {
|
|
1241
|
+
const basePath = project_id
|
|
1242
|
+
? `/api/v1/projects/${project_id}/knowledge/okf/export`
|
|
1243
|
+
: "/api/v1/knowledge/okf/export";
|
|
1244
|
+
|
|
1245
|
+
const result = await apiCall(
|
|
1246
|
+
"GET",
|
|
1247
|
+
`${basePath}?format=json`,
|
|
1248
|
+
null,
|
|
1249
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
if (result.error) return toContent(result);
|
|
1253
|
+
|
|
1254
|
+
const bundle = result.data || result;
|
|
1255
|
+
const files = bundle.files || {};
|
|
1256
|
+
const meta = bundle.meta || {};
|
|
1257
|
+
|
|
1258
|
+
if (!out_dir) {
|
|
1259
|
+
return toContent({ meta, file_count: Object.keys(files).length, files });
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const nodePath = await import("node:path");
|
|
1263
|
+
const pathErr = assertSafeAbsolutePath(nodePath, out_dir, "out_dir");
|
|
1264
|
+
if (pathErr) {
|
|
1265
|
+
return { content: [{ type: "text", text: `Error: ${pathErr}` }], isError: true };
|
|
1266
|
+
}
|
|
1267
|
+
// Normalize so `..`/trailing-slash can't defeat the per-file fence below.
|
|
1268
|
+
const root = nodePath.resolve(out_dir);
|
|
1269
|
+
|
|
1270
|
+
const fs = await import("node:fs/promises");
|
|
1271
|
+
let written = 0;
|
|
1272
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
1273
|
+
// Keep writes inside root even if a server-supplied path is adversarial.
|
|
1274
|
+
const dest = nodePath.resolve(root, rel);
|
|
1275
|
+
if (dest !== root && !dest.startsWith(root + nodePath.sep)) continue;
|
|
1276
|
+
await fs.mkdir(nodePath.dirname(dest), { recursive: true });
|
|
1277
|
+
await fs.writeFile(dest, content, "utf8");
|
|
1278
|
+
written += 1;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return toContent({ meta, out_dir: root, written });
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async function knowledgeOkfImport({ bundle_dir, project_id, merge, dry_run }) {
|
|
1285
|
+
const nodePath = await import("node:path");
|
|
1286
|
+
const pathErr = assertSafeAbsolutePath(nodePath, bundle_dir, "bundle_dir");
|
|
1287
|
+
if (pathErr) {
|
|
1288
|
+
return { content: [{ type: "text", text: `Error: ${pathErr}` }], isError: true };
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const fs = await import("node:fs/promises");
|
|
1292
|
+
|
|
1293
|
+
let stat;
|
|
1294
|
+
try {
|
|
1295
|
+
stat = await fs.stat(bundle_dir);
|
|
1296
|
+
} catch {
|
|
1297
|
+
return { content: [{ type: "text", text: `Error: bundle_dir '${bundle_dir}' not found.` }], isError: true };
|
|
1298
|
+
}
|
|
1299
|
+
if (!stat.isDirectory()) {
|
|
1300
|
+
return { content: [{ type: "text", text: `Error: bundle_dir '${bundle_dir}' is not a directory.` }], isError: true };
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Kept in step with the server-side caps in okf_controller.ex.
|
|
1304
|
+
const MAX_FILES = 10_000;
|
|
1305
|
+
const MAX_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
1306
|
+
const files = {};
|
|
1307
|
+
let totalBytes = 0;
|
|
1308
|
+
|
|
1309
|
+
async function walk(dir) {
|
|
1310
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1311
|
+
for (const entry of entries) {
|
|
1312
|
+
const abs = nodePath.join(dir, entry.name);
|
|
1313
|
+
// Never traverse or read through symlinks (escape out of bundle_dir).
|
|
1314
|
+
if (entry.isSymbolicLink()) {
|
|
1315
|
+
continue;
|
|
1316
|
+
} else if (entry.isDirectory()) {
|
|
1317
|
+
await walk(abs);
|
|
1318
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1319
|
+
if (Object.keys(files).length >= MAX_FILES) {
|
|
1320
|
+
throw new Error(`bundle exceeds ${MAX_FILES} files`);
|
|
1321
|
+
}
|
|
1322
|
+
const content = await fs.readFile(abs, "utf8");
|
|
1323
|
+
totalBytes += Buffer.byteLength(content, "utf8");
|
|
1324
|
+
if (totalBytes > MAX_TOTAL_BYTES) {
|
|
1325
|
+
throw new Error("bundle exceeds 25 MiB total");
|
|
1326
|
+
}
|
|
1327
|
+
files[nodePath.relative(bundle_dir, abs).split(nodePath.sep).join("/")] = content;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
try {
|
|
1333
|
+
await walk(bundle_dir);
|
|
1334
|
+
} catch (e) {
|
|
1335
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (Object.keys(files).length === 0) {
|
|
1339
|
+
return { content: [{ type: "text", text: `Error: no .md files found under '${bundle_dir}'.` }], isError: true };
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const payload = { files };
|
|
1343
|
+
if (project_id) payload.project_id = project_id;
|
|
1344
|
+
if (merge != null) payload.merge = merge;
|
|
1345
|
+
if (dry_run != null) payload.dry_run = dry_run;
|
|
1346
|
+
|
|
1347
|
+
const result = await apiCall(
|
|
1348
|
+
"POST",
|
|
1349
|
+
"/api/v1/knowledge/okf/import",
|
|
1350
|
+
payload,
|
|
1351
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1352
|
+
);
|
|
1353
|
+
return toContent(result);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
868
1356
|
// --- Discovery Tools ---
|
|
869
1357
|
|
|
870
1358
|
async function listRoutes() {
|
|
@@ -934,10 +1422,15 @@ const TOOLS = [
|
|
|
934
1422
|
},
|
|
935
1423
|
{
|
|
936
1424
|
name: "list_projects",
|
|
937
|
-
description:
|
|
1425
|
+
description:
|
|
1426
|
+
"List projects in the current tenant. Paginated (page/page_size); advance " +
|
|
1427
|
+
"`page` to enumerate all projects. Response includes pagination meta.",
|
|
938
1428
|
inputSchema: {
|
|
939
1429
|
type: "object",
|
|
940
|
-
properties: {
|
|
1430
|
+
properties: {
|
|
1431
|
+
page: { type: "integer", description: "Page number (default 1)." },
|
|
1432
|
+
page_size: { type: "integer", description: "Items per page (default 20)." },
|
|
1433
|
+
},
|
|
941
1434
|
required: [],
|
|
942
1435
|
},
|
|
943
1436
|
},
|
|
@@ -1098,8 +1591,8 @@ const TOOLS = [
|
|
|
1098
1591
|
description:
|
|
1099
1592
|
"List stories for a project, optionally filtered by agent_status, verified_status, or epic_id. " +
|
|
1100
1593
|
"Returns compact results (no acceptance_criteria/description) — use get_story for full details. " +
|
|
1101
|
-
"
|
|
1102
|
-
"Filter by epic_id or agent_status to reduce result size.",
|
|
1594
|
+
"Defaults to 20 per page; pass `limit` up to 500 to page larger. Use offset to paginate " +
|
|
1595
|
+
"(response includes total_count). Filter by epic_id or agent_status to reduce result size.",
|
|
1103
1596
|
inputSchema: {
|
|
1104
1597
|
type: "object",
|
|
1105
1598
|
properties: {
|
|
@@ -1122,7 +1615,7 @@ const TOOLS = [
|
|
|
1122
1615
|
},
|
|
1123
1616
|
limit: {
|
|
1124
1617
|
type: "integer",
|
|
1125
|
-
description: "Maximum number of stories to return.",
|
|
1618
|
+
description: "Maximum number of stories to return (default 20, max 500).",
|
|
1126
1619
|
},
|
|
1127
1620
|
offset: {
|
|
1128
1621
|
type: "integer",
|
|
@@ -1141,7 +1634,8 @@ const TOOLS = [
|
|
|
1141
1634
|
description:
|
|
1142
1635
|
"List stories that are ready to be worked on (contracted, dependencies met). " +
|
|
1143
1636
|
"Returns compact results — use get_story for full details. " +
|
|
1144
|
-
"
|
|
1637
|
+
"Paginated (page/page_size); advance `page` to enumerate all ready stories. " +
|
|
1638
|
+
"Response includes total_count.",
|
|
1145
1639
|
inputSchema: {
|
|
1146
1640
|
type: "object",
|
|
1147
1641
|
properties: {
|
|
@@ -1149,9 +1643,10 @@ const TOOLS = [
|
|
|
1149
1643
|
type: "string",
|
|
1150
1644
|
description: "The UUID of the project.",
|
|
1151
1645
|
},
|
|
1152
|
-
|
|
1646
|
+
page: { type: "integer", description: "Page number (default 1)." },
|
|
1647
|
+
page_size: {
|
|
1153
1648
|
type: "integer",
|
|
1154
|
-
description: "
|
|
1649
|
+
description: "Stories per page (default 100, max 500).",
|
|
1155
1650
|
},
|
|
1156
1651
|
},
|
|
1157
1652
|
required: ["project_id"],
|
|
@@ -1496,7 +1991,9 @@ const TOOLS = [
|
|
|
1496
1991
|
},
|
|
1497
1992
|
{
|
|
1498
1993
|
name: "get_story_token_usage",
|
|
1499
|
-
description:
|
|
1994
|
+
description:
|
|
1995
|
+
"Get token usage records for a single story. Paginated (page/page_size); " +
|
|
1996
|
+
"advance `page` to enumerate all records.",
|
|
1500
1997
|
inputSchema: {
|
|
1501
1998
|
type: "object",
|
|
1502
1999
|
properties: {
|
|
@@ -1504,6 +2001,8 @@ const TOOLS = [
|
|
|
1504
2001
|
type: "string",
|
|
1505
2002
|
description: "The UUID of the story.",
|
|
1506
2003
|
},
|
|
2004
|
+
page: { type: "integer", description: "Page number (default 1)." },
|
|
2005
|
+
page_size: { type: "integer", description: "Records per page (default 20)." },
|
|
1507
2006
|
},
|
|
1508
2007
|
required: ["story_id"],
|
|
1509
2008
|
},
|
|
@@ -1512,7 +2011,8 @@ const TOOLS = [
|
|
|
1512
2011
|
name: "get_cost_anomalies",
|
|
1513
2012
|
description:
|
|
1514
2013
|
"Get cost anomaly alerts — stories or agents that exceed expected token budgets. " +
|
|
1515
|
-
"Optionally filter by project."
|
|
2014
|
+
"Optionally filter by project. Paginated (page/page_size); advance `page` to " +
|
|
2015
|
+
"enumerate all anomalies.",
|
|
1516
2016
|
inputSchema: {
|
|
1517
2017
|
type: "object",
|
|
1518
2018
|
properties: {
|
|
@@ -1520,6 +2020,8 @@ const TOOLS = [
|
|
|
1520
2020
|
type: "string",
|
|
1521
2021
|
description: "Optional: filter anomalies to a specific project UUID.",
|
|
1522
2022
|
},
|
|
2023
|
+
page: { type: "integer", description: "Page number (default 1)." },
|
|
2024
|
+
page_size: { type: "integer", description: "Anomalies per page (default 20)." },
|
|
1523
2025
|
},
|
|
1524
2026
|
required: [],
|
|
1525
2027
|
},
|
|
@@ -1560,7 +2062,13 @@ const TOOLS = [
|
|
|
1560
2062
|
{
|
|
1561
2063
|
name: "knowledge_index",
|
|
1562
2064
|
description:
|
|
1563
|
-
"
|
|
2065
|
+
"Browse/paginate the knowledge wiki catalog. Returns LIGHTWEIGHT article metadata grouped by " +
|
|
2066
|
+
"category — by default only id, title, category per article (NOT full metadata). " +
|
|
2067
|
+
"Honors category/tags filters and offset/limit pagination with deterministic ordering, so " +
|
|
2068
|
+
"every article is reachable (meta.categories reports per-category counts over the whole " +
|
|
2069
|
+
"filtered set; meta.has_more/meta.truncated signal more pages). Use `fields` to control the " +
|
|
2070
|
+
"projection (request tags/status/updated_at explicitly; id and category are always included) " +
|
|
2071
|
+
"to keep the payload small on large catalogs. " +
|
|
1564
2072
|
"Pass story_id when working on a loopctl story so reads attribute correctly.",
|
|
1565
2073
|
inputSchema: {
|
|
1566
2074
|
type: "object",
|
|
@@ -1575,51 +2083,395 @@ const TOOLS = [
|
|
|
1575
2083
|
format: "uuid",
|
|
1576
2084
|
description: "Optional: loopctl story UUID for attribution tracking.",
|
|
1577
2085
|
},
|
|
2086
|
+
category: {
|
|
2087
|
+
type: "string",
|
|
2088
|
+
enum: ["pattern", "convention", "decision", "finding", "reference"],
|
|
2089
|
+
description: "Optional: filter to a single category. Rejected (400) if unknown.",
|
|
2090
|
+
},
|
|
2091
|
+
tags: {
|
|
2092
|
+
type: "string",
|
|
2093
|
+
description: "Optional: comma-separated tags; matches articles carrying ANY of them.",
|
|
2094
|
+
},
|
|
2095
|
+
match: {
|
|
2096
|
+
type: "string",
|
|
2097
|
+
enum: ["any", "all"],
|
|
2098
|
+
description: "Optional: tag match mode — 'any' (default, OR) or 'all' (AND, every tag).",
|
|
2099
|
+
},
|
|
2100
|
+
offset: {
|
|
2101
|
+
type: "integer",
|
|
2102
|
+
description: "Optional: rows to skip for pagination (default 0).",
|
|
2103
|
+
},
|
|
2104
|
+
limit: {
|
|
2105
|
+
type: "integer",
|
|
2106
|
+
description: "Optional: max articles per page (default 1000, max 1000).",
|
|
2107
|
+
},
|
|
2108
|
+
fields: {
|
|
2109
|
+
type: "array",
|
|
2110
|
+
items: {
|
|
2111
|
+
type: "string",
|
|
2112
|
+
enum: ["id", "title", "category", "tags", "status", "updated_at"],
|
|
2113
|
+
},
|
|
2114
|
+
description:
|
|
2115
|
+
"Optional: projection of article fields to return. Default: id, title, category. `id` is always included.",
|
|
2116
|
+
},
|
|
1578
2117
|
},
|
|
1579
2118
|
required: [],
|
|
1580
2119
|
},
|
|
1581
2120
|
},
|
|
1582
2121
|
{
|
|
1583
|
-
name: "
|
|
2122
|
+
name: "knowledge_list",
|
|
1584
2123
|
description:
|
|
1585
|
-
"
|
|
1586
|
-
"
|
|
2124
|
+
"List articles (id, title, category, status, tags, source_type, source_id, " +
|
|
2125
|
+
"idempotency_key, timestamps), filtered and paginated. **Body-less summary by default** " +
|
|
2126
|
+
"— the right tool to enumerate, dedup, or repair at scale (safe to page up to limit=1000). " +
|
|
2127
|
+
"Pass `include_body: true` to also return the full `body`, in which case the server bounds " +
|
|
2128
|
+
"the page by a ~5 MB serialized-body budget and returns meta.next_offset/has_more/" +
|
|
2129
|
+
"byte_truncated for continuation (for a single full body use knowledge_get; for the relevant " +
|
|
2130
|
+
"bodies use knowledge_context; for a bulk content dump use knowledge_export). Unlike " +
|
|
2131
|
+
"knowledge_search (ranked, PUBLISHED-only, lags writes while embeddings index) and " +
|
|
2132
|
+
"knowledge_index (id/title/category only), this is the LAG-FREE, ALL-STATUS read of the DB " +
|
|
2133
|
+
"of record (draft, published, archived, superseded visible). Use for idempotency/existence " +
|
|
2134
|
+
"checks: filter by `tags`, `source_type`+`source_id`, or `idempotency_key` and read " +
|
|
2135
|
+
"`meta.total_count` (exact) to answer \"does an article for X already exist?\" reliably " +
|
|
2136
|
+
"right after a write. Paginate via offset/limit.",
|
|
1587
2137
|
inputSchema: {
|
|
1588
2138
|
type: "object",
|
|
1589
2139
|
properties: {
|
|
1590
|
-
q: {
|
|
1591
|
-
type: "string",
|
|
1592
|
-
description: "Search query string.",
|
|
1593
|
-
},
|
|
1594
2140
|
project_id: {
|
|
1595
2141
|
type: "string",
|
|
1596
2142
|
format: "uuid",
|
|
1597
|
-
description: "Optional: scope
|
|
2143
|
+
description: "Optional: scope to a project UUID.",
|
|
1598
2144
|
},
|
|
1599
|
-
|
|
2145
|
+
category: {
|
|
1600
2146
|
type: "string",
|
|
1601
|
-
|
|
1602
|
-
description: "Optional: loopctl story UUID for attribution tracking.",
|
|
2147
|
+
description: "Optional: filter by category.",
|
|
1603
2148
|
},
|
|
1604
|
-
|
|
2149
|
+
status: {
|
|
1605
2150
|
type: "string",
|
|
1606
|
-
description: "Optional: filter
|
|
2151
|
+
description: "Optional: filter by status (draft, published, archived, superseded).",
|
|
1607
2152
|
},
|
|
1608
2153
|
tags: {
|
|
1609
2154
|
type: "string",
|
|
1610
|
-
description: "Optional: comma-separated tags
|
|
2155
|
+
description: "Optional: comma-separated tags; matches articles carrying ANY of them.",
|
|
1611
2156
|
},
|
|
1612
|
-
|
|
2157
|
+
match: {
|
|
1613
2158
|
type: "string",
|
|
1614
|
-
enum: ["
|
|
1615
|
-
description: "Optional:
|
|
2159
|
+
enum: ["any", "all"],
|
|
2160
|
+
description: "Optional: tag match mode — 'any' (default, OR) or 'all' (AND, every tag).",
|
|
2161
|
+
},
|
|
2162
|
+
source_type: {
|
|
2163
|
+
type: "string",
|
|
2164
|
+
description: "Optional: filter by source_type.",
|
|
2165
|
+
},
|
|
2166
|
+
source_id: {
|
|
2167
|
+
type: "string",
|
|
2168
|
+
description: "Optional: filter by source_id (a malformed id matches nothing).",
|
|
2169
|
+
},
|
|
2170
|
+
idempotency_key: {
|
|
2171
|
+
type: "string",
|
|
2172
|
+
description:
|
|
2173
|
+
"Optional: filter by exact idempotency_key — the lag-free existence check for a " +
|
|
2174
|
+
"prior capture.",
|
|
2175
|
+
},
|
|
2176
|
+
offset: {
|
|
2177
|
+
type: "integer",
|
|
2178
|
+
description: "Optional: rows to skip for pagination (default 0).",
|
|
1616
2179
|
},
|
|
1617
2180
|
limit: {
|
|
1618
2181
|
type: "integer",
|
|
1619
|
-
description:
|
|
2182
|
+
description:
|
|
2183
|
+
"Optional: max articles per page (default 20, max 1000). A limit above " +
|
|
2184
|
+
"the max is rejected with 400 — not silently clamped — so paging by offset " +
|
|
2185
|
+
"over meta.total_count enumerates the complete set without skipping rows.",
|
|
1620
2186
|
},
|
|
2187
|
+
include_body: {
|
|
2188
|
+
type: "boolean",
|
|
2189
|
+
description:
|
|
2190
|
+
"Optional (default false): when true, include the full article `body`. The page is " +
|
|
2191
|
+
"then bounded by a ~5 MB serialized-body budget (it may return fewer than `limit` " +
|
|
2192
|
+
"rows); continue via meta.next_offset while meta.has_more is true. Leave false to " +
|
|
2193
|
+
"enumerate metadata cheaply at scale.",
|
|
2194
|
+
},
|
|
2195
|
+
},
|
|
2196
|
+
required: [],
|
|
2197
|
+
},
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
name: "knowledge_stats",
|
|
2201
|
+
description:
|
|
2202
|
+
"Get aggregate article counts for the wiki without pulling any article metadata. " +
|
|
2203
|
+
"Returns { total, by_category, by_status } via cheap COUNT(*) GROUP BY. This is the " +
|
|
2204
|
+
"right tool to answer \"how many articles are in this project?\" — knowledge_index " +
|
|
2205
|
+
"pages article metadata and knowledge_search's total_count is query-dependent. Counts " +
|
|
2206
|
+
"span all statuses (draft/published/archived/superseded); see by_status for the split. " +
|
|
2207
|
+
"Note: `total` is NOT the same as knowledge_index's meta.total_count (which counts only " +
|
|
2208
|
+
"published) — they differ whenever drafts/archived exist.",
|
|
2209
|
+
inputSchema: {
|
|
2210
|
+
type: "object",
|
|
2211
|
+
properties: {
|
|
2212
|
+
project_id: {
|
|
2213
|
+
type: "string",
|
|
2214
|
+
format: "uuid",
|
|
2215
|
+
description:
|
|
2216
|
+
"Optional: scope counts to a project (counts both tenant-wide and project-specific articles).",
|
|
2217
|
+
},
|
|
2218
|
+
},
|
|
2219
|
+
required: [],
|
|
2220
|
+
},
|
|
2221
|
+
},
|
|
2222
|
+
{
|
|
2223
|
+
name: "knowledge_count",
|
|
2224
|
+
description:
|
|
2225
|
+
"Count articles matching filters WITHOUT returning any rows. Accepts the same filters " +
|
|
2226
|
+
"as knowledge_list (category, status, tags, match, source_type, source_id, " +
|
|
2227
|
+
"idempotency_key, project_id). With tags + match:'all' it counts articles carrying ALL " +
|
|
2228
|
+
"the tags; add status:'published' to answer \"how many PUBLISHED articles tagged both X " +
|
|
2229
|
+
"and Y\". Removes the need to paginate rows just to count. Returns { count }.",
|
|
2230
|
+
inputSchema: {
|
|
2231
|
+
type: "object",
|
|
2232
|
+
properties: {
|
|
2233
|
+
project_id: { type: "string", format: "uuid", description: "Optional: scope to a project UUID." },
|
|
2234
|
+
category: { type: "string", description: "Optional: filter by category." },
|
|
2235
|
+
status: { type: "string", description: "Optional: filter by status." },
|
|
2236
|
+
tags: { type: "string", description: "Optional: comma-separated tags." },
|
|
2237
|
+
match: {
|
|
2238
|
+
type: "string",
|
|
2239
|
+
enum: ["any", "all"],
|
|
2240
|
+
description: "Tag match mode: 'any' (default, OR) or 'all' (AND — carries every tag).",
|
|
2241
|
+
},
|
|
2242
|
+
source_type: { type: "string", description: "Optional: filter by source_type." },
|
|
2243
|
+
source_id: { type: "string", description: "Optional: filter by source_id." },
|
|
2244
|
+
idempotency_key: { type: "string", description: "Optional: filter by idempotency_key." },
|
|
2245
|
+
},
|
|
2246
|
+
required: [],
|
|
2247
|
+
},
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
name: "knowledge_facets",
|
|
2251
|
+
description:
|
|
2252
|
+
"Count articles grouped by each distinct tag, over the filtered set, WITHOUT returning " +
|
|
2253
|
+
"rows. Returns { data: { tag: count }, meta: { distinct_count, truncated } }. " +
|
|
2254
|
+
"meta.distinct_count is the TRUE number of distinct tags (independent of limit); " +
|
|
2255
|
+
"meta.truncated flags when limit capped the rows. Each `count` is the number of distinct " +
|
|
2256
|
+
"articles carrying that tag. Pass tag_prefix to restrict to a tag family (e.g. 'book-') " +
|
|
2257
|
+
"so you get the DISTINCT count of that family (how many distinct books) plus per-member " +
|
|
2258
|
+
"totals — without dragging tens of thousands of rows through context. Honors the same " +
|
|
2259
|
+
"filters as knowledge_count (status, tags, match). Cost: unnests tags over the whole " +
|
|
2260
|
+
"filtered set; on large tenants narrow with tag_prefix/category/status/project_id.",
|
|
2261
|
+
inputSchema: {
|
|
2262
|
+
type: "object",
|
|
2263
|
+
properties: {
|
|
2264
|
+
project_id: { type: "string", format: "uuid", description: "Optional: scope to a project UUID." },
|
|
2265
|
+
category: { type: "string", description: "Optional: filter by category." },
|
|
2266
|
+
status: { type: "string", description: "Optional: filter by status." },
|
|
2267
|
+
tags: { type: "string", description: "Optional: comma-separated tags." },
|
|
2268
|
+
match: {
|
|
2269
|
+
type: "string",
|
|
2270
|
+
enum: ["any", "all"],
|
|
2271
|
+
description: "Tag match mode: 'any' (default, OR) or 'all' (AND).",
|
|
2272
|
+
},
|
|
2273
|
+
tag_prefix: {
|
|
2274
|
+
type: "string",
|
|
2275
|
+
description: "Optional: only tags starting with this literal prefix (e.g. 'book-').",
|
|
2276
|
+
},
|
|
2277
|
+
limit: { type: "integer", description: "Optional: max distinct tags in the result (default all, max 1000; values above 1000 are rejected with 400)." },
|
|
2278
|
+
},
|
|
2279
|
+
required: [],
|
|
2280
|
+
},
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
name: "knowledge_graph",
|
|
2284
|
+
description:
|
|
2285
|
+
"Traverse the published article-link graph outward from an article, up to `depth` hops " +
|
|
2286
|
+
"(1–3, default 1). BIDIRECTIONAL (follows links regardless of source/target direction) and " +
|
|
2287
|
+
"cycle-safe (no node twice). Returns { nodes: [{id,title,category,depth}], edges: " +
|
|
2288
|
+
"[{source_article_id,target_article_id,relationship_type}], truncated, node_count }. Bounded " +
|
|
2289
|
+
"to 100 nodes / 500 edges (truncated:true when hit). Use to explore how a piece of knowledge " +
|
|
2290
|
+
"connects (relates_to/derived_from/contradicts/supersedes) beyond the 1-hop links in " +
|
|
2291
|
+
"knowledge_context.",
|
|
2292
|
+
inputSchema: {
|
|
2293
|
+
type: "object",
|
|
2294
|
+
properties: {
|
|
2295
|
+
article_id: {
|
|
2296
|
+
type: "string",
|
|
2297
|
+
format: "uuid",
|
|
2298
|
+
description: "Starting article UUID (required).",
|
|
2299
|
+
},
|
|
2300
|
+
depth: {
|
|
2301
|
+
type: "integer",
|
|
2302
|
+
minimum: 1,
|
|
2303
|
+
maximum: 3,
|
|
2304
|
+
description: "Hops to traverse (1–3, default 1). Out of range → 400.",
|
|
2305
|
+
},
|
|
2306
|
+
project_id: { type: "string", format: "uuid", description: "Optional: project UUID." },
|
|
2307
|
+
},
|
|
2308
|
+
required: ["article_id"],
|
|
2309
|
+
},
|
|
2310
|
+
},
|
|
2311
|
+
{
|
|
2312
|
+
name: "knowledge_suggest_links",
|
|
2313
|
+
description:
|
|
2314
|
+
"Suggest ranked typed-link CANDIDATES for an article by embedding similarity — " +
|
|
2315
|
+
"READ-ONLY, creates nothing. Excludes the article itself and any already-linked " +
|
|
2316
|
+
"article (either direction, any relationship type); only embedded published articles. " +
|
|
2317
|
+
"Returns { data: [{id, title, category, similarity_score}] } highest-similarity first. " +
|
|
2318
|
+
"Review them and create the one you want as a TYPED link (relates_to/derived_from/" +
|
|
2319
|
+
"contradicts/supersedes) — unlike the auto-linker which only makes ambient relates_to. " +
|
|
2320
|
+
"Optional: threshold (cosine floor 0–1, default 0.5), limit (default 5).",
|
|
2321
|
+
inputSchema: {
|
|
2322
|
+
type: "object",
|
|
2323
|
+
properties: {
|
|
2324
|
+
article_id: {
|
|
2325
|
+
type: "string",
|
|
2326
|
+
format: "uuid",
|
|
2327
|
+
description: "The article to suggest links for (required).",
|
|
2328
|
+
},
|
|
2329
|
+
limit: { type: "integer", description: "Max candidates (default 5)." },
|
|
2330
|
+
threshold: {
|
|
2331
|
+
type: "number",
|
|
2332
|
+
minimum: 0,
|
|
2333
|
+
maximum: 1,
|
|
2334
|
+
description: "Cosine similarity floor (default 0.5).",
|
|
2335
|
+
},
|
|
2336
|
+
},
|
|
2337
|
+
required: ["article_id"],
|
|
2338
|
+
},
|
|
2339
|
+
},
|
|
2340
|
+
{
|
|
2341
|
+
name: "knowledge_distant_pairs",
|
|
2342
|
+
description:
|
|
2343
|
+
"Find distant-but-bridgeable article pairs in the optimal-novelty embedding band " +
|
|
2344
|
+
"(cosine distance min..max, default 0.3–0.7) — the creative sweet spot (neither banal " +
|
|
2345
|
+
"nor nonsense). Returns { data: [{a, b, distance}], meta:{count} }. With bridge_path:true, " +
|
|
2346
|
+
"only pairs also connected in the link graph (≤2 hops) are returned. Samples up to 1000 " +
|
|
2347
|
+
"embedded published articles; paginate via limit/offset. For computational-creativity " +
|
|
2348
|
+
"ideation (remote-associates generator).",
|
|
2349
|
+
inputSchema: {
|
|
2350
|
+
type: "object",
|
|
2351
|
+
properties: {
|
|
2352
|
+
min_distance: { type: "number", description: "Lower cosine-distance bound (default 0.3)." },
|
|
2353
|
+
max_distance: { type: "number", description: "Upper cosine-distance bound (default 0.7)." },
|
|
2354
|
+
bridge_path: { type: "boolean", description: "Require a ≤2-hop graph path (default false)." },
|
|
2355
|
+
limit: { type: "integer", description: "Max pairs (default 20, max 100)." },
|
|
2356
|
+
offset: { type: "integer", description: "Pairs to skip." },
|
|
1621
2357
|
},
|
|
1622
|
-
required: [
|
|
2358
|
+
required: [],
|
|
2359
|
+
},
|
|
2360
|
+
},
|
|
2361
|
+
{
|
|
2362
|
+
name: "knowledge_novelty",
|
|
2363
|
+
description:
|
|
2364
|
+
"Score the NOVELTY of ideas: each idea's text is embedded and compared to the nearest " +
|
|
2365
|
+
"prior proposal, returning novelty_score = cosine distance (0 = identical to existing " +
|
|
2366
|
+
"work, higher = more novel, up to 2.0; null when the idea text is blank, no priors " +
|
|
2367
|
+
"exist, or embedding fails). Priors default to published articles tagged 'proposal' " +
|
|
2368
|
+
"(override with prior_tag). Provide the ideas as EITHER `texts` (a list of strings) " +
|
|
2369
|
+
"OR `ideas` (a list of strings or objects {text|title/spark/thesis,...}); all forms " +
|
|
2370
|
+
"are accepted. " +
|
|
2371
|
+
"Returns { data: [{...idea, novelty_score}], meta: { prior_count } }. Use to rerank " +
|
|
2372
|
+
"generated ideas by novelty × value and avoid repeating prior work.",
|
|
2373
|
+
inputSchema: {
|
|
2374
|
+
type: "object",
|
|
2375
|
+
properties: {
|
|
2376
|
+
ideas: {
|
|
2377
|
+
type: "array",
|
|
2378
|
+
description:
|
|
2379
|
+
"Ideas to score (≤50). Each is a string, or an object whose embed text is " +
|
|
2380
|
+
"`text` (else title/spark/thesis are joined).",
|
|
2381
|
+
items: { type: ["string", "object"] },
|
|
2382
|
+
},
|
|
2383
|
+
texts: {
|
|
2384
|
+
type: "array",
|
|
2385
|
+
description: "Alternative to `ideas`: a list of idea strings (the #152 AC shape).",
|
|
2386
|
+
items: { type: "string" },
|
|
2387
|
+
},
|
|
2388
|
+
prior_tag: {
|
|
2389
|
+
type: "string",
|
|
2390
|
+
description: "Tag identifying prior proposals to compare against (default 'proposal').",
|
|
2391
|
+
},
|
|
2392
|
+
},
|
|
2393
|
+
required: [],
|
|
2394
|
+
},
|
|
2395
|
+
},
|
|
2396
|
+
{
|
|
2397
|
+
name: "knowledge_random_walk",
|
|
2398
|
+
description:
|
|
2399
|
+
"Random walk through the link graph from a starting article (up to `length` published " +
|
|
2400
|
+
"nodes, no cycles, stops at a dead end). Surfaces unexpected connections for creative " +
|
|
2401
|
+
"incubation. Returns { data: [{id,title,category}], meta:{count} } in walk order.",
|
|
2402
|
+
inputSchema: {
|
|
2403
|
+
type: "object",
|
|
2404
|
+
properties: {
|
|
2405
|
+
start_id: { type: "string", format: "uuid", description: "Starting article UUID (required)." },
|
|
2406
|
+
length: { type: "integer", description: "Walk steps (default 4, max 25)." },
|
|
2407
|
+
},
|
|
2408
|
+
required: ["start_id"],
|
|
2409
|
+
},
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
name: "knowledge_search",
|
|
2413
|
+
description:
|
|
2414
|
+
"Search the knowledge wiki by topic. Returns snippets. Ranked, and returns PUBLISHED " +
|
|
2415
|
+
"articles only; it LAGS writes by minutes while embeddings index. Do NOT use it for " +
|
|
2416
|
+
"existence/idempotency/dedup checks ('already captured?') — a freshly-written article will " +
|
|
2417
|
+
"false-negative. Use knowledge_list (lag-free, all-status, exact meta.total_count) for that. " +
|
|
2418
|
+
"q is optional when tags and/or category are supplied: in that list mode it returns the " +
|
|
2419
|
+
"COMPLETE filtered set (no relevance ranking) paginated via offset/limit over " +
|
|
2420
|
+
"meta.total_count, so you can enumerate every article carrying a tag/category. " +
|
|
2421
|
+
"IMPORTANT: meta.total_count is mode-dependent — read meta.total_count_scope to know what " +
|
|
2422
|
+
"it counts: keyword_matches (stop-word-filtered tsquery matches; 'the' matches ~nothing), " +
|
|
2423
|
+
"ranked_corpus (semantic ranks all EMBEDDED published articles — that embedded set's size, " +
|
|
2424
|
+
"not a match count, and <= the published count), merged_candidates (combined: deduped UNION of " +
|
|
2425
|
+
"a keyword and a semantic sub-search, each capped at 100, so up to ~200), or filtered_set " +
|
|
2426
|
+
"(list mode: the full set). Do NOT use a relevance-mode total_count to size the wiki — use " +
|
|
2427
|
+
"list mode or knowledge_stats. " +
|
|
2428
|
+
"Pass story_id when working on a loopctl story so reads attribute correctly.",
|
|
2429
|
+
inputSchema: {
|
|
2430
|
+
type: "object",
|
|
2431
|
+
properties: {
|
|
2432
|
+
q: {
|
|
2433
|
+
type: "string",
|
|
2434
|
+
description:
|
|
2435
|
+
"Search query string. Optional when tags/category are supplied (enumeration mode).",
|
|
2436
|
+
},
|
|
2437
|
+
project_id: {
|
|
2438
|
+
type: "string",
|
|
2439
|
+
format: "uuid",
|
|
2440
|
+
description: "Optional: scope search to a specific project UUID.",
|
|
2441
|
+
},
|
|
2442
|
+
story_id: {
|
|
2443
|
+
type: "string",
|
|
2444
|
+
format: "uuid",
|
|
2445
|
+
description: "Optional: loopctl story UUID for attribution tracking.",
|
|
2446
|
+
},
|
|
2447
|
+
category: {
|
|
2448
|
+
type: "string",
|
|
2449
|
+
description: "Optional: filter results by category.",
|
|
2450
|
+
},
|
|
2451
|
+
tags: {
|
|
2452
|
+
type: "string",
|
|
2453
|
+
description: "Optional: comma-separated tags to filter by.",
|
|
2454
|
+
},
|
|
2455
|
+
match: {
|
|
2456
|
+
type: "string",
|
|
2457
|
+
enum: ["any", "all"],
|
|
2458
|
+
description: "Optional: tag match mode — 'any' (default, OR) or 'all' (AND, every tag).",
|
|
2459
|
+
},
|
|
2460
|
+
mode: {
|
|
2461
|
+
type: "string",
|
|
2462
|
+
enum: ["keyword", "semantic", "combined"],
|
|
2463
|
+
description: "Optional: search mode (keyword, semantic, or combined).",
|
|
2464
|
+
},
|
|
2465
|
+
limit: {
|
|
2466
|
+
type: "integer",
|
|
2467
|
+
description: "Optional: maximum number of results to return.",
|
|
2468
|
+
},
|
|
2469
|
+
offset: {
|
|
2470
|
+
type: "integer",
|
|
2471
|
+
description: "Optional: results to skip for pagination (default 0).",
|
|
2472
|
+
},
|
|
2473
|
+
},
|
|
2474
|
+
required: [],
|
|
1623
2475
|
},
|
|
1624
2476
|
},
|
|
1625
2477
|
{
|
|
@@ -1653,7 +2505,11 @@ const TOOLS = [
|
|
|
1653
2505
|
name: "knowledge_context",
|
|
1654
2506
|
description:
|
|
1655
2507
|
"Get ranked full articles for a task query. Returns best knowledge with linked references. " +
|
|
1656
|
-
"Pass story_id when working on a loopctl story so reads attribute correctly."
|
|
2508
|
+
"Pass story_id when working on a loopctl story so reads attribute correctly. For agent " +
|
|
2509
|
+
"memory, scope to a memory_type/agent/conversation via the memory_types/agents/" +
|
|
2510
|
+
"conversation_id filters (articles whose metadata carries those keys). NOTE (#163): for " +
|
|
2511
|
+
"an agent key, another agent's private/owner memories are never returned (results AND " +
|
|
2512
|
+
"linked refs) regardless of the agents= filter — visibility is enforced, not advisory.",
|
|
1657
2513
|
inputSchema: {
|
|
1658
2514
|
type: "object",
|
|
1659
2515
|
properties: {
|
|
@@ -1681,6 +2537,20 @@ const TOOLS = [
|
|
|
1681
2537
|
minimum: 0,
|
|
1682
2538
|
maximum: 1,
|
|
1683
2539
|
},
|
|
2540
|
+
memory_types: {
|
|
2541
|
+
type: "string",
|
|
2542
|
+
description:
|
|
2543
|
+
"Optional agent-memory scope: comma-separated memory_types (OR) — " +
|
|
2544
|
+
"observation|finding|summary|decision|question|task.",
|
|
2545
|
+
},
|
|
2546
|
+
agents: {
|
|
2547
|
+
type: "string",
|
|
2548
|
+
description: "Optional agent-memory scope: comma-separated agent_ids (OR).",
|
|
2549
|
+
},
|
|
2550
|
+
conversation_id: {
|
|
2551
|
+
type: "string",
|
|
2552
|
+
description: "Optional agent-memory scope: exact conversation_id.",
|
|
2553
|
+
},
|
|
1684
2554
|
},
|
|
1685
2555
|
required: ["query"],
|
|
1686
2556
|
},
|
|
@@ -1689,7 +2559,15 @@ const TOOLS = [
|
|
|
1689
2559
|
name: "knowledge_create",
|
|
1690
2560
|
description:
|
|
1691
2561
|
"Create a new knowledge article. Use to file findings, document patterns, or record decisions " +
|
|
1692
|
-
"discovered during implementation."
|
|
2562
|
+
"discovered during implementation. Articles are PUBLISHED IMMEDIATELY by default and are visible " +
|
|
2563
|
+
"to agents (search/index/context) right away — no separate publish step is needed. Pass " +
|
|
2564
|
+
"draft: true to stage the article for later review instead; the response `note` says which " +
|
|
2565
|
+
"outcome occurred, and a draft can be published afterwards with knowledge_publish. " +
|
|
2566
|
+
"Concurrency-safe: if a create races/retries against an " +
|
|
2567
|
+
"existing article with the same title AND an identical body (ignoring surrounding whitespace), the " +
|
|
2568
|
+
"server returns that existing article idempotently (HTTP 200) instead of a 422. A same-title create " +
|
|
2569
|
+
"with a DIFFERENT body returns 409 title_conflict — do not retry; choose a different title or PATCH " +
|
|
2570
|
+
"the existing article.",
|
|
1693
2571
|
inputSchema: {
|
|
1694
2572
|
type: "object",
|
|
1695
2573
|
properties: {
|
|
@@ -1714,6 +2592,47 @@ const TOOLS = [
|
|
|
1714
2592
|
type: "string",
|
|
1715
2593
|
description: "Optional: associate the article with a project UUID.",
|
|
1716
2594
|
},
|
|
2595
|
+
draft: {
|
|
2596
|
+
type: "boolean",
|
|
2597
|
+
description:
|
|
2598
|
+
"Optional: stage as a draft instead of publishing on create (default false → " +
|
|
2599
|
+
"published immediately). Publish later with knowledge_publish.",
|
|
2600
|
+
},
|
|
2601
|
+
idempotency_key: {
|
|
2602
|
+
type: "string",
|
|
2603
|
+
description:
|
|
2604
|
+
"Optional: stable per-article key for idempotent capture (max 255). Re-creating " +
|
|
2605
|
+
"with the same key is a no-op that returns a reference to the existing article " +
|
|
2606
|
+
"(deduplicated; id only, not its body) instead of a partial duplicate. Use a " +
|
|
2607
|
+
"HIGH-ENTROPY value (e.g. a content hash) — it is a per-tenant lookup key, not a " +
|
|
2608
|
+
"secret, so a guessable key lets another agent in your tenant probe which keys " +
|
|
2609
|
+
"exist. Distinct from source_type/source_id (which mark a shared source).",
|
|
2610
|
+
},
|
|
2611
|
+
metadata: {
|
|
2612
|
+
type: "object",
|
|
2613
|
+
description:
|
|
2614
|
+
"Optional: extensible JSONB. Set the agent-memory keys to file this article as a " +
|
|
2615
|
+
"scoped agent memory: `memory_type` (observation/finding/summary/decision/question/" +
|
|
2616
|
+
"task) and `visibility` (shared | private | owner). TRUST MODEL (#163): for an " +
|
|
2617
|
+
"agent key, `metadata.agent_id` is stamped server-side from your verified key " +
|
|
2618
|
+
"identity — do NOT set it (any value you pass is overridden); `private`/`owner` " +
|
|
2619
|
+
"memories are then readable only by you (other agents get 404/exclusion across all " +
|
|
2620
|
+
"knowledge reads), while `shared` (the default) is visible tenant-wide. An agent " +
|
|
2621
|
+
"key with no agent identity gets 403 agent_identity_required when writing memory " +
|
|
2622
|
+
"metadata. Higher roles may attribute on behalf of others.",
|
|
2623
|
+
},
|
|
2624
|
+
source_type: {
|
|
2625
|
+
type: "string",
|
|
2626
|
+
description:
|
|
2627
|
+
"Optional: advisory provenance for the originating source (e.g. 'web_article', " +
|
|
2628
|
+
"'newsletter'). Shared across articles from the same source; not an idempotency key.",
|
|
2629
|
+
},
|
|
2630
|
+
source_id: {
|
|
2631
|
+
type: "string",
|
|
2632
|
+
description:
|
|
2633
|
+
"Optional: UUID of the originating source entity (shared across articles from " +
|
|
2634
|
+
"that source). Pair with source_type.",
|
|
2635
|
+
},
|
|
1717
2636
|
},
|
|
1718
2637
|
required: ["title", "body"],
|
|
1719
2638
|
},
|
|
@@ -1738,19 +2657,48 @@ const TOOLS = [
|
|
|
1738
2657
|
{
|
|
1739
2658
|
name: "knowledge_bulk_publish",
|
|
1740
2659
|
description:
|
|
1741
|
-
"
|
|
1742
|
-
"
|
|
1743
|
-
"
|
|
1744
|
-
"
|
|
1745
|
-
"
|
|
2660
|
+
"Publish draft articles, partial-success style. REQUIRES LOOPCTL_USER_KEY " +
|
|
2661
|
+
"(user role — orchestrator is NOT sufficient). Every valid draft is published; " +
|
|
2662
|
+
"each other id gets a per-id outcome instead of failing the whole call: " +
|
|
2663
|
+
"published, skipped (already published — idempotent — or archived/superseded), " +
|
|
2664
|
+
"not_found, or errored. Duplicate ids are de-duplicated and there is NO 100-id " +
|
|
2665
|
+
"cap (larger requests are auto-chunked server-side). The response's meta.count " +
|
|
2666
|
+
"is the number actually published; meta.counts has the full breakdown and " +
|
|
2667
|
+
"meta.results is the per-id list in request order. Safe to retry — already " +
|
|
2668
|
+
"published ids are skipped, not errored.",
|
|
1746
2669
|
inputSchema: {
|
|
1747
2670
|
type: "object",
|
|
1748
2671
|
properties: {
|
|
1749
2672
|
article_ids: {
|
|
1750
2673
|
type: "array",
|
|
1751
2674
|
items: { type: "string" },
|
|
1752
|
-
description:
|
|
1753
|
-
|
|
2675
|
+
description:
|
|
2676
|
+
"Article UUIDs to publish. Any length (auto-chunked); duplicates ignored.",
|
|
2677
|
+
},
|
|
2678
|
+
},
|
|
2679
|
+
required: ["article_ids"],
|
|
2680
|
+
},
|
|
2681
|
+
},
|
|
2682
|
+
{
|
|
2683
|
+
name: "knowledge_bulk_unpublish",
|
|
2684
|
+
description:
|
|
2685
|
+
"Unpublish (published → draft) articles in bulk, partial-success style — the mirror " +
|
|
2686
|
+
"of knowledge_bulk_publish, for cleanup passes. REQUIRES LOOPCTL_USER_KEY (user role). " +
|
|
2687
|
+
"Every currently-published id is reverted to draft; each other id gets a per-id outcome: " +
|
|
2688
|
+
"unpublished, skipped (already draft — idempotent — or archived/superseded), not_found, " +
|
|
2689
|
+
"or errored. Duplicate ids de-duplicated; no 100-id cap (auto-chunked server-side, " +
|
|
2690
|
+
"bounded to 5000/call). meta.count = number actually unpublished; meta.counts has the " +
|
|
2691
|
+
"full breakdown; meta.results is the per-id list in request order. Safe to retry — " +
|
|
2692
|
+
"already-draft ids are skipped, not errored. Articles are NOT deleted (re-publish with " +
|
|
2693
|
+
"knowledge_bulk_publish); to archive/soft-delete use knowledge_bulk_delete.",
|
|
2694
|
+
inputSchema: {
|
|
2695
|
+
type: "object",
|
|
2696
|
+
properties: {
|
|
2697
|
+
article_ids: {
|
|
2698
|
+
type: "array",
|
|
2699
|
+
items: { type: "string" },
|
|
2700
|
+
description:
|
|
2701
|
+
"Article UUIDs to unpublish. Any length (auto-chunked); duplicates ignored.",
|
|
1754
2702
|
},
|
|
1755
2703
|
},
|
|
1756
2704
|
required: ["article_ids"],
|
|
@@ -1810,20 +2758,92 @@ const TOOLS = [
|
|
|
1810
2758
|
required: ["article_id"],
|
|
1811
2759
|
},
|
|
1812
2760
|
},
|
|
2761
|
+
{
|
|
2762
|
+
name: "knowledge_bulk_delete",
|
|
2763
|
+
description:
|
|
2764
|
+
"Bulk archive (default, reversible) or IRREVERSIBLE hard-delete of articles by selector. " +
|
|
2765
|
+
"REQUIRES LOOPCTL_USER_KEY (user role — orchestrator is NOT sufficient). Provide EXACTLY ONE " +
|
|
2766
|
+
"selector: article_ids (explicit list), source_type + source_id (every active article from " +
|
|
2767
|
+
"that source), or tag + confirm:true (every active article carrying the tag — high blast " +
|
|
2768
|
+
"radius, so confirm:true is required). " +
|
|
2769
|
+
"DEFAULT (soft archive): rows move to archived, never dropped; set-based + idempotent; " +
|
|
2770
|
+
"meta.count = archived, meta.counts/meta.results give the breakdown. " +
|
|
2771
|
+
"DRY-RUN: dry_run:true mutates NOTHING and returns meta.would_affect (and, for hard, a " +
|
|
2772
|
+
"single-use meta.token / for oversized selectors a meta.confirm_hash). " +
|
|
2773
|
+
"HARD DELETE (irreversible): first dry_run with hard:true to get a token, then call again " +
|
|
2774
|
+
"with hard:true + that token to FK-correctly delete the FROZEN id-set (links removed first, " +
|
|
2775
|
+
"access events cascade). The token is single-use and TTL-bounded. Bounded to 5000 per call.",
|
|
2776
|
+
inputSchema: {
|
|
2777
|
+
type: "object",
|
|
2778
|
+
properties: {
|
|
2779
|
+
article_ids: {
|
|
2780
|
+
type: "array",
|
|
2781
|
+
items: { type: "string" },
|
|
2782
|
+
description: "Explicit article UUIDs (selector 1).",
|
|
2783
|
+
},
|
|
2784
|
+
source_type: {
|
|
2785
|
+
type: "string",
|
|
2786
|
+
description: "With source_id: every active article from this source (selector 2).",
|
|
2787
|
+
},
|
|
2788
|
+
source_id: {
|
|
2789
|
+
type: "string",
|
|
2790
|
+
description: "With source_type: the source entity UUID (selector 2).",
|
|
2791
|
+
},
|
|
2792
|
+
tag: {
|
|
2793
|
+
type: "string",
|
|
2794
|
+
description:
|
|
2795
|
+
"Every active article carrying this tag (selector 3). Requires confirm:true.",
|
|
2796
|
+
},
|
|
2797
|
+
confirm: {
|
|
2798
|
+
type: "boolean",
|
|
2799
|
+
description: "Required (true) when selecting by tag — guards the high blast radius.",
|
|
2800
|
+
},
|
|
2801
|
+
dry_run: {
|
|
2802
|
+
type: "boolean",
|
|
2803
|
+
description:
|
|
2804
|
+
"Preview only — mutate nothing. Returns meta.would_affect; with hard:true also a " +
|
|
2805
|
+
"single-use meta.token (or meta.confirm_hash for oversized selectors).",
|
|
2806
|
+
},
|
|
2807
|
+
hard: {
|
|
2808
|
+
type: "boolean",
|
|
2809
|
+
description:
|
|
2810
|
+
"IRREVERSIBLE hard delete (vs default reversible archive). Run dry_run first to get a " +
|
|
2811
|
+
"token, then pass hard:true + token.",
|
|
2812
|
+
},
|
|
2813
|
+
token: {
|
|
2814
|
+
type: "string",
|
|
2815
|
+
description:
|
|
2816
|
+
"The single-use frozen-set token from a `dry_run:true, hard:true` preview. Required " +
|
|
2817
|
+
"for the hard delete.",
|
|
2818
|
+
},
|
|
2819
|
+
confirm_hash: {
|
|
2820
|
+
type: "string",
|
|
2821
|
+
description:
|
|
2822
|
+
"For an oversized hard-delete selector (no token): the meta.confirm_hash from the " +
|
|
2823
|
+
"dry-run, echoed back to re-confirm the id-set hasn't drifted.",
|
|
2824
|
+
},
|
|
2825
|
+
},
|
|
2826
|
+
required: [],
|
|
2827
|
+
},
|
|
2828
|
+
},
|
|
1813
2829
|
{
|
|
1814
2830
|
name: "knowledge_drafts",
|
|
1815
2831
|
description:
|
|
1816
2832
|
"List draft (unpublished) knowledge articles. Requires orchestrator role. " +
|
|
1817
|
-
"Returns paginated drafts with total_count in meta.
|
|
2833
|
+
"Returns paginated drafts with total_count in meta. Paginate via offset/limit " +
|
|
2834
|
+
"(limit honored up to 1000; a limit above the max is rejected with 400, not " +
|
|
2835
|
+
"silently clamped).",
|
|
1818
2836
|
inputSchema: {
|
|
1819
2837
|
type: "object",
|
|
1820
2838
|
properties: {
|
|
1821
2839
|
limit: {
|
|
1822
2840
|
type: "integer",
|
|
1823
|
-
description:
|
|
2841
|
+
description:
|
|
2842
|
+
"Max drafts per page (default 20, max 1000). A limit above the max is " +
|
|
2843
|
+
"rejected with 400 — not silently clamped — so offset pagination stays complete.",
|
|
1824
2844
|
default: 20,
|
|
1825
2845
|
minimum: 1,
|
|
1826
|
-
maximum:
|
|
2846
|
+
maximum: 1000,
|
|
1827
2847
|
},
|
|
1828
2848
|
offset: {
|
|
1829
2849
|
type: "integer",
|
|
@@ -1878,8 +2898,9 @@ const TOOLS = [
|
|
|
1878
2898
|
{
|
|
1879
2899
|
name: "knowledge_export",
|
|
1880
2900
|
description:
|
|
1881
|
-
"Export all knowledge articles as
|
|
1882
|
-
"
|
|
2901
|
+
"Export all knowledge articles as an OKF v0.1 bundle — gzipped tar archive, unbounded, bounded-memory streaming, " +
|
|
2902
|
+
"fail-closed (no partial bundles). Because binary cannot be returned as MCP content, this tool returns a curl command. " +
|
|
2903
|
+
"Optional: ?format=json for buffered in-memory JSON (capped at export_max_buffered_export_articles, 413 over cap).",
|
|
1883
2904
|
inputSchema: {
|
|
1884
2905
|
type: "object",
|
|
1885
2906
|
properties: {
|
|
@@ -1887,10 +2908,71 @@ const TOOLS = [
|
|
|
1887
2908
|
type: "string",
|
|
1888
2909
|
description: "Optional: scope export to a specific project UUID.",
|
|
1889
2910
|
},
|
|
2911
|
+
format: {
|
|
2912
|
+
type: "string",
|
|
2913
|
+
enum: ["tar.gz", "json"],
|
|
2914
|
+
description: "Optional: tar.gz (default, unbounded streaming) or json (buffered, capped).",
|
|
2915
|
+
},
|
|
2916
|
+
},
|
|
2917
|
+
required: [],
|
|
2918
|
+
},
|
|
2919
|
+
},
|
|
2920
|
+
{
|
|
2921
|
+
name: "knowledge_okf_export",
|
|
2922
|
+
description:
|
|
2923
|
+
"Export the knowledge wiki as a portable OKF (Open Knowledge Format) v0.1 bundle — a tree of " +
|
|
2924
|
+
"markdown files with YAML frontmatter. Requires LOOPCTL_USER_KEY. If out_dir is given, the bundle " +
|
|
2925
|
+
"is written there (one .md file per concept, plus index.md/log.md) and a summary is returned; " +
|
|
2926
|
+
"otherwise the bundle is returned inline as {files, meta}.",
|
|
2927
|
+
inputSchema: {
|
|
2928
|
+
type: "object",
|
|
2929
|
+
properties: {
|
|
2930
|
+
project_id: {
|
|
2931
|
+
type: "string",
|
|
2932
|
+
format: "uuid",
|
|
2933
|
+
description: "Optional: scope the export to a project (includes tenant-wide articles too).",
|
|
2934
|
+
},
|
|
2935
|
+
out_dir: {
|
|
2936
|
+
type: "string",
|
|
2937
|
+
description:
|
|
2938
|
+
"Optional: absolute path of a directory to write the bundle into. When omitted, the " +
|
|
2939
|
+
"bundle is returned inline.",
|
|
2940
|
+
},
|
|
1890
2941
|
},
|
|
1891
2942
|
required: [],
|
|
1892
2943
|
},
|
|
1893
2944
|
},
|
|
2945
|
+
{
|
|
2946
|
+
name: "knowledge_okf_import",
|
|
2947
|
+
description:
|
|
2948
|
+
"Import an OKF (Open Knowledge Format) v0.1 bundle from a local directory into the wiki. " +
|
|
2949
|
+
"Requires LOOPCTL_USER_KEY. Reserved files (index.md/log.md) are skipped; each concept is created, " +
|
|
2950
|
+
"or (with merge=true, the default) updated in place when it matches an existing article. Unknown " +
|
|
2951
|
+
"frontmatter types/keys are tolerated and preserved. Returns a per-file import report.",
|
|
2952
|
+
inputSchema: {
|
|
2953
|
+
type: "object",
|
|
2954
|
+
properties: {
|
|
2955
|
+
bundle_dir: {
|
|
2956
|
+
type: "string",
|
|
2957
|
+
description: "Absolute path of the OKF bundle directory to read .md files from.",
|
|
2958
|
+
},
|
|
2959
|
+
project_id: {
|
|
2960
|
+
type: "string",
|
|
2961
|
+
format: "uuid",
|
|
2962
|
+
description: "Optional: assign imported articles to a project.",
|
|
2963
|
+
},
|
|
2964
|
+
merge: {
|
|
2965
|
+
type: "boolean",
|
|
2966
|
+
description: "Update existing articles instead of skipping them (default true).",
|
|
2967
|
+
},
|
|
2968
|
+
dry_run: {
|
|
2969
|
+
type: "boolean",
|
|
2970
|
+
description: "Validate and plan only; write nothing (default false).",
|
|
2971
|
+
},
|
|
2972
|
+
},
|
|
2973
|
+
required: ["bundle_dir"],
|
|
2974
|
+
},
|
|
2975
|
+
},
|
|
1894
2976
|
|
|
1895
2977
|
// Knowledge Ingestion Tools
|
|
1896
2978
|
{
|
|
@@ -1898,7 +2980,9 @@ const TOOLS = [
|
|
|
1898
2980
|
description:
|
|
1899
2981
|
"Submit a URL or raw content for knowledge extraction. " +
|
|
1900
2982
|
"Enqueues an Oban job that fetches the content (if URL), extracts knowledge articles via LLM, " +
|
|
1901
|
-
"and inserts them
|
|
2983
|
+
"and inserts them. Extracted articles are DRAFTS by default (lower-trust LLM output, staged " +
|
|
2984
|
+
"for review) — unlike knowledge_create which publishes by default. Pass publish:true to " +
|
|
2985
|
+
"publish them on extraction. Requires orchestrator role.",
|
|
1902
2986
|
inputSchema: {
|
|
1903
2987
|
type: "object",
|
|
1904
2988
|
properties: {
|
|
@@ -1918,6 +3002,11 @@ const TOOLS = [
|
|
|
1918
3002
|
type: "string",
|
|
1919
3003
|
description: "Optional: scope extracted articles to a specific project UUID.",
|
|
1920
3004
|
},
|
|
3005
|
+
publish: {
|
|
3006
|
+
type: "boolean",
|
|
3007
|
+
description:
|
|
3008
|
+
"Optional: publish extracted articles immediately instead of staging them as drafts (default false).",
|
|
3009
|
+
},
|
|
1921
3010
|
},
|
|
1922
3011
|
required: ["source_type"],
|
|
1923
3012
|
},
|
|
@@ -1956,6 +3045,11 @@ const TOOLS = [
|
|
|
1956
3045
|
type: "string",
|
|
1957
3046
|
description: "Optional: scope the item to a specific project UUID.",
|
|
1958
3047
|
},
|
|
3048
|
+
publish: {
|
|
3049
|
+
type: "boolean",
|
|
3050
|
+
description:
|
|
3051
|
+
"Optional: publish this item's extracted articles immediately (default false → draft).",
|
|
3052
|
+
},
|
|
1959
3053
|
metadata: {
|
|
1960
3054
|
type: "object",
|
|
1961
3055
|
description: "Optional metadata map.",
|
|
@@ -1968,6 +3062,10 @@ const TOOLS = [
|
|
|
1968
3062
|
type: "string",
|
|
1969
3063
|
description: "Optional batch-level default project UUID applied to items that don't specify their own.",
|
|
1970
3064
|
},
|
|
3065
|
+
publish: {
|
|
3066
|
+
type: "boolean",
|
|
3067
|
+
description: "Optional batch-level default publish flag applied to items that don't specify their own.",
|
|
3068
|
+
},
|
|
1971
3069
|
},
|
|
1972
3070
|
required: ["items"],
|
|
1973
3071
|
},
|
|
@@ -1975,11 +3073,20 @@ const TOOLS = [
|
|
|
1975
3073
|
{
|
|
1976
3074
|
name: "knowledge_ingestion_jobs",
|
|
1977
3075
|
description:
|
|
1978
|
-
"List
|
|
1979
|
-
"
|
|
3076
|
+
"List content ingestion jobs for the current tenant, newest first. " +
|
|
3077
|
+
"Paginated (limit/offset over the full history; advance `offset` by `meta.limit` " +
|
|
3078
|
+
"to enumerate to completeness). Optional `since_days` narrows to a recent window. " +
|
|
3079
|
+
"Requires orchestrator role.",
|
|
1980
3080
|
inputSchema: {
|
|
1981
3081
|
type: "object",
|
|
1982
|
-
properties: {
|
|
3082
|
+
properties: {
|
|
3083
|
+
limit: { type: "integer", description: "Jobs per page (default 20)." },
|
|
3084
|
+
offset: { type: "integer", description: "Rows to skip (default 0)." },
|
|
3085
|
+
since_days: {
|
|
3086
|
+
type: "integer",
|
|
3087
|
+
description: "Optional: only jobs from the last N days (default: all history).",
|
|
3088
|
+
},
|
|
3089
|
+
},
|
|
1983
3090
|
required: [],
|
|
1984
3091
|
},
|
|
1985
3092
|
},
|
|
@@ -2298,9 +3405,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2298
3405
|
case "knowledge_index":
|
|
2299
3406
|
return await knowledgeIndex(args);
|
|
2300
3407
|
|
|
3408
|
+
case "knowledge_stats":
|
|
3409
|
+
return await knowledgeStats(args);
|
|
3410
|
+
|
|
3411
|
+
case "knowledge_count":
|
|
3412
|
+
return await knowledgeCount(args);
|
|
3413
|
+
|
|
3414
|
+
case "knowledge_facets":
|
|
3415
|
+
return await knowledgeFacets(args);
|
|
3416
|
+
|
|
3417
|
+
case "knowledge_graph":
|
|
3418
|
+
return await knowledgeGraph(args);
|
|
3419
|
+
|
|
3420
|
+
case "knowledge_suggest_links":
|
|
3421
|
+
return await knowledgeSuggestLinks(args);
|
|
3422
|
+
|
|
3423
|
+
case "knowledge_distant_pairs":
|
|
3424
|
+
return await knowledgeDistantPairs(args);
|
|
3425
|
+
|
|
3426
|
+
case "knowledge_novelty":
|
|
3427
|
+
return await knowledgeNovelty(args);
|
|
3428
|
+
|
|
3429
|
+
case "knowledge_random_walk":
|
|
3430
|
+
return await knowledgeRandomWalk(args);
|
|
3431
|
+
|
|
2301
3432
|
case "knowledge_search":
|
|
2302
3433
|
return await knowledgeSearch(args);
|
|
2303
3434
|
|
|
3435
|
+
case "knowledge_list":
|
|
3436
|
+
return await knowledgeList(args);
|
|
3437
|
+
|
|
2304
3438
|
case "knowledge_get":
|
|
2305
3439
|
return await knowledgeGet(args);
|
|
2306
3440
|
|
|
@@ -2317,6 +3451,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2317
3451
|
case "knowledge_bulk_publish":
|
|
2318
3452
|
return await knowledgeBulkPublish(args);
|
|
2319
3453
|
|
|
3454
|
+
case "knowledge_bulk_unpublish":
|
|
3455
|
+
return await knowledgeBulkUnpublish(args);
|
|
3456
|
+
|
|
2320
3457
|
case "knowledge_unpublish":
|
|
2321
3458
|
return await knowledgeUnpublish(args);
|
|
2322
3459
|
|
|
@@ -2326,6 +3463,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2326
3463
|
case "knowledge_delete":
|
|
2327
3464
|
return await knowledgeDelete(args);
|
|
2328
3465
|
|
|
3466
|
+
case "knowledge_bulk_delete":
|
|
3467
|
+
return await knowledgeBulkDelete(args);
|
|
3468
|
+
|
|
2329
3469
|
case "knowledge_drafts":
|
|
2330
3470
|
return await knowledgeDrafts(args);
|
|
2331
3471
|
|
|
@@ -2335,6 +3475,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2335
3475
|
case "knowledge_export":
|
|
2336
3476
|
return await knowledgeExport(args);
|
|
2337
3477
|
|
|
3478
|
+
case "knowledge_okf_export":
|
|
3479
|
+
return await knowledgeOkfExport(args);
|
|
3480
|
+
|
|
3481
|
+
case "knowledge_okf_import":
|
|
3482
|
+
return await knowledgeOkfImport(args);
|
|
3483
|
+
|
|
2338
3484
|
// Knowledge Ingestion Tools
|
|
2339
3485
|
case "knowledge_ingest":
|
|
2340
3486
|
return await knowledgeIngest(args);
|