@edda-business/mcp 0.69.1 → 0.71.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/package.json +1 -1
- package/src/server.js +253 -39
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -62,18 +62,26 @@ const norm = (s) => String(s ?? "").trim().toLowerCase().replace(/\.md$/, "");
|
|
|
62
62
|
async function resolvePathToId(path) {
|
|
63
63
|
const list = await get("/v2/company/pages");
|
|
64
64
|
const pages = list?.pages ?? [];
|
|
65
|
-
const
|
|
65
|
+
const segs = String(path).split("/").map((x) => x.trim()).filter(Boolean);
|
|
66
|
+
const base = norm(segs[segs.length - 1] ?? "");
|
|
67
|
+
const folderSegs = segs.length > 1 ? segs.slice(0, -1).map(norm) : null;
|
|
66
68
|
let cands = pages.filter((p) => norm(p.name) === base);
|
|
67
|
-
|
|
69
|
+
// EDDA-91: a folder-qualified request MUST honour its folder. The old basename-only match
|
|
70
|
+
// silently substituted an unrelated same-named page from the caller's visible scope when the
|
|
71
|
+
// requested folder was out of scope (or misspelled) — an exact read must return THAT document
|
|
72
|
+
// or a truthful unavailable, never masquerade. The message deliberately does not reveal
|
|
73
|
+
// whether the path exists for someone else.
|
|
74
|
+
if (folderSegs) {
|
|
75
|
+
cands = cands.filter((p) => p.folder && folderSegs.includes(norm(p.folder)));
|
|
76
|
+
}
|
|
77
|
+
if (!cands.length) {
|
|
78
|
+
return { error: folderSegs
|
|
79
|
+
? `"${String(path)}" is not available here — it either does not exist or is outside what this seat can read. Use search to see what you can access.`
|
|
80
|
+
: `No company page named "${String(path).split("/").pop()}". Use search to find the exact path first.` };
|
|
81
|
+
}
|
|
68
82
|
if (cands.length > 1) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (pref.length === 1) return { id: pref[0].id };
|
|
72
|
-
cands = pref.length ? pref : cands;
|
|
73
|
-
if (cands.length > 1) {
|
|
74
|
-
const opts = cands.map((p) => ` • ${[p.folder, p.name].filter(Boolean).join("/")} (id: ${p.id})`).join("\n");
|
|
75
|
-
return { error: `Several pages match "${base}". Pass one of these ids as \`id\`:\n${opts}` };
|
|
76
|
-
}
|
|
83
|
+
const opts = cands.map((p) => ` • ${[p.folder, p.name].filter(Boolean).join("/")} (id: ${p.id})`).join("\n");
|
|
84
|
+
return { error: `Several pages match "${base}". Pass one of these ids as \`id\`:\n${opts}` };
|
|
77
85
|
}
|
|
78
86
|
return { id: cands[0].id };
|
|
79
87
|
}
|
|
@@ -129,10 +137,27 @@ export function createServer() {
|
|
|
129
137
|
if (want === "documents") docs = docs.filter((d) => d.kind === "declared");
|
|
130
138
|
if (want === "distilled") docs = docs.filter((d) => d.kind === "derived");
|
|
131
139
|
docs = docs.slice(0, perLimit);
|
|
132
|
-
|
|
140
|
+
// Claims are what the company currently BELIEVES, as opposed to what a document says. They
|
|
141
|
+
// lead the answer: a settled current-state fact beats a page that merely mentions the words,
|
|
142
|
+
// and each carries its standing so a non-binding figure cannot be read as a commitment.
|
|
143
|
+
const claims = want === "distilled" ? [] : (r?.claims ?? []);
|
|
144
|
+
|
|
145
|
+
if (!docs.length && !claims.length) {
|
|
133
146
|
return text(`Nothing matched "${question}"${want !== "all" ? ` in ${want}` : ""} in the company knowledge.`);
|
|
134
147
|
}
|
|
135
148
|
const lines = [`Company knowledge for "${question}"${want !== "all" ? ` (${want})` : ""}:`, ""];
|
|
149
|
+
if (claims.length) {
|
|
150
|
+
lines.push("CURRENT STATE — what this company believes right now:");
|
|
151
|
+
for (const c of claims) {
|
|
152
|
+
const mark = c.standing && c.standing !== "committed" && c.standing !== "reported" ? ` (${c.standing})` : "";
|
|
153
|
+
const infer = c.confidence === "inferred" ? " (inferred)" : "";
|
|
154
|
+
const label = String(c.predicate ?? "").replace(/[_:]/g, " ");
|
|
155
|
+
lines.push(`[claim] ${c.subject ? `${c.subject} · ` : ""}${label}${mark}${infer}: ${c.object}`);
|
|
156
|
+
lines.push(` as of ${String(c.as_of ?? "").slice(0, 10)} · evidence ${c.source?.kind}:${c.source?.id}`);
|
|
157
|
+
}
|
|
158
|
+
lines.push("");
|
|
159
|
+
}
|
|
160
|
+
if (docs.length) lines.push("DOCUMENTS:", "");
|
|
136
161
|
for (const d of docs) {
|
|
137
162
|
const declared = d.kind === "declared";
|
|
138
163
|
const tag = declared ? "[company]" : "[company·distilled]";
|
|
@@ -250,39 +275,192 @@ export function createServer() {
|
|
|
250
275
|
// ===========================================================================
|
|
251
276
|
server.tool(
|
|
252
277
|
"submit_company_candidate",
|
|
253
|
-
"SHARE
|
|
254
|
-
"Edda's Inbox.
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
"is
|
|
278
|
+
"SHARE something up into the COMPANY knowledge — stage one user-approved item into the company " +
|
|
279
|
+
"Edda's Inbox. Takes EITHER markdown you wrote (`content`) OR a real file (`file_base64`): a " +
|
|
280
|
+
"proposal, invoice, report, financial model or scan. The server extracts the text from PDF, " +
|
|
281
|
+
"Word, Excel, images, CSV and plain text, so send the file itself rather than your summary of " +
|
|
282
|
+
"it — a summary loses the numbers. You do NOT pick where it lands: the company routes it into " +
|
|
283
|
+
"the right folder on the next reconcile, and you may `suggest` a destination as a hint. " +
|
|
284
|
+
"Optionally attach `claims` for what this CHANGES about what the company believes — an update " +
|
|
285
|
+
"to existing truth is not a new document. Only stage what the person has approved sharing: " +
|
|
286
|
+
"this leaves their private vault. This stages PAGES and FILES, never folders — for a folder " +
|
|
287
|
+
"call `create`, which is direct and does not go through the Inbox.",
|
|
261
288
|
{
|
|
262
|
-
name: z.string().describe("File/page name, e.g. 'MCP Overview EFB'. A '.md' suffix is added if missing."),
|
|
263
|
-
content: z.string().describe("The full markdown content
|
|
289
|
+
name: z.string().describe("File/page name, e.g. 'MCP Overview EFB' or 'Q3 invoices.xlsx'. A '.md' suffix is added to markdown items if missing."),
|
|
290
|
+
content: z.string().optional().describe("The full markdown content, when you are sharing text you wrote. Omit when sending a FILE via file_base64."),
|
|
291
|
+
file_base64: z.string().optional().describe(
|
|
292
|
+
"A real FILE, base64-encoded — a proposal, invoice, report, financial model, scan. The server extracts the text: " +
|
|
293
|
+
"PDF (scans are OCR'd), Word, Excel and legacy .xls (one heading per sheet), images (OCR'd), CSV and plain text. " +
|
|
294
|
+
"A format with no readable text (.zip, .dwg, an installer) is refused with a reason rather than filed as an empty page. " +
|
|
295
|
+
"Send the file itself rather than pasting your own summary of it: the extracted text is what makes it searchable, " +
|
|
296
|
+
"and a summary loses the numbers."
|
|
297
|
+
),
|
|
298
|
+
mime: z.string().optional().describe("The file's MIME type when known, e.g. application/pdf. Helps route the extractor; the filename extension is used as a fallback."),
|
|
264
299
|
suggested: z.string().optional().describe("Optional destination HINT, e.g. 'Projects/Data Centre/Working Documents' or 'General Edda'. The company still resolves/creates and places it — this only nudges."),
|
|
265
300
|
grade: z.enum(["fact", "summary", "full"]).optional().describe("How much of the source this carries: a single fact, a summary, or the full document."),
|
|
266
|
-
audience: z.enum(["company-wide", "department", "owner", "filing-agent-decides"]).optional().describe(
|
|
301
|
+
audience: z.enum(["company-wide", "department", "owner", "filing-agent-decides"]).optional().describe(
|
|
302
|
+
"The MOST this may be seen by — an upper bound the company may narrow, never broaden. " +
|
|
303
|
+
"LEAVE THIS UNSET unless the person asked you to restrict it: the default lets the company decide, which is almost always right for something being shared UP into the shared brain. " +
|
|
304
|
+
"'owner' means nobody but the submitter can ever see it, including the admin reviewing the Inbox — it does not mean 'be careful', it means the contribution is invisible and has to be promoted by hand. " +
|
|
305
|
+
"Choose it only when the person explicitly said this must stay private to them."
|
|
306
|
+
),
|
|
267
307
|
idempotency_key: z.string().optional().describe("Optional stable key so re-submitting the same approved item files it at most once."),
|
|
308
|
+
claims: z.array(z.object({
|
|
309
|
+
predicate: z.string().describe("What is being claimed. Tier 1 (bare): status | next_step | owner | risk | blocker | decision. Tier 2 (root:qualifier): sentiment:<topic> | doc_status:<name> | position:<topic> | availability:<topic>."),
|
|
310
|
+
object: z.string().describe("The value, in plain words, e.g. 'received — Tom is unhappy with the covenant'."),
|
|
311
|
+
standing: z.enum(["committed", "reported", "indicative", "contested"]).optional().describe("How FIRM the fact is, independent of who said it. committed = signed/agreed/done · reported = a source states it, unverified · indicative = explicitly non-binding, draft, conditional · contested = disputed. Default reported. Get this right: an indicative figure recorded as fact reads as a commitment."),
|
|
312
|
+
as_of: z.string().optional().describe("When it became TRUE (YYYY-MM-DD), not when you are writing it down. A late note about an old fact must not overwrite newer knowledge."),
|
|
313
|
+
subject_kind: z.enum(["project", "person", "company", "page"]).optional().describe("Default project — the project this is filed into."),
|
|
314
|
+
subject_id: z.string().optional().describe("Only when the subject is NOT the destination project."),
|
|
315
|
+
confidence: z.enum(["stated", "inferred"]).optional().describe("stated = the source says it outright; inferred = you read it between the lines."),
|
|
316
|
+
})).optional().describe(
|
|
317
|
+
"What this material CHANGES about what the company believes — the point of sharing an update rather than a document. " +
|
|
318
|
+
"'The term sheet arrived and Tom is unhappy with it' is not a document, it is two claims. " +
|
|
319
|
+
"These are PROPOSALS: they appear in the review gate as a belief diff (what we believed → what you are asserting) " +
|
|
320
|
+
"and are applied only when a person approves the item. Cite nothing yourself — the filed document becomes the evidence."
|
|
321
|
+
),
|
|
268
322
|
},
|
|
269
|
-
async ({ name, content, suggested, grade, audience, idempotency_key }) => {
|
|
323
|
+
async ({ name, content, file_base64, mime, suggested, grade, audience, idempotency_key, claims }) => {
|
|
270
324
|
try {
|
|
271
|
-
|
|
325
|
+
if (!content && !file_base64) return text(`Give "${name}" either markdown content or a file (file_base64) to stage.`);
|
|
326
|
+
const r = await post("/v2/company/river/stage", { name, content, file_base64, mime, suggested, grade, audience, idempotency_key, claims });
|
|
272
327
|
const rc = r?.receipt ?? r;
|
|
273
328
|
if (rc?.idempotent) return text(`Already staged — "${name}" is in the company Inbox (${rc.location}). Nothing duplicated.`);
|
|
274
329
|
if (rc?.rejected === "hash_mismatch") return text(`Couldn't stage "${name}": the content didn't match its declared checksum (it may have changed after approval). Re-capture and try again.`);
|
|
275
330
|
if (rc?.staged === false) return text(`Couldn't stage "${name}": ${rc?.reason ?? "unknown error"}.`);
|
|
276
|
-
|
|
331
|
+
const n = Array.isArray(claims) ? claims.length : 0;
|
|
332
|
+
// Say what is waiting, including the belief change — the person needs to know a claim is
|
|
333
|
+
// pending a human decision, not quietly applied.
|
|
334
|
+
return text(
|
|
335
|
+
`Staged "${name}" into the company Inbox (${rc?.location ?? "Inbox"}). The company will route it to the right folder on the next reconcile.`
|
|
336
|
+
+ (n ? `\n${n} proposed knowledge update${n === 1 ? "" : "s"} rides with it — shown in the review gate as a belief change and applied only on approval.` : ""),
|
|
337
|
+
);
|
|
277
338
|
} catch (e) {
|
|
278
339
|
const msg = String(e?.message || e);
|
|
279
|
-
if (msg.includes("admins_only") || msg.includes("(403)")) return text("
|
|
340
|
+
if (msg.includes("read_only") || msg.includes("admins_only") || msg.includes("(403)")) return text("This seat is read-only on the company Edda, so it can search and read but not contribute. Ask an admin for a write role.");
|
|
341
|
+
if (msg.includes("no_readable_text")) return text("That file has no text to extract \u2014 it is a binary format. The River reads PDF, Word, Excel, images and scans (OCR\u2019d), CSV and plain text. Filing it as a page would create something that looks like knowledge and answers nothing.");
|
|
280
342
|
if (msg.includes("name_required")) return text("Give the item a name and content to stage.");
|
|
281
343
|
throw e;
|
|
282
344
|
}
|
|
283
345
|
},
|
|
284
346
|
);
|
|
285
347
|
|
|
348
|
+
// ===========================================================================
|
|
349
|
+
// TOOL: submit_company_batch — POST /v2/company/river/stage-batch
|
|
350
|
+
// One periodic ingest run = one submission. A seat's ingest sweeps the last few hours of email,
|
|
351
|
+
// transcripts, notes and attachments, and produces a handful of documents plus the belief
|
|
352
|
+
// changes they imply. That is one arrival, not fifteen.
|
|
353
|
+
// ===========================================================================
|
|
354
|
+
server.tool(
|
|
355
|
+
"submit_company_batch",
|
|
356
|
+
"SHARE SEVERAL things up into the COMPANY knowledge in one call — the batch form of " +
|
|
357
|
+
"submit_company_candidate, for a periodic ingest run rather than a single share. Use this when " +
|
|
358
|
+
"a sweep produced multiple documents and belief changes at once: the contracts and invoices " +
|
|
359
|
+
"that arrived, plus what they change about what the company believes. Everything lands in the " +
|
|
360
|
+
"company Inbox; you never choose the final destination. A per-item failure does not stop the " +
|
|
361
|
+
"rest — each item gets its own receipt saying what happened. Up to 100 items. " +
|
|
362
|
+
"IMPORTANT: attach `claims` to the item they came from rather than writing a separate " +
|
|
363
|
+
"'here is what changed' document — a belief change carried as structured claims is applied by " +
|
|
364
|
+
"the reviewer's single approval, whereas one buried in prose has to be read and re-derived by " +
|
|
365
|
+
"whoever files it.",
|
|
366
|
+
{
|
|
367
|
+
items: z.array(z.object({
|
|
368
|
+
name: z.string().describe("File/page name, e.g. 'Cook Street construction proposal.pdf'."),
|
|
369
|
+
content: z.string().optional().describe("Markdown content, when sharing text you wrote. Omit when sending a file."),
|
|
370
|
+
file_base64: z.string().optional().describe("The file itself, base64-encoded — PDF, Word, Excel, image, CSV. Send the file rather than a summary of it."),
|
|
371
|
+
mime: z.string().optional().describe("MIME type when known."),
|
|
372
|
+
suggested: z.string().optional().describe("Optional destination HINT. A path you cannot see is dropped, and the item still lands in the Inbox."),
|
|
373
|
+
grade: z.enum(["fact", "summary", "full"]).optional(),
|
|
374
|
+
audience: z.enum(["company-wide", "department", "owner", "filing-agent-decides"]).optional().describe("Leave unset unless the person asked to restrict this item. 'owner' makes it invisible to everyone including the reviewer."),
|
|
375
|
+
idempotency_key: z.string().optional().describe("Stable key so a re-run of the same ingest files each item at most once. Strongly recommended for a periodic sweep."),
|
|
376
|
+
claims: z.array(z.object({
|
|
377
|
+
predicate: z.string().describe("status | next_step | owner | risk | blocker | decision, or a qualified root: sentiment:<topic> | doc_status:<name> | position:<topic> | availability:<topic>."),
|
|
378
|
+
object: z.string().describe("The value in plain words."),
|
|
379
|
+
standing: z.enum(["committed", "reported", "indicative", "contested"]).optional().describe("How firm the fact is. committed = signed/agreed/done · reported = a source states it · indicative = non-binding, draft, conditional · contested = disputed."),
|
|
380
|
+
as_of: z.string().optional().describe("When it became TRUE (YYYY-MM-DD), not when you wrote it down."),
|
|
381
|
+
subject_kind: z.enum(["project", "person", "company", "page"]).optional(),
|
|
382
|
+
subject_id: z.string().optional(),
|
|
383
|
+
confidence: z.enum(["stated", "inferred"]).optional(),
|
|
384
|
+
})).optional().describe("What THIS item changes about what the company believes."),
|
|
385
|
+
})).max(100).describe("The items from one ingest run."),
|
|
386
|
+
},
|
|
387
|
+
async ({ items }) => {
|
|
388
|
+
try {
|
|
389
|
+
const r = await post("/v2/company/river/stage-batch", { candidates: items });
|
|
390
|
+
const staged = r?.staged ?? 0;
|
|
391
|
+
const failed = r?.failed ?? 0;
|
|
392
|
+
const lines = [`Staged ${staged} item(s) into the company Inbox${failed ? `, ${failed} rejected` : ""}.`];
|
|
393
|
+
for (const rec of r?.receipts ?? []) {
|
|
394
|
+
if (rec.staged === false) lines.push(` ✗ ${rec.name ?? "(unnamed)"} — ${rec.reason ?? rec.rejected ?? "unknown"}`);
|
|
395
|
+
else if (rec.suggestion_dropped) lines.push(` • ${rec.name} — staged; your suggested destination was dropped (not visible to you)`);
|
|
396
|
+
}
|
|
397
|
+
lines.push("", "They wait for a person to review and file them. Nothing has been placed in a project yet.");
|
|
398
|
+
return text(lines.join("\n"));
|
|
399
|
+
} catch (e) {
|
|
400
|
+
const msg = String(e?.message || e);
|
|
401
|
+
if (msg.includes("read_only") || msg.includes("(403)")) return text("This seat is read-only on the company Edda, so it cannot contribute.");
|
|
402
|
+
if (msg.includes("too_many")) return text("Too many items — send at most 100 per call.");
|
|
403
|
+
throw e;
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// ===========================================================================
|
|
409
|
+
// TOOL: submit_belief_update — POST /v2/company/river/stage-claims
|
|
410
|
+
// A correction with no document. The last piece of "updates to existing truth, not necessarily
|
|
411
|
+
// specific documents": until this existed, an agent holding a pure correction had to invent a
|
|
412
|
+
// document to carry it.
|
|
413
|
+
// ===========================================================================
|
|
414
|
+
server.tool(
|
|
415
|
+
"submit_belief_update",
|
|
416
|
+
"CORRECT what the company believes about a project, when NO new document arrived. Use this " +
|
|
417
|
+
"when something already filed is now out of date: the term sheet you filed last week has been " +
|
|
418
|
+
"rejected, the deadline moved, the owner changed. It creates NO page — approving it updates " +
|
|
419
|
+
"the project's current state and nothing else. " +
|
|
420
|
+
"Use submit_company_candidate instead whenever actual material arrived (a file, a note, a " +
|
|
421
|
+
"transcript); the claims ride along with it and one approval applies both. " +
|
|
422
|
+
"NEVER write a document whose only purpose is to describe a change — that is what this tool " +
|
|
423
|
+
"replaces. " +
|
|
424
|
+
"`source_refs` is required and must point at evidence ALREADY filed and live in the company " +
|
|
425
|
+
"brain: a correction has to point at something. If you have nothing to point at, what you are " +
|
|
426
|
+
"holding is new material — submit it as a document. Like every contribution this waits in the " +
|
|
427
|
+
"company Inbox for a person to approve.",
|
|
428
|
+
{
|
|
429
|
+
folder_id: z.string().describe("The project whose current state changes. Get it from `orientation` or `search`."),
|
|
430
|
+
claims: z.array(z.object({
|
|
431
|
+
predicate: z.string().describe("status | next_step | owner | risk | blocker | decision, or a qualified root: sentiment:<topic> | doc_status:<name> | position:<topic> | availability:<topic>."),
|
|
432
|
+
object: z.string().describe("What is true now, in plain words."),
|
|
433
|
+
standing: z.enum(["committed", "reported", "indicative", "contested"]).optional().describe("committed = signed/agreed/done · reported = a source states it · indicative = non-binding, draft, conditional · contested = disputed. A weaker claim never retires a firmer one."),
|
|
434
|
+
as_of: z.string().optional().describe("When it became TRUE (YYYY-MM-DD), not when you learned it."),
|
|
435
|
+
confidence: z.enum(["stated", "inferred"]).optional(),
|
|
436
|
+
})).max(25).describe("The beliefs that changed. One per fact — do not pack two facts into one sentence."),
|
|
437
|
+
source_refs: z.array(z.object({
|
|
438
|
+
kind: z.enum(["company_file", "river_original", "river_receipt"]),
|
|
439
|
+
id: z.string(),
|
|
440
|
+
})).describe("Evidence already live in the brain that this correction rests on."),
|
|
441
|
+
note: z.string().optional().describe("One line of context for the reviewer, if the claims alone would not explain themselves."),
|
|
442
|
+
idempotency_key: z.string().optional().describe("Stable key so a re-run of the same ingest proposes this at most once."),
|
|
443
|
+
},
|
|
444
|
+
async ({ folder_id, claims, source_refs, note, idempotency_key }) => {
|
|
445
|
+
try {
|
|
446
|
+
const r = await post("/v2/company/river/stage-claims", { folder_id, claims, source_refs, note, idempotency_key });
|
|
447
|
+
const lines = claims.map((c) => ` · ${String(c.predicate).replace(/[_:]/g, " ")}: ${c.object}${c.standing ? ` (${c.standing})` : ""}`);
|
|
448
|
+
return text(
|
|
449
|
+
`Belief update waiting in the company Inbox${r?.idempotent ? " (already submitted earlier — not duplicated)" : ""}:\n`
|
|
450
|
+
+ lines.join("\n")
|
|
451
|
+
+ "\n\nA person reviews it. Approving updates the project's current state; no document is created."
|
|
452
|
+
);
|
|
453
|
+
} catch (e) {
|
|
454
|
+
const msg = String(e?.message || e);
|
|
455
|
+
if (msg.includes("source_refs_required")) return text("A correction has to cite evidence already filed in the brain. If you have none, submit the material as a document with submit_company_candidate instead.");
|
|
456
|
+
if (msg.includes("bad_source_refs")) return text(`That evidence could not be verified: ${msg}. Cite only documents that are already filed and live.`);
|
|
457
|
+
if (msg.includes("folder_not_visible")) return text("You do not have access to that project, so you cannot change what the company believes about it.");
|
|
458
|
+
if (msg.includes("read_only") || msg.includes("(403)")) return text("This seat is read-only on the company Edda, so it cannot contribute.");
|
|
459
|
+
throw e;
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
);
|
|
463
|
+
|
|
286
464
|
// ===========================================================================
|
|
287
465
|
// TOOL: inbox — GET /v2/company/inbox
|
|
288
466
|
// The waiting room the agent-run ingest reads (Decision Log 2026-09-04: the agent is
|
|
@@ -290,24 +468,60 @@ export function createServer() {
|
|
|
290
468
|
// ===========================================================================
|
|
291
469
|
server.tool(
|
|
292
470
|
"inbox",
|
|
293
|
-
"LIST what is waiting in the company Inbox
|
|
294
|
-
"submitter
|
|
295
|
-
"company ingest: read each item (use `read` for the full text if the
|
|
296
|
-
"decide where it belongs among the real projects, then file it with " +
|
|
297
|
-
"reconcile { stagingId, path }.
|
|
298
|
-
|
|
299
|
-
|
|
471
|
+
"LIST what is waiting in the company Inbox, AND what was decided about the things you sent. " +
|
|
472
|
+
"Each waiting item comes with its id, submitter, suggested destination and a content preview. " +
|
|
473
|
+
"This is the start of the company ingest: read each item (use `read` for the full text if the " +
|
|
474
|
+
"preview is not enough), decide where it belongs among the real projects, then file it with " +
|
|
475
|
+
"reconcile { stagingId, path }. An ADMIN sees everything waiting; a MEMBER sees only their own " +
|
|
476
|
+
"submissions. Pass `since` (ISO timestamp) to see only decisions made after your last run — " +
|
|
477
|
+
"that is the cursor a periodic ingest should keep, so it can tell its user what was filed and " +
|
|
478
|
+
"what was rejected, and stop re-sending things that were turned down.",
|
|
479
|
+
{
|
|
480
|
+
since: z.string().optional().describe("ISO timestamp of your last ingest run. Decisions after it are reported back; omit for the last 7 days."),
|
|
481
|
+
},
|
|
482
|
+
async ({ since }) => {
|
|
300
483
|
try {
|
|
484
|
+
// Two questions in one call, because a periodic ingest asks both every time: what still
|
|
485
|
+
// needs filing, and what happened to what I already sent. The second half is the only
|
|
486
|
+
// signal a submitting agent ever gets — without it, it re-sends rejected items forever.
|
|
487
|
+
const cursor = since || new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString();
|
|
488
|
+
const decided = await get(`/v2/company/river/receipts?limit=50&since=${encodeURIComponent(cursor)}`)
|
|
489
|
+
.then((d) => d?.receipts ?? [])
|
|
490
|
+
.catch(() => []);
|
|
491
|
+
const outcomes = decided.length
|
|
492
|
+
? "\n\n" + `Decided since ${cursor.slice(0, 16).replace("T", " ")}:\n`
|
|
493
|
+
+ decided.map((d) => {
|
|
494
|
+
const head = d.status === "rejected" ? `✗ ${d.name} — rejected` : `✓ ${d.name} → ${d.location}`;
|
|
495
|
+
const why = d.detail ? ` — ${d.detail}` : (d.status === "rejected" ? " (no reason given)" : "");
|
|
496
|
+
const cl = d.claims_asserted ? ` · updated ${d.claims_asserted} belief(s)` : "";
|
|
497
|
+
return ` ${head}${why}${cl}`;
|
|
498
|
+
}).join("\n")
|
|
499
|
+
: "";
|
|
500
|
+
|
|
301
501
|
const r = await get("/v2/company/inbox");
|
|
302
502
|
const pending = r?.pending ?? [];
|
|
303
|
-
if (!pending.length) return text(
|
|
304
|
-
const lines = pending.map((f) =>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
503
|
+
if (!pending.length) return text(`The company Inbox is empty — nothing waiting.${outcomes}`);
|
|
504
|
+
const lines = pending.map((f) => {
|
|
505
|
+
// The belief diff, beside the destination. An admin running the ingest is deciding on
|
|
506
|
+
// BOTH — where the document goes and what it changes about what the company believes —
|
|
507
|
+
// and the second one is invisible unless it is written down here.
|
|
508
|
+
const changes = (f.changes ?? []).map((c) => {
|
|
509
|
+
if (c.invalid) return ` ! ${c.predicate}: ${c.invalid}`;
|
|
510
|
+
const mark = c.standing && c.standing !== "committed" && c.standing !== "reported" ? ` (${c.standing})` : "";
|
|
511
|
+
const label = String(c.predicate).replace(/[_:]/g, " ");
|
|
512
|
+
return c.from
|
|
513
|
+
? ` ${label}${mark}: ${c.from} → ${c.to}`
|
|
514
|
+
: ` ${label}${mark}: (new) ${c.to}`;
|
|
515
|
+
});
|
|
516
|
+
return `• ${f.name} (stagingId: ${f.id})\n staged ${String(f.created_at).slice(0, 10)}`
|
|
517
|
+
+ `${f.suggested ? ` · suggested: ${f.suggested}` : ""}`
|
|
518
|
+
+ `\n ${String(f.preview || "").replace(/\s+/g, " ").slice(0, 240)}`
|
|
519
|
+
+ (changes.length ? `\n changes what we believe:\n${changes.join("\n")}` : "");
|
|
520
|
+
});
|
|
521
|
+
return text(`${pending.length} item(s) waiting in the company Inbox:\n\n${lines.join("\n\n")}${outcomes}`);
|
|
308
522
|
} catch (e) {
|
|
309
523
|
const msg = String(e?.message || e);
|
|
310
|
-
if (msg.includes("
|
|
524
|
+
if (msg.includes("read_only") || msg.includes("(403)")) return text("This seat is read-only on the company Edda, so it has nothing waiting in the Inbox to show.");
|
|
311
525
|
throw e;
|
|
312
526
|
}
|
|
313
527
|
},
|