loopctl-mcp-server 2.63.0 → 2.65.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 +4 -2
- package/index.js +85 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -239,6 +239,7 @@ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for age
|
|
|
239
239
|
| Find by topic / relevance (ranked, published-only, lags writes) | `knowledge_search` |
|
|
240
240
|
| One trustworthy answer + provenance (curated-first, retrieval fallback) | `knowledge_hybrid_search` |
|
|
241
241
|
| Survey a topic cheaply (capped stubs) then open only what you need | `knowledge_progressive_index` + `knowledge_progressive_drill` |
|
|
242
|
+
| A search came back empty/thin, or you don't yet know what to ask | `knowledge_heat_index` (no query at all) |
|
|
242
243
|
| Enumerate / dedup / repair, or "does X exist?" (full fields, lag-free, all-status) | `knowledge_list` |
|
|
243
244
|
|
|
244
245
|
| Tool | Description |
|
|
@@ -253,9 +254,10 @@ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for age
|
|
|
253
254
|
| `knowledge_search` | Search the knowledge wiki by topic (keyword, semantic, or combined). Returns snippets. **Ranked, published-only, and LAGS writes by minutes while embeddings index — do NOT use for existence/idempotency/dedup checks (a fresh write false-negatives); use `knowledge_list` for that.** `q` is optional when `tags`/`category` are supplied — that **list mode** returns the complete filtered set paginated via `offset`/`limit` over `meta.total_count`. `meta.total_count` is mode-dependent — read `meta.total_count_scope` (`keyword_matches`/`ranked_corpus`/`merged_candidates`/`filtered_set`) and don't use a relevance-mode count to size the wiki (use `knowledge_list` or `knowledge_stats`). Optional: `project_id`, `story_id` for attribution. |
|
|
254
255
|
| `knowledge_hybrid_search` | Resolve a topic to a **single best answer with provenance** (US-31.4). Runs combined keyword+semantic over the full ranked pool, then decides whether a governed **curated** source actually answers. `meta.provenance` is `curated` (trust it — the canonical article is first in `data`, `meta.curated_article_id` points at it) or `retrieved` (best fuzzy match, `curated_article_id` null); `meta.confidence` is the winner's absolute score. Prefer over `knowledge_search` when you want one trustworthy answer, not a list to triage. Degrades to keyword-only like `knowledge_search` when embeddings are unavailable. Required: `query`. Optional: `project_id`, `category`, `tags`, `match`, `limit`, `offset`. |
|
|
255
256
|
| `knowledge_progressive_index` | Progressive disclosure — a **cheap, capped index** of what's relevant to a topic (compact stubs: `id`/`title`/`category`/`summary`, **no bodies**), curated-preferred and hub-enriched, capped at top-K (`meta.truncated` when the pool exceeded it). Survey a topic without flooding context, then open only what you need via `knowledge_progressive_drill`. Required: `topic`. Optional: `category`, `limit`. |
|
|
256
|
-
| `
|
|
257
|
+
| `knowledge_heat_index` | Browse the corpus with **no query at all** — capped compact stubs (`id`/`title`/`category`/`heat`/`summary`, **no bodies**) ranked by how many **distinct readers** (agents, not key rows — repeat reads by one reader count once, ties broken by the number of distinct days read, never by raw read count) opened each article inside a window. Every other retrieval tool starts from a query and so shares one failure mode: a paraphrase, or material topically central but lexically dissimilar to the question, comes back empty and reads as "the KB has nothing" rather than "I asked badly". Reach for it when a search came back empty or thin, or to survey what the fleet actually reads before you know what to ask. **Ordering is usage, not relevance.** `meta` states `heat_window` (snapped to a UTC day boundary, narrowing only, so the payload is stable between refreshes and safe in a cached prefix), `counted_access_types`, `char_budget`/`chars`, `truncated` and `unresolved`. Open a stub with `knowledge_progressive_drill` — **not** `knowledge_get`, which cannot resolve the published system canonicals this index also lists. Optional: `category`, `limit`, `since`. |
|
|
258
|
+
| `knowledge_progressive_drill` | Open one stub from `knowledge_progressive_index` or `knowledge_heat_index` — returns the **full article body** for the given id, scope-enforced. Resolves both tenant-owned articles and published system canonicals (the same set those indexes surface). Required: `article_id`. |
|
|
257
259
|
| `knowledge_list` | List articles (`id`, `title`, `category`, `status`, `tags`, `source_type`, `source_id`, `idempotency_key`, timestamps), filtered + paginated. **Body-less summary by default** (safe to page up to `limit=1000`); pass `include_body: true` to also return `body`, in which case the page is bounded by a ~5 MB byte budget — continue via `meta.next_offset` while `meta.has_more`. **Lag-free, all-status** read of the DB of record — unlike `knowledge_search` (ranked, published-only, lags writes) and `knowledge_index` (id/title/category only). The right tool to enumerate/dedup/repair and for idempotency/existence checks: filter by `tags`, `source_type`+`source_id`, or `idempotency_key` and read `meta.total_count` (exact). Single full body → `knowledge_get`; relevant bodies → `knowledge_context`; bulk dump → `knowledge_export`. Optional: `project_id`, `category`, `status`, `tags`, `source_type`, `source_id`, `idempotency_key`, `offset`, `limit`, `include_body`. |
|
|
258
|
-
| `knowledge_get` | Get full article content by ID. Use after search to read an article in detail. Optional: `project_id`, `story_id` for attribution. |
|
|
260
|
+
| `knowledge_get` | Get full article content by ID. Use after search to read an article in detail. Each link carries only its FAR side (`article: {id, title}`, plus `similarity` when scored); both arrays are ranked (open conflicts first, then descending similarity, then oldest-first for the unscored) and capped at 25 per direction, with `links_total` / `links_truncated` reporting the truth (`count` returns both, so one cheap call tells you whether the full fetch is capped). Pass `links: "count"` or `"none"` when you only want the text — on a well-linked hub the link block is several times the body. `potential_conflicts` is returned in all three modes, itself capped at 25 with `conflicts_total` / `conflicts_truncated`. Optional: `links`, `project_id`, `story_id` for attribution. |
|
|
259
261
|
| `knowledge_context` | Get relevance-and-recency-ranked full articles for a task query. Best knowledge for your current context. **Agent-memory scoping**: `memory_types` (comma-separated, OR — observation/finding/summary/decision/question/task), `agents` (comma-separated agent_ids, OR), `conversation_id` (exact) filter on article `metadata` (JSONB `@>`). Optional: `project_id`, `story_id` for attribution, `limit`, `recency_weight`. |
|
|
260
262
|
| `knowledge_graph` | Multi-hop traversal of the published article-link graph from `article_id` (depth 1–3, default 1), **bounded to agent's visible articles**. Agent callers see only their own and `shared` articles. Bidirectional, cycle-safe, bounded to 100 nodes / 500 edges (`truncated` flags a cap). Returns `nodes` (`id`/`title`/`category`/`depth`) + `edges` (`source_article_id`/`target_article_id`/`relationship_type`). Explore typed connections beyond `knowledge_context`'s 1-hop links. Required: `article_id`. Optional: `depth`, `project_id`. |
|
|
261
263
|
| `knowledge_suggest_links` | Ranked typed-link **candidates** for an article by embedding similarity among **visible articles** — **read-only** (creates nothing). Excludes the article itself + any already-linked article (either direction, any type); only embedded published articles visible to the caller. Agent callers see only their own and `shared` articles. Returns `{id, title, category, similarity_score}` highest-first, to create as a **typed** link (relates_to/derived_from/contradicts/supersedes). Required: `article_id`. Optional: `threshold` (cosine floor 0–1, default 0.5), `limit` (default 5). |
|
package/index.js
CHANGED
|
@@ -1414,6 +1414,21 @@ async function knowledgeProgressiveIndex({ topic, category, limit }) {
|
|
|
1414
1414
|
return toContent(result);
|
|
1415
1415
|
}
|
|
1416
1416
|
|
|
1417
|
+
async function knowledgeHeatIndex({ category, limit, since }) {
|
|
1418
|
+
const params = new URLSearchParams();
|
|
1419
|
+
if (category) params.set("category", category);
|
|
1420
|
+
if (limit != null) params.set("limit", String(limit));
|
|
1421
|
+
if (since) params.set("since", since);
|
|
1422
|
+
|
|
1423
|
+
const result = await apiCall(
|
|
1424
|
+
"GET",
|
|
1425
|
+
`/api/v1/knowledge/heat_index?${params}`,
|
|
1426
|
+
null,
|
|
1427
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1428
|
+
);
|
|
1429
|
+
return toContent(result);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1417
1432
|
async function knowledgeProgressiveDrill({ article_id }) {
|
|
1418
1433
|
const result = await apiCall(
|
|
1419
1434
|
"GET",
|
|
@@ -1461,10 +1476,11 @@ async function knowledgeList({
|
|
|
1461
1476
|
return toContent(result);
|
|
1462
1477
|
}
|
|
1463
1478
|
|
|
1464
|
-
async function knowledgeGet({ article_id, project_id, story_id }) {
|
|
1479
|
+
async function knowledgeGet({ article_id, project_id, story_id, links }) {
|
|
1465
1480
|
const params = new URLSearchParams();
|
|
1466
1481
|
if (project_id) params.set("project_id", project_id);
|
|
1467
1482
|
if (story_id) params.set("story_id", story_id);
|
|
1483
|
+
if (links) params.set("links", links);
|
|
1468
1484
|
const qs = params.toString();
|
|
1469
1485
|
const path = qs ? `/api/v1/articles/${article_id}?${qs}` : `/api/v1/articles/${article_id}`;
|
|
1470
1486
|
const result = await apiCall("GET", path, null, process.env.LOOPCTL_AGENT_KEY);
|
|
@@ -4481,10 +4497,51 @@ const TOOLS = [
|
|
|
4481
4497
|
required: ["topic"],
|
|
4482
4498
|
},
|
|
4483
4499
|
},
|
|
4500
|
+
{
|
|
4501
|
+
name: "knowledge_heat_index",
|
|
4502
|
+
description:
|
|
4503
|
+
"Browse the corpus with NO query — a capped list of compact stubs " +
|
|
4504
|
+
"(id/title/category/heat/summary, NO bodies) ranked by how many DISTINCT readers " +
|
|
4505
|
+
"(agents, not key rows — repeat reads by one reader count once, ties broken by the " +
|
|
4506
|
+
"number of distinct days read, never by raw read count) actually opened each article " +
|
|
4507
|
+
"inside a window. " +
|
|
4508
|
+
"Every other retrieval tool starts from " +
|
|
4509
|
+
"a query, so they share one failure mode: a paraphrase, or material that is topically " +
|
|
4510
|
+
"central but lexically dissimilar to your question, comes back empty and reads as 'the " +
|
|
4511
|
+
"KB has nothing' rather than 'I asked badly'. Reach for this when a search came back " +
|
|
4512
|
+
"empty or thin, or to survey what the fleet actually reads before you know what to " +
|
|
4513
|
+
"ask. Ordering is usage, NOT relevance to any query. Open a stub with " +
|
|
4514
|
+
"knowledge_progressive_drill (not knowledge_get — this index also lists published " +
|
|
4515
|
+
"system canonicals, which knowledge_get cannot resolve).",
|
|
4516
|
+
inputSchema: {
|
|
4517
|
+
type: "object",
|
|
4518
|
+
properties: {
|
|
4519
|
+
category: {
|
|
4520
|
+
type: "string",
|
|
4521
|
+
description: "Optional: restrict to one category.",
|
|
4522
|
+
},
|
|
4523
|
+
limit: {
|
|
4524
|
+
type: "integer",
|
|
4525
|
+
description: "Optional: top-K override (clamped to the configured cap).",
|
|
4526
|
+
},
|
|
4527
|
+
since: {
|
|
4528
|
+
type: "string",
|
|
4529
|
+
description:
|
|
4530
|
+
"Optional: ISO-8601 timestamp; count only reads at/after it. Defaults to the " +
|
|
4531
|
+
"last 90 days, clamped to at most 365 days of lookback and to no later than " +
|
|
4532
|
+
"today. Snapped to a UTC day boundary in the NARROWING direction (never wider " +
|
|
4533
|
+
"than you asked for; a timestamp inside the current UTC day is used exactly as " +
|
|
4534
|
+
"given), and meta.heat_window echoes what you got.",
|
|
4535
|
+
},
|
|
4536
|
+
},
|
|
4537
|
+
required: [],
|
|
4538
|
+
},
|
|
4539
|
+
},
|
|
4484
4540
|
{
|
|
4485
4541
|
name: "knowledge_progressive_drill",
|
|
4486
4542
|
description:
|
|
4487
|
-
"Drill into one stub from knowledge_progressive_index
|
|
4543
|
+
"Drill into one stub from knowledge_progressive_index or knowledge_heat_index — " +
|
|
4544
|
+
"returns the FULL article " +
|
|
4488
4545
|
"body for the given id, scope-enforced. Resolves both tenant-owned articles and " +
|
|
4489
4546
|
"published system canonicals (the same set the index surfaces). This is the drill " +
|
|
4490
4547
|
"half of progressive disclosure: index cheaply, then open only the article(s) you " +
|
|
@@ -4511,7 +4568,20 @@ const TOOLS = [
|
|
|
4511
4568
|
"material to your current task, act on it: read the peer, judge redundant/complementary/" +
|
|
4512
4569
|
"contradictory against the live system, and knowledge_resolve_conflict (dismiss a false " +
|
|
4513
4570
|
"positive, supersede when one clearly wins, merge when both should combine). If you can't " +
|
|
4514
|
-
"tell which is right, leave it. See the 'Resolving knowledge conflicts' wiki playbook
|
|
4571
|
+
"tell which is right, leave it. See the 'Resolving knowledge conflicts' wiki playbook.\n\n" +
|
|
4572
|
+
"LINKS: each link carries only its FAR side as `article: {id, title}` (plus " +
|
|
4573
|
+
"`similarity` when the auto-linker scored it) — direction is already given by which " +
|
|
4574
|
+
"array it is in. Both arrays are ranked (open conflicts first, then descending " +
|
|
4575
|
+
"similarity, then oldest-first for the unscored) and capped at 25 per direction; " +
|
|
4576
|
+
"read `links_total` for the true count and `links_truncated` to know the cap bit — " +
|
|
4577
|
+
"both are returned by links: 'count' too, so one cheap call tells you whether the " +
|
|
4578
|
+
"full fetch is even complete. When you only want the article's TEXT, " +
|
|
4579
|
+
"pass links: 'count' (or 'none') — on a well-linked hub the link block is several " +
|
|
4580
|
+
"times the size of the body, and you are paying for it on every read. " +
|
|
4581
|
+
"`potential_conflicts` is returned in all three modes, so opting out of the link " +
|
|
4582
|
+
"list never hides a conflict from you; it is capped at 25 (strongest first) with " +
|
|
4583
|
+
"`conflicts_total` / `conflicts_truncated`. To actually traverse the graph, use " +
|
|
4584
|
+
"knowledge_graph rather than raising this cap.",
|
|
4515
4585
|
inputSchema: {
|
|
4516
4586
|
type: "object",
|
|
4517
4587
|
properties: {
|
|
@@ -4520,6 +4590,15 @@ const TOOLS = [
|
|
|
4520
4590
|
format: "uuid",
|
|
4521
4591
|
description: "The UUID of the article.",
|
|
4522
4592
|
},
|
|
4593
|
+
links: {
|
|
4594
|
+
type: "string",
|
|
4595
|
+
enum: ["full", "count", "none"],
|
|
4596
|
+
description:
|
|
4597
|
+
"Optional: how much of the link graph to return. 'full' (default) = ranked, " +
|
|
4598
|
+
"capped arrays; 'count' = just links_total + links_truncated; 'none' = omit " +
|
|
4599
|
+
"link fields. potential_conflicts (capped, with conflicts_total) is always " +
|
|
4600
|
+
"returned.",
|
|
4601
|
+
},
|
|
4523
4602
|
project_id: {
|
|
4524
4603
|
type: "string",
|
|
4525
4604
|
format: "uuid",
|
|
@@ -6708,6 +6787,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6708
6787
|
case "knowledge_progressive_index":
|
|
6709
6788
|
return await knowledgeProgressiveIndex(args);
|
|
6710
6789
|
|
|
6790
|
+
case "knowledge_heat_index":
|
|
6791
|
+
return await knowledgeHeatIndex(args);
|
|
6792
|
+
|
|
6711
6793
|
case "knowledge_progressive_drill":
|
|
6712
6794
|
return await knowledgeProgressiveDrill(args);
|
|
6713
6795
|
|