loopctl-mcp-server 2.71.0 → 2.72.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.js +40 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -304,7 +304,7 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
304
304
  | `knowledge_bulk_delete` | **Requires `LOOPCTL_USER_KEY`.** Bulk archive (default — non-destructive, but NOT reversible by any call: `:archived` is terminal and restoring needs a user-role PATCH) or IRREVERSIBLE hard-delete by selector. Provide exactly one selector: `article_ids` (list), `source_type`+`source_id` (every active article from a source), or `tag`+`confirm:true` (every active article with the tag — high blast radius). Default = set-based soft archive (idempotent; `meta.count`=archived, `meta.counts`/`meta.results` give the breakdown; ≤5000). **Dry-run** (`dry_run:true`) mutates nothing, returns `meta.would_affect` (with `hard:true` also a single-use `meta.token`, or `meta.confirm_hash` for oversized selectors). **Hard delete** (irreversible): dry-run with `hard:true` for a token, then call again with `hard:true`+`token` to FK-correctly delete the frozen id-set (links first, access events cascade). |
305
305
  | `knowledge_drafts` | List draft (unpublished) knowledge articles with pagination. Optional: `limit` (default 20, max 1000 — over-max → 400, no silent clamp), `offset` (default 0), `project_id`. Returns `meta.total_count`. |
306
306
  | `knowledge_lint` | Run a lint check on the knowledge wiki to identify stale or low-coverage articles. Optional: `project_id`, `stale_days`, `min_coverage`, `max_per_category` (default 50, max 500). True totals returned in `summary.total_per_category`. |
307
- | `knowledge_consolidation` | Read the nightly consolidation ("dream") report: NUMBERED proposals for reconciling the corpus, each naming the articles involved and quoting an excerpt from each as evidence. **This tool** applies nothing and recomputes nothing — it returns persisted rows. **The pass** it reports on does write: since #608 the nightly run UNPUBLISHES the losers of each `duplicate_capture` group that two consecutive reports both propose. That is its only write to `articles`, it is an unpublish and never an archive (archive is terminal for an article), and it still writes no links or conflict resolutions. Classes: `duplicate_capture` (titles that collide once case/punctuation normalize away, or idempotency keys that collide under the same normalization while differing verbatim — capture tag-format drift, which the novelty gate does not catch because novelty scoring and idempotency are separate paths), `generic_title` (a placeholder title that collides on active-title uniqueness and blocks hub creation). Two classes are **RETIRED** (#605) and no longer produced, though the `class` filter still accepts them so historical reports stay readable: `contradiction_candidate` (the nightly lint judges those pairs itself now) and `stale_entry` (age is not a defect signal — for stale articles call `knowledge_lint`, which computes them with a caller-chosen `stale_days`). **Denominators:** `corpus_size` = PUBLISHED articles owned by the tenant at scan time, not its total article count; `proposal_count` = the TRUE pre-cap count of PROPOSALS, not of articles (one duplicate group of three articles is ONE proposal, and one article can appear in proposals of several classes); `persisted_count` = proposal ROWS the report carries, lower than `proposal_count` exactly when a class hit `max_per_class` (`truncated` flags which); `meta.total_count` counts persisted proposals matching the `class` filter, so it is bounded by `persisted_count`, never `proposal_count`. `review_status`/`reviewed_by`/`reviewed_at` are vestigial — nothing reads them to decide anything, there is no approve/reject surface and there will not be one (#605 supersedes #594); auto-apply is gated on reversibility and two-run agreement. They still reset to pending/null whenever the nightly pass re-derives a proposal, so refreshed machine output never inherits an earlier verdict. Requires orchestrator role. Optional: `day` (ISO8601, default most recent report), `class`, `limit` (default 50, max 500), `offset`. |
307
+ | `knowledge_consolidation` | Read the nightly consolidation ("dream") report: NUMBERED proposals for reconciling the corpus, each naming the articles involved and quoting an excerpt from each as evidence. **This tool** applies nothing and recomputes nothing — it returns persisted rows. **The pass** it reports on does write: since #608 the nightly run UNPUBLISHES the losers of each `duplicate_capture` group that two consecutive reports both propose (consecutive meaning the previous report is at most 2 days older, so one skipped nightly run is tolerated and a longer outage is not). That is its only write to `articles`, it is an unpublish and never an archive (archive is terminal for an article), and it still writes no links or conflict resolutions. Classes: `duplicate_capture` (titles that collide once case/punctuation normalize away, or idempotency keys that collide under the same normalization while differing verbatim — capture tag-format drift, which the novelty gate does not catch because novelty scoring and idempotency are separate paths), `generic_title` (a placeholder title that collides on active-title uniqueness and blocks hub creation). Two classes are **RETIRED** (#605) and no longer produced, though the `class` filter still accepts them so historical reports stay readable: `contradiction_candidate` (the nightly lint judges those pairs itself now) and `stale_entry` (age is not a defect signal — for stale articles call `knowledge_lint`, which computes them with a caller-chosen `stale_days`). **Denominators:** `corpus_size` = PUBLISHED articles owned by the tenant at scan time, not its total article count; `proposal_count` = the TRUE pre-cap count of PROPOSALS, not of articles (one duplicate group of three articles is ONE proposal, and one article can appear in proposals of several classes); `persisted_count` = proposal ROWS the report carries, lower than `proposal_count` exactly when a class hit `max_per_class` (`truncated` flags which); `meta.total_count` counts persisted proposals matching the `class` filter, so it is bounded by `persisted_count`, never `proposal_count`. `review_status`/`reviewed_by`/`reviewed_at` are vestigial — nothing reads them to decide anything, there is no approve/reject surface and there will not be one (#605 supersedes #594); auto-apply is gated on reversibility and two-run agreement. They still reset to pending/null whenever the nightly pass re-derives a proposal, so refreshed machine output never inherits an earlier verdict. Requires orchestrator role. Optional: `day` (ISO8601, default most recent report), `class`, `limit` (default 50, max 500), `offset`. |
308
308
  | `knowledge_export` | Export all knowledge articles as an OKF v0.1 bundle (gzipped tar archive, unbounded, bounded-memory streaming, fail-closed). Returns a curl command for direct download — **the download requires `LOOPCTL_USER_KEY`** (an orchestrator key would 403). Pass `format=json` for buffered in-memory JSON (convenience tool for file writers; capped at `export_max_buffered_export_articles` — returns 413 if over-cap). Optional: `project_id`, `format` (`tar.gz` default or `json`). |
309
309
  | `knowledge_ingest` | Submit a URL or raw content for knowledge extraction. Enqueues an Oban job. Extracted articles are **drafts by default** (lower-trust LLM output); pass `publish: true` to publish on extraction. **BYO:** runs on the tenant's own Anthropic key — a keyless tenant gets a 422 whose result leads with an `ACTION REQUIRED` notice pointing at `set_llm_config` (see [First-time setup](#first-time-setup--provision-your-byo-llm-keys)). Required: `source_type`. One of: `url` or `content`. Optional: `project_id`, `publish`. |
310
310
  | `knowledge_ingest_batch` | Submit up to 50 ingestion items in a single request. Each item has the same shape as `knowledge_ingest` (incl. `publish`). Returns per-item results. Required: `items`. Optional: batch-level `project_id` / `publish` defaults. |
package/index.js CHANGED
@@ -1942,12 +1942,16 @@ async function knowledgeConsolidation({ day, class: klass, limit, offset } = {})
1942
1942
  return toContent(result);
1943
1943
  }
1944
1944
 
1945
- async function knowledgeIngest({ url, content, source_type, project_id, publish }) {
1945
+ async function knowledgeIngest({ url, content, source_type, project_id, publish, metadata }) {
1946
1946
  const body = { source_type };
1947
1947
  if (url) body.url = url;
1948
1948
  if (content) body.content = content;
1949
1949
  if (project_id) body.project_id = project_id;
1950
1950
  if (publish) body.publish = true;
1951
+ // Forwarded so `metadata.source_ref` is reachable at all: the server honours it as the
1952
+ // source that article titles are qualified with, and declaring it in the schema above
1953
+ // without forwarding it here would advertise a parameter that silently does nothing.
1954
+ if (metadata && typeof metadata === "object") body.metadata = metadata;
1951
1955
  const result = await apiCall("POST", "/api/v1/knowledge/ingest", body, process.env.LOOPCTL_ORCH_KEY);
1952
1956
  // A keyless tenant gets a 422 (code no_api_key) carrying a remediation — surface it
1953
1957
  // prominently so a first-time agent knows to call set_llm_config before ingesting.
@@ -5492,7 +5496,9 @@ const TOOLS = [
5492
5496
  "the corpus, each naming the articles involved and quoting an excerpt from each as " +
5493
5497
  "evidence. THIS TOOL applies nothing and recomputes nothing — it returns persisted rows. " +
5494
5498
  "The PASS it reports on does write: since #608 the nightly run UNPUBLISHES the losers of " +
5495
- "each `duplicate_capture` group that two consecutive reports both propose. That is its " +
5499
+ "each `duplicate_capture` group that two consecutive reports both propose consecutive " +
5500
+ "meaning the previous report is at most 2 days older, so ONE skipped nightly run is " +
5501
+ "tolerated and a longer outage is not. That is its " +
5496
5502
  "only write to articles, it is an unpublish and never an archive (archive is terminal for " +
5497
5503
  "an article), and it still writes no links or conflict resolutions. Requires orchestrator " +
5498
5504
  "role.\n\n" +
@@ -5665,6 +5671,25 @@ const TOOLS = [
5665
5671
  description:
5666
5672
  "Optional: publish extracted articles immediately instead of staging them as drafts (default false).",
5667
5673
  },
5674
+ metadata: {
5675
+ type: "object",
5676
+ description:
5677
+ "Optional metadata map. `source_ref` is the one key with behaviour: it names the " +
5678
+ "SPECIFIC source (a URL, repo, or document name) and is what lets extracted article " +
5679
+ "titles qualify themselves — without it a CHANGELOG file can only become an article " +
5680
+ "titled \"Changelog\", which is indistinguishable from every other document's " +
5681
+ "changelog once it is in the corpus. It overrides the name derived from `url`, and " +
5682
+ "is the ONLY way to name the source of an inline `content` ingest. Its value is " +
5683
+ "included in the extraction prompt POSTed to the tenant's LLM provider (reduced the " +
5684
+ "same way a url is: userinfo and query string stripped, host and path kept). Omit it " +
5685
+ "rather than passing a placeholder — a model will qualify a title WITH it.",
5686
+ properties: {
5687
+ source_ref: {
5688
+ type: "string",
5689
+ description: "The specific source that article titles are qualified with.",
5690
+ },
5691
+ },
5692
+ },
5668
5693
  },
5669
5694
  required: ["source_type"],
5670
5695
  },
@@ -5710,7 +5735,19 @@ const TOOLS = [
5710
5735
  },
5711
5736
  metadata: {
5712
5737
  type: "object",
5713
- description: "Optional metadata map.",
5738
+ description:
5739
+ "Optional metadata map. Set `source_ref` to the SPECIFIC source (URL, repo, " +
5740
+ "or document name) so extracted titles qualify themselves — without it a " +
5741
+ "CHANGELOG becomes an article titled \"Changelog\", indistinguishable from " +
5742
+ "every other document's changelog in the corpus. Overrides the url-derived " +
5743
+ "name, and is the only way to name an inline `content` item. Sent to the " +
5744
+ "LLM provider in the extraction prompt. Omit rather than passing a placeholder.",
5745
+ properties: {
5746
+ source_ref: {
5747
+ type: "string",
5748
+ description: "The specific source that article titles are qualified with.",
5749
+ },
5750
+ },
5714
5751
  },
5715
5752
  },
5716
5753
  required: ["source_type"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.71.0",
3
+ "version": "2.72.0",
4
4
  "description": "MCP server for loopctl \u2014 structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",