loopctl-mcp-server 2.38.0 → 2.39.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 +4 -3
  2. package/index.js +83 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -121,7 +121,7 @@ REST endpoint (`PATCH /api/v1/tenants/me/llm-config`), and the docs — so you (
121
121
  autonomous agent) can self-remediate without a human. Full agent-tenant lifecycle:
122
122
  [`docs/onboarding-agent-tenant.md`](../docs/onboarding-agent-tenant.md).
123
123
 
124
- ## Tools (83)
124
+ ## Tools (84)
125
125
 
126
126
  ### Project Tools
127
127
 
@@ -214,6 +214,7 @@ autonomous agent) can self-remediate without a human. Full agent-tenant lifecycl
214
214
  | `knowledge_conflicts` | List potential-conflict article pairs — published articles flagged "too similar to comfortably coexist" by the auto-linker / nightly lint sweep, highest-overlap first. The KB only FLAGS the pair; it does NOT decide redundancy-vs-contradiction — that's your call with live context. Each entry has both articles (id/title/status/category) + similarity. Then merge (supersede one, `knowledge_create` the merged article, or PATCH) or reconcile if they genuinely disagree. Paginated with `total_count` in meta. Agent role. Optional: `limit` (default 50, max 1000, clamped), `offset`. |
215
215
  | `knowledge_resolve_conflict` | Record YOUR verdict on a potential-conflict pair (from `knowledge_conflicts`). Dispositions: `dismiss` (false positive, drops from queue), `supersede` (one wins — pass `authoritative_article_id`; nightly executor links + retires loser, only at `confidence:"high"`, reversible/audited), `merge` (at high confidence an LLM synthesizes both into ONE new DRAFT, sources preserved, never auto-published). Non-destructive at agent role — you record intent, the privileged nightly job executes. Last-write-wins per pair. Required: `source_article_id`, `target_article_id`, `disposition`. Optional: `authoritative_article_id`, `classification`, `evidence`, `confidence`. |
216
216
  | `knowledge_create` | Create a new knowledge article. File findings, document patterns, or record decisions. **Published immediately by default** (visible per `metadata.visibility` — default `owner` for agent authors, only visible to that agent; `shared` for visibility to all agents) — the response `note` says which outcome occurred. Pass `draft: true` to stage it for later review instead (publish afterwards with `knowledge_publish`). Pass `metadata: {visibility: "shared"}` to make the article visible to other agents; higher roles can set visibility and agent_id explicitly. Pass `idempotency_key` for idempotent capture (re-creating with the same key is a no-op returning the existing article — no partial duplicates). Optional: `category`, `tags`, `project_id`, `draft`, `idempotency_key`, `source_type`, `source_id`, `metadata`. |
217
+ | `knowledge_update` | Edit an EXISTING article IN PLACE, **preserving its ID** (IDs are load-bearing — cited in project CLAUDE.mds and cross-links). Fold in a new fact, tidy a hub, retag, or reclassify without churning a new row. Send only the fields to change; `tags` REPLACES the whole array. A changed body/tags re-triggers embedding + auto-linking. Agent role — KB-content curation (reversible + audited); visibility-scoped, so another agent's private/owner memory 404s. Required: `article_id`. Optional: `title`, `body`, `category`, `tags`, `metadata`. |
217
218
  | `knowledge_okf_export` | **Requires `LOOPCTL_USER_KEY`.** Export the wiki as a portable OKF (Open Knowledge Format) v0.1 bundle of markdown files. Writes to `out_dir`, or returns `{files, meta}` inline. |
218
219
  | `knowledge_okf_import` | **Requires `LOOPCTL_USER_KEY`.** Import an OKF v0.1 bundle from a local directory. Creates or (with `merge`) updates articles; tolerates and preserves unknown frontmatter. |
219
220
 
@@ -243,8 +244,8 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
243
244
  | `knowledge_bulk_publish` | **Requires `LOOPCTL_USER_KEY`.** Publish drafts, partial-success style: every valid draft publishes; others are reported per-id as `skipped` (already published — idempotent — or archived/superseded), `not_found`, or `errored`. No 100-id cap (auto-chunked); duplicates ignored; safe to retry. `meta.count` = published; `meta.counts`/`meta.results` give the breakdown. Required: `article_ids` (array). |
244
245
  | `knowledge_unpublish` | **Requires `LOOPCTL_USER_KEY`.** Revert a published article back to draft (hidden from search/context, not deleted). Required: `article_id`. |
245
246
  | `knowledge_bulk_unpublish` | **Requires `LOOPCTL_USER_KEY`.** Revert published articles to draft in bulk, partial-success style (mirror of `knowledge_bulk_publish`): per-id `unpublished`/`skipped` (already draft, or archived/superseded)/`not_found`/`errored`. No 100-id cap (auto-chunked, ≤5000); duplicates ignored; safe to retry. Not deleted (re-publish to restore; `knowledge_bulk_delete` to archive). `meta.count`/`meta.counts`/`meta.results` give the breakdown. Required: `article_ids` (array). |
246
- | `knowledge_archive` | **Requires `LOOPCTL_USER_KEY`.** Soft-delete an article (draft or published). Row retained for audit; hidden from all reads. Required: `article_id`. |
247
- | `knowledge_delete` | **Requires `LOOPCTL_USER_KEY`.** Alias for `knowledge_archive` — DELETE verb on the REST API archives under the hood. Required: `article_id`. |
247
+ | `knowledge_archive` | Soft-delete an article (draft or published). Row retained for audit; hidden from all reads (reversible — re-publish/edit it back). Agent role — KB-content curation, visibility-scoped (another agent's private/owner memory 404s). Required: `article_id`. |
248
+ | `knowledge_delete` | Alias for `knowledge_archive` — DELETE verb on the REST API archives under the hood (soft delete, reversible, audited). Agent role. (Irreversible HARD delete is `knowledge_bulk_delete hard:true`, which stays `LOOPCTL_USER_KEY`.) Required: `article_id`. |
248
249
  | `knowledge_bulk_delete` | **Requires `LOOPCTL_USER_KEY`.** Bulk archive (default, reversible) 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). |
249
250
  | `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`. |
250
251
  | `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`. |
package/index.js CHANGED
@@ -1065,6 +1065,26 @@ async function knowledgeCreate({
1065
1065
  return toContent(result);
1066
1066
  }
1067
1067
 
1068
+ // In-place edit of an existing article (#331). PATCH preserves the ID; only the
1069
+ // provided fields change. Agent role — KB-content curation, visibility-scoped
1070
+ // server-side (another agent's private/owner memory 404s).
1071
+ async function knowledgeUpdate({ article_id, title, body, category, tags, metadata }) {
1072
+ const payload = {};
1073
+ if (title != null) payload.title = title;
1074
+ if (body != null) payload.body = body;
1075
+ if (category != null) payload.category = category;
1076
+ if (tags != null) payload.tags = tags;
1077
+ if (metadata != null) payload.metadata = metadata;
1078
+
1079
+ const result = await apiCall(
1080
+ "PATCH",
1081
+ `/api/v1/articles/${article_id}`,
1082
+ payload,
1083
+ process.env.LOOPCTL_AGENT_KEY,
1084
+ );
1085
+ return toContent(result);
1086
+ }
1087
+
1068
1088
  // --- Agent Memory Tools (US-28.4) ---
1069
1089
  //
1070
1090
  // memory_* is YOUR own scoped, private, accumulated working state — recall
@@ -1237,22 +1257,25 @@ async function knowledgeUnpublish({ article_id }) {
1237
1257
  return toContent(result);
1238
1258
  }
1239
1259
 
1260
+ // #331: single-article archive is agent-role KB curation (reversible soft delete,
1261
+ // audited, visibility-scoped server-side).
1240
1262
  async function knowledgeArchive({ article_id }) {
1241
1263
  const result = await apiCall(
1242
1264
  "POST",
1243
1265
  `/api/v1/articles/${article_id}/archive`,
1244
1266
  null,
1245
- process.env.LOOPCTL_USER_KEY
1267
+ process.env.LOOPCTL_AGENT_KEY
1246
1268
  );
1247
1269
  return toContent(result);
1248
1270
  }
1249
1271
 
1272
+ // #331: soft-delete (archive) is agent-role KB curation, same as knowledge_archive.
1250
1273
  async function knowledgeDelete({ article_id }) {
1251
1274
  const result = await apiCall(
1252
1275
  "DELETE",
1253
1276
  `/api/v1/articles/${article_id}`,
1254
1277
  null,
1255
- process.env.LOOPCTL_USER_KEY
1278
+ process.env.LOOPCTL_AGENT_KEY
1256
1279
  );
1257
1280
  return toContent(result);
1258
1281
  }
@@ -3157,6 +3180,54 @@ const TOOLS = [
3157
3180
  required: ["title", "body"],
3158
3181
  },
3159
3182
  },
3183
+ {
3184
+ name: "knowledge_update",
3185
+ description:
3186
+ "Edit an EXISTING knowledge article IN PLACE, preserving its ID. Use this to fold a " +
3187
+ "new fact into a canonical article, tidy a hub, retag, or reclassify — WITHOUT " +
3188
+ "creating a new row and churning the article ID (IDs are load-bearing: cited in " +
3189
+ "project CLAUDE.mds and cross-links). Send only the fields you want to change; every " +
3190
+ "field is optional except article_id. `tags` REPLACES the whole array (send the full " +
3191
+ "desired set, not a delta). A changed body/tags re-triggers embedding + auto-linking. " +
3192
+ "Agent role — this is KB-content curation (reversible + audited). Visibility-scoped: " +
3193
+ "you can only edit an article you can see, so another agent's private/owner memory " +
3194
+ "returns 404. `tenant_id` is never accepted. Returns the full updated article. To " +
3195
+ "instead retire/replace an article, use knowledge_archive or knowledge_resolve_conflict.",
3196
+ inputSchema: {
3197
+ type: "object",
3198
+ properties: {
3199
+ article_id: {
3200
+ type: "string",
3201
+ description: "The UUID of the article to edit (preserved across the update).",
3202
+ },
3203
+ title: {
3204
+ type: "string",
3205
+ description: "Optional: new title.",
3206
+ },
3207
+ body: {
3208
+ type: "string",
3209
+ description: "Optional: new body (Markdown). Re-triggers embedding + linking.",
3210
+ },
3211
+ category: {
3212
+ type: "string",
3213
+ description: "Optional: new category.",
3214
+ },
3215
+ tags: {
3216
+ type: "array",
3217
+ items: { type: "string" },
3218
+ description: "Optional: REPLACES the whole tags array (send the full desired set).",
3219
+ },
3220
+ metadata: {
3221
+ type: "object",
3222
+ description:
3223
+ "Optional: extensible JSONB (merged/replaced wholesale). For an agent key, an " +
3224
+ "agent-memory `agent_id` is stamped from your verified key identity — you cannot " +
3225
+ "re-attribute a memory to another agent.",
3226
+ },
3227
+ },
3228
+ required: ["article_id"],
3229
+ },
3230
+ },
3160
3231
 
3161
3232
  // Agent Memory Tools (US-28.4)
3162
3233
  {
@@ -3421,9 +3492,10 @@ const TOOLS = [
3421
3492
  name: "knowledge_archive",
3422
3493
  description:
3423
3494
  "Archive an article (soft delete). The article is hidden from search, context, " +
3424
- "and the index but the row is retained for audit/history. Works for drafts and " +
3425
- "published articles. REQUIRES LOOPCTL_USER_KEY (user role orchestrator role is " +
3426
- "NOT sufficient for this destructive operation).",
3495
+ "and the index but the row is retained for audit/history (reversible re-publish " +
3496
+ "or edit it back). Works for drafts and published articles. Agent role KB-content " +
3497
+ "curation. Visibility-scoped: you can only archive an article you can see, so " +
3498
+ "another agent's private/owner memory returns 404.",
3427
3499
  inputSchema: {
3428
3500
  type: "object",
3429
3501
  properties: {
@@ -3440,8 +3512,9 @@ const TOOLS = [
3440
3512
  description:
3441
3513
  "Delete an article. Under the hood this performs the same soft-delete (archive) " +
3442
3514
  "as knowledge_archive — use whichever name is clearer at the call site. The row " +
3443
- "is retained for audit; there is no hard delete. REQUIRES LOOPCTL_USER_KEY (user " +
3444
- "role orchestrator role is NOT sufficient for this destructive operation).",
3515
+ "is retained for audit; there is no hard delete (that is knowledge_bulk_delete " +
3516
+ "hard:true, which stays user-gated). Agent role KB-content curation, reversible + " +
3517
+ "audited, visibility-scoped (another agent's private/owner memory 404s).",
3445
3518
  inputSchema: {
3446
3519
  type: "object",
3447
3520
  properties: {
@@ -4544,6 +4617,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4544
4617
  case "knowledge_create":
4545
4618
  return await knowledgeCreate(args);
4546
4619
 
4620
+ case "knowledge_update":
4621
+ return await knowledgeUpdate(args);
4622
+
4547
4623
  // Agent Memory Tools
4548
4624
  case "memory_remember":
4549
4625
  return await memoryRemember(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.38.0",
3
+ "version": "2.39.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",