@orboto/mcp 0.192.0 → 0.194.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/dist/event-bridge.js +1 -47
- package/dist/event-bridge.test.js +0 -19
- package/dist/http-transport-binding.test.js +1 -4
- package/dist/http-transport.js +1 -213
- package/dist/http-transport.test.js +0 -56
- package/dist/index.js +1 -48
- package/dist/input-schema.js +2 -76
- package/dist/input-schema.test.js +1 -15
- package/dist/instructions-budget.test.js +0 -2
- package/dist/manifest-size.test.js +3 -72
- package/dist/oauth-bootstrap.js +0 -47
- package/dist/oauth-bootstrap.test.js +4 -16
- package/dist/orboto-client.js +0 -30
- package/dist/orboto-client.test.js +0 -12
- package/dist/prompts.js +0 -39
- package/dist/protect-text-usage.test.js +1 -9
- package/dist/required-rules-wrapper.test.js +0 -5
- package/dist/required-rules.js +0 -2
- package/dist/resources.js +0 -62
- package/dist/resources.test.js +0 -16
- package/dist/response-budget.js +2 -150
- package/dist/response-budget.test.js +1 -32
- package/dist/server.js +4 -180
- package/dist/session-nudge.js +0 -9
- package/dist/session-nudge.test.js +2 -10
- package/dist/session-nudge.transport.test.js +0 -23
- package/dist/tool-annotations.test.js +0 -44
- package/dist/tool-docs.js +1 -12
- package/dist/tool-docs.test.js +2 -8
- package/dist/tool-parameters.test.js +0 -19
- package/dist/tools/absence-writes.js +1 -17
- package/dist/tools/admin-translations.js +0 -11
- package/dist/tools/admin-writes.js +0 -28
- package/dist/tools/admin-writes.test.js +0 -1
- package/dist/tools/agent-coordination.js +2 -24
- package/dist/tools/agent-coordination.test.js +0 -2
- package/dist/tools/agent-instructions.js +0 -10
- package/dist/tools/agent-instructions.test.js +0 -2
- package/dist/tools/agent-messages.js +0 -1
- package/dist/tools/ai-status.js +0 -14
- package/dist/tools/ai-usage.js +0 -10
- package/dist/tools/analytics.js +0 -2
- package/dist/tools/api-call.js +0 -12
- package/dist/tools/api-call.test.js +1 -2
- package/dist/tools/api-search.js +1 -6
- package/dist/tools/approvals.js +0 -6
- package/dist/tools/attach.js +0 -25
- package/dist/tools/backup.js +0 -13
- package/dist/tools/backup.test.js +1 -2
- package/dist/tools/bulk-create.js +1 -25
- package/dist/tools/bulk-create.test.js +0 -4
- package/dist/tools/bulk-writes.js +69 -61
- package/dist/tools/bulk-writes.test.js +34 -4
- package/dist/tools/check-similar.js +0 -17
- package/dist/tools/checklist-writes.js +82 -27
- package/dist/tools/checklist-writes.test.js +69 -1
- package/dist/tools/claim.js +1 -71
- package/dist/tools/claim.test.js +0 -2
- package/dist/tools/critical-path.js +0 -5
- package/dist/tools/critical-path.test.js +0 -3
- package/dist/tools/cross-project-links.ee.js +1 -24
- package/dist/tools/doc-attachments.js +0 -28
- package/dist/tools/doc-attachments.test.js +1 -4
- package/dist/tools/doc-comments.js +1 -37
- package/dist/tools/doc-comments.test.js +1 -2
- package/dist/tools/doc-edits.js +1 -31
- package/dist/tools/doc-edits.test.js +0 -9
- package/dist/tools/doc-export.js +0 -24
- package/dist/tools/doc-export.test.js +0 -12
- package/dist/tools/doc-revisions.js +0 -23
- package/dist/tools/docs-ai.js +0 -34
- package/dist/tools/docs-ai.test.js +0 -11
- package/dist/tools/docs.js +1 -75
- package/dist/tools/docs.test.js +1 -3
- package/dist/tools/embedding-status.js +0 -13
- package/dist/tools/embedding-status.test.js +0 -1
- package/dist/tools/feedback.js +1 -15
- package/dist/tools/feedback.test.js +0 -1
- package/dist/tools/get-checklists.js +6 -14
- package/dist/tools/get-project-primer.js +1 -25
- package/dist/tools/get-project.js +0 -17
- package/dist/tools/get-ticket.js +1 -61
- package/dist/tools/get-timer.js +0 -11
- package/dist/tools/git-health-reasons.js +0 -1
- package/dist/tools/identity.js +0 -13
- package/dist/tools/list-projects.js +0 -20
- package/dist/tools/list-projects.test.js +0 -1
- package/dist/tools/list-tickets.js +10 -9
- package/dist/tools/milestones-crud.test.js +0 -4
- package/dist/tools/milestones.js +0 -43
- package/dist/tools/my-tickets.js +0 -7
- package/dist/tools/phase-b-tools.test.js +14 -21
- package/dist/tools/primer-facts.js +1 -49
- package/dist/tools/primer-facts.test.js +0 -20
- package/dist/tools/project-listings.js +0 -25
- package/dist/tools/query.js +1 -12
- package/dist/tools/query.test.js +0 -8
- package/dist/tools/raci.js +0 -11
- package/dist/tools/requirements-spec.js +1 -12
- package/dist/tools/response-expand.js +0 -19
- package/dist/tools/review-policy.js +0 -24
- package/dist/tools/review-policy.test.js +0 -2
- package/dist/tools/search.js +1 -13
- package/dist/tools/session-start.js +1 -136
- package/dist/tools/session-start.test.js +1 -44
- package/dist/tools/set-parent.js +1 -19
- package/dist/tools/shared.js +0 -41
- package/dist/tools/shared.test.js +0 -4
- package/dist/tools/similar-projection.js +0 -15
- package/dist/tools/ticket-attachments.js +1 -25
- package/dist/tools/ticket-schedules.js +0 -9
- package/dist/tools/ticket-schedules.test.js +0 -1
- package/dist/tools/ticket-writes.js +1 -110
- package/dist/tools/ticket-writes.test.js +1 -15
- package/dist/tools/time-writes.js +26 -46
- package/dist/tools/time-writes.test.js +18 -0
- package/dist/tools/update-project.js +1 -27
- package/dist/tools/update-project.test.js +1 -1
- package/dist/tools/wiki.js +0 -8
- package/dist/tools/work-next.js +0 -5
- package/dist/tools/work-sessions-start.test.js +0 -2
- package/dist/tools/work-sessions.js +0 -48
- package/dist/tools/work-sessions.test.js +0 -5
- package/dist/toolset.js +2 -81
- package/dist/toolset.test.js +0 -20
- package/dist/version-guard.test.js +0 -4
- package/dist/version.js +0 -12
- package/dist/with-metrics-output-schema.test.js +1 -14
- package/dist/with-metrics.js +2 -74
- package/dist/with-metrics.test.js +7 -29
- package/package.json +1 -1
package/dist/tool-docs.js
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-1741 - the manifest diet (epic ORB-1691).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* eager-loading client pays it on connect, every session. The curated
|
|
6
|
-
* toolset (ORB-1520) cut the tool COUNT; this module cuts the per-tool
|
|
7
|
-
* TEXT. At registration time (with-metrics.ts) the full description is
|
|
8
|
-
* captured here and the wire manifest carries only a one-sentence
|
|
9
|
-
* summary; `orboto_help { tool }` serves the full guidance lazily - the
|
|
10
|
-
* same deferred-docs pattern ToolSearch uses for deferred tools.
|
|
11
|
-
*
|
|
12
|
-
* The full texts stay where they always lived - in each tool file's
|
|
13
|
-
* `description` - so authors keep writing complete guidance and nothing
|
|
14
|
-
* is lost: the summary is DERIVED (first sentence, or a hand-written
|
|
15
|
-
* override for the few whose first sentence overruns the cap).
|
|
4
|
+
* @see ORB-1520
|
|
16
5
|
*/
|
|
17
6
|
/** Soft cap for a wire description; the ratchet in manifest-size.test.ts
|
|
18
7
|
* enforces the aggregate outcome. */
|
package/dist/tool-docs.test.js
CHANGED
|
@@ -34,7 +34,7 @@ describe('summarizeToolDescription', () => {
|
|
|
34
34
|
const s = summarizeToolDescription('orboto_unknown_tool', long);
|
|
35
35
|
expect(s.length).toBeLessThanOrEqual(SUMMARY_MAX_CHARS);
|
|
36
36
|
expect(s.endsWith('...')).toBe(true);
|
|
37
|
-
expect(s).not.toMatch(/\swor\.\.\.$/);
|
|
37
|
+
expect(s).not.toMatch(/\swor\.\.\.$/);
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
describe('orboto_help handler', () => {
|
|
@@ -67,20 +67,14 @@ describe('live manifest vs help registry (no guidance lost)', () => {
|
|
|
67
67
|
expect(tools.length).toBeGreaterThan(150);
|
|
68
68
|
for (const t of tools) {
|
|
69
69
|
const wire = t.description ?? '';
|
|
70
|
-
// Acceptance: no registered wire description exceeds the cap.
|
|
71
70
|
expect(wire.length, `${t.name} wire description too long`).toBeLessThanOrEqual(SUMMARY_MAX_CHARS);
|
|
72
|
-
// The full text is behind the registry and STARTS where the
|
|
73
|
-
// summary came from (override tools aside, whose full text is
|
|
74
|
-
// still captured verbatim).
|
|
75
71
|
const full = getToolDoc(t.name);
|
|
76
72
|
expect(full, `${t.name} missing from the help registry`).toBeTruthy();
|
|
77
73
|
expect(summarizeToolDescription(t.name, full)).toBe(wire);
|
|
78
74
|
}
|
|
79
|
-
// Spot-check the measured worst offender end to end via the tool.
|
|
80
75
|
const res = await client.callTool({ name: 'orboto_help', arguments: { tool: 'orboto_create_ticket' } });
|
|
81
|
-
// First block can be the ORB-1331 session-start nudge - join all.
|
|
82
76
|
const text = res.content.map((c) => c.text ?? '').join('\n');
|
|
83
|
-
expect(text.length).toBeGreaterThan(1000);
|
|
77
|
+
expect(text.length).toBeGreaterThan(1000);
|
|
84
78
|
expect(text).toContain('Duplicate-detection safety-net');
|
|
85
79
|
}
|
|
86
80
|
finally {
|
|
@@ -1,24 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-1805 - the schema diet must never cost a PARAMETER.
|
|
3
|
-
*
|
|
4
|
-
* The diet trims description text: tool prose moved into
|
|
5
|
-
* `orboto_help` / the skill's REFERENCE.md, per-parameter prose
|
|
6
|
-
* shortened to the phrase a caller acts on. What it must never do is
|
|
7
|
-
* drop a field, flip a required field to optional, or shrink an enum -
|
|
8
|
-
* those are the changes that silently break a caller.
|
|
9
|
-
*
|
|
10
|
-
* `tool-parameters.baseline.json` is the snapshot taken from the FULL
|
|
11
|
-
* manifest immediately BEFORE the ORB-1805 diet: for every tool, every
|
|
12
|
-
* property path in its input schema (nested objects and array items
|
|
13
|
-
* included, as dotted paths), its required list, and every enum's
|
|
14
|
-
* values. This test walks the live manifest the same way and fails on
|
|
15
|
-
* any loss.
|
|
16
|
-
*
|
|
17
|
-
* Direction matters: it is a SUPERSET check. Adding a parameter, making
|
|
18
|
-
* a required one optional-to-required is caught, and adding an enum
|
|
19
|
-
* value is fine - only removal fails. Re-baseline ONLY when a parameter
|
|
20
|
-
* is intentionally removed (which is an API break in its own right and
|
|
21
|
-
* belongs in its own ticket), never to make this test go quiet.
|
|
22
3
|
*/
|
|
23
4
|
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
|
24
5
|
import { readFileSync } from 'node:fs';
|
|
@@ -2,19 +2,9 @@
|
|
|
2
2
|
* ORB-933 - write-side MCP tools for absences and the two admin tables
|
|
3
3
|
* that ride alongside them (public holidays, company closures).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - orboto_update_company_closure - PATCH /admin/company-closures/:id
|
|
7
|
-
* - orboto_update_absence - PATCH /absences/:id
|
|
8
|
-
*
|
|
9
|
-
* Today these surfaces only had POST + DELETE - typo correction or date
|
|
10
|
-
* fix meant delete + recreate, losing the original row id. PATCH fills
|
|
11
|
-
* that gap. Listing endpoints stay on the existing routes (no read tools
|
|
12
|
-
* here yet - that's Phase 3 of the CRUD parity epic, ORB-935).
|
|
5
|
+
* @see ORB-935
|
|
13
6
|
*/
|
|
14
7
|
import { z } from 'zod';
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// orboto_update_public_holiday
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
8
|
export const updatePublicHolidayToolConfig = {
|
|
19
9
|
title: 'Edit a public holiday',
|
|
20
10
|
description: 'Correct a typo or wrong date on a public holiday without losing the row id. Admin-only (`admin:absences:write`). Pass only the fields you want to change; at least one field is required.',
|
|
@@ -38,9 +28,6 @@ export function makeUpdatePublicHolidayHandler(client) {
|
|
|
38
28
|
};
|
|
39
29
|
};
|
|
40
30
|
}
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// orboto_update_company_closure
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
31
|
export const updateCompanyClosureToolConfig = {
|
|
45
32
|
title: 'Edit a company closure',
|
|
46
33
|
description: 'Update a workspace-wide non-working block (company offsite, holiday shutdown). Admin-only (`admin:absences:write`). Pass only the fields you want to change; at least one field is required. If only one of `startDate`/`endDate` is passed, the other is left as-is - the API still validates start ≤ end after the partial patch.',
|
|
@@ -63,9 +50,6 @@ export function makeUpdateCompanyClosureHandler(client) {
|
|
|
63
50
|
};
|
|
64
51
|
};
|
|
65
52
|
}
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// orboto_update_absence
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
53
|
export const updateAbsenceToolConfig = {
|
|
70
54
|
title: 'Edit a pending or draft absence request',
|
|
71
55
|
description: 'Edit your own absence request before a reviewer acts on it. Locked once the status is `approved`, `rejected`, or `cancelled` - those transitions go through the approve / reject / cancel endpoints, not this one. Caller must be the absence owner or a super-admin. `durationDays` is recomputed automatically when dates move (uses the owner\'s working-day rules + holidays + closures).',
|
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-893 - admin-translation review queue MCP tools.
|
|
3
|
-
*
|
|
4
|
-
* Three thin wrappers around `/admin/tickets/translations*`:
|
|
5
|
-
*
|
|
6
|
-
* - `orboto_admin_translation_list` - paginated list of
|
|
7
|
-
* auto-translated tickets (pending review by default, or `all`).
|
|
8
|
-
* - `orboto_admin_translation_approve` - stamp the row reviewed.
|
|
9
|
-
* - `orboto_admin_translation_revert` - restore the pre-translation
|
|
10
|
-
* title + description from the audit comment.
|
|
11
|
-
*
|
|
12
|
-
* All three require `admin:translation_review` (super-admin holds it
|
|
13
|
-
* by default).
|
|
14
3
|
*/
|
|
15
4
|
import { z } from 'zod';
|
|
16
5
|
export const listAdminTranslationsToolConfig = {
|
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-244 Phase C Group 4 - admin-only tools.
|
|
3
|
-
*
|
|
4
|
-
* These three are listed under "write tools" in the ticket because
|
|
5
|
-
* they're privileged surfaces, but two of them (`list_users`,
|
|
6
|
-
* `get_audit_log`) are read-only - the gating is what makes them
|
|
7
|
-
* "writes" from a permission perspective. `trigger_backup` is the
|
|
8
|
-
* only true mutation.
|
|
9
|
-
*
|
|
10
|
-
* Gating: the API enforces super-admin via the `admin:*` permission
|
|
11
|
-
* slugs. A non-admin's request lands a 403 from the API, which
|
|
12
|
-
* surfaces as OrbotoApiError on the MCP side. The tools themselves
|
|
13
|
-
* don't double-check - that would race against the API anyway. We
|
|
14
|
-
* just rewrite 403 into a clear message.
|
|
15
|
-
*
|
|
16
|
-
* Tools:
|
|
17
|
-
* - orboto_list_users - admin user directory with cursor pagination
|
|
18
|
-
* - orboto_get_audit_log - recent admin / mutation events
|
|
19
|
-
* - orboto_trigger_backup - run a configured backup job by name
|
|
20
3
|
*/
|
|
21
4
|
import { z } from 'zod';
|
|
22
5
|
import { OrbotoApiError } from '../orboto-client.js';
|
|
@@ -28,9 +11,6 @@ function rewrite403(action) {
|
|
|
28
11
|
throw err;
|
|
29
12
|
};
|
|
30
13
|
}
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// orboto_list_users
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
14
|
export const listUsersToolConfig = {
|
|
35
15
|
title: 'List workspace users (admin)',
|
|
36
16
|
description: 'Return the workspace user directory. Super-admin only. Useful for AI agents that need to look up an email beyond their visible project members (e.g. to assign someone who isn\'t on the current project yet). Imported placeholder users (`*@imported.ghost`) are hidden by default; set `showGhosts=true` to include them.',
|
|
@@ -79,9 +59,6 @@ export function makeListUsersHandler(client) {
|
|
|
79
59
|
};
|
|
80
60
|
};
|
|
81
61
|
}
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
// orboto_get_audit_log
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
62
|
export const getAuditLogToolConfig = {
|
|
86
63
|
title: 'Read the workspace audit log (admin)',
|
|
87
64
|
description: 'Recent audit events - admin actions, role changes, ticket deletions, federation handshakes, etc. Super-admin only. Filter by actor or entity to narrow the window.',
|
|
@@ -99,7 +76,6 @@ export function makeGetAuditLogHandler(client) {
|
|
|
99
76
|
if (entityType)
|
|
100
77
|
qs.set('entityType', entityType);
|
|
101
78
|
if (actorEmail) {
|
|
102
|
-
// Resolve actor email → userId via the admin users list.
|
|
103
79
|
const userPage = await client.get(`/admin/users?search=${encodeURIComponent(actorEmail)}&limit=10`).catch(rewrite403('audit log lookup'));
|
|
104
80
|
const actor = userPage.items.find((u) => u.email.toLowerCase() === actorEmail.toLowerCase());
|
|
105
81
|
if (!actor)
|
|
@@ -111,7 +87,6 @@ export function makeGetAuditLogHandler(client) {
|
|
|
111
87
|
? 'No matching audit entries.'
|
|
112
88
|
: page.items.map((e) => {
|
|
113
89
|
const who = e.actorName ?? e.actorEmail ?? '(system)';
|
|
114
|
-
// ORB-1368 - mark agent-performed actions.
|
|
115
90
|
const agentMark = e.isAgentWork ? ' [agent]' : '';
|
|
116
91
|
const what = e.entityType
|
|
117
92
|
? ` ${e.entityType}${e.entityId ? `:${e.entityId.slice(0, 8)}` : ''}`
|
|
@@ -134,9 +109,6 @@ export function makeGetAuditLogHandler(client) {
|
|
|
134
109
|
};
|
|
135
110
|
};
|
|
136
111
|
}
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
// orboto_trigger_backup
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
112
|
export const triggerBackupToolConfig = {
|
|
141
113
|
title: 'Run a configured backup job now (admin)',
|
|
142
114
|
description: 'Manually trigger a backup job by name. Resolves to the job\'s UUID via /admin/backup/jobs and POSTs to the run endpoint. Super-admin only.',
|
|
@@ -65,7 +65,6 @@ describe('orboto_get_audit_log', () => {
|
|
|
65
65
|
{ json: { items: [], nextCursor: null } },
|
|
66
66
|
]);
|
|
67
67
|
await makeGetAuditLogHandler(client)({ actorEmail: 'ada@acme' });
|
|
68
|
-
// Call 0 = users-search lookup, Call 1 = audit-log with actorId=u1
|
|
69
68
|
expect(calls[0].url).toContain('/admin/users');
|
|
70
69
|
expect(calls[1].url).toContain('actorId=u1');
|
|
71
70
|
});
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-705 - MCP coordination tools.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* orboto_agent_heartbeat - wraps POST /v1/agent/heartbeat. Returns
|
|
6
|
-
* the sessionToken the agent persists for next call.
|
|
7
|
-
* orboto_agent_presence - wraps GET /v1/agent/presence. Returns
|
|
8
|
-
* active sessions visible to the caller (own only for non-admin,
|
|
9
|
-
* workspace-wide for super-admin).
|
|
10
|
-
* orboto_agent_notify - wraps the existing notification surface
|
|
11
|
-
* to dispatch a directed message to another agent / user.
|
|
12
|
-
* Routing target is identified by email (works for both bots and
|
|
13
|
-
* humans). Fire-and-forget; the recipient sees it via the
|
|
14
|
-
* standard notifications channel (ORB-706 wires the bridge).
|
|
4
|
+
* @see ORB-704, ORB-706
|
|
15
5
|
*/
|
|
16
6
|
import { z } from 'zod';
|
|
17
7
|
import { mcpInstanceToken } from './shared.js';
|
|
18
8
|
export const agentHeartbeatToolConfig = {
|
|
19
9
|
title: 'Agent heartbeat (Multi-Agent Coordination)',
|
|
20
|
-
description: 'Register or refresh this agent\'s presence
|
|
10
|
+
description: 'Register or refresh this agent\'s presence with status detail. A live connection already counts as online (an open MCP event stream, `orboto messages --follow`, the agent WebSocket) - the heartbeat adds status: idle (default) | working (+workingOnTicketId) | blocked, capabilities (free-form strings for operator filters) and clientInfo.name (the runtime), and is the only presence path for turn-based clients without a standing connection. Rows older than 90 s count as offline; persist the returned sessionToken and send it on later heartbeats.',
|
|
21
11
|
inputSchema: z.object({
|
|
22
12
|
sessionToken: z.string().nullable().optional(),
|
|
23
13
|
status: z.enum(['idle', 'working', 'blocked']).optional(),
|
|
@@ -102,13 +92,8 @@ export const agentNotifyToolConfig = {
|
|
|
102
92
|
kind: z.enum(['info', 'request', 'complete', 'error']).default('info'),
|
|
103
93
|
subject: z.string().min(1).max(200),
|
|
104
94
|
payload: z.record(z.string(), z.unknown()).optional(),
|
|
105
|
-
// ORB-1727 - reply chaining: the id of the inbox message being answered.
|
|
106
95
|
threadId: z.string().uuid().optional(),
|
|
107
|
-
// ORB-1732 - optional project scope: address "the agent working project
|
|
108
|
-
// X" when the recipient identity runs multiple sessions.
|
|
109
96
|
project: z.string().min(1).max(64).optional().describe('Project key or UUID: scope the message to the recipient session working that project.'),
|
|
110
|
-
// ORB-1742 - defaults to this MCP session's instance token so a shared
|
|
111
|
-
// identity never wakes itself with its own outbound mail.
|
|
112
97
|
senderRef: z.string().min(1).max(128).optional().describe('Sender-session ref for self-echo exclusion; defaults to this MCP session.'),
|
|
113
98
|
}).shape,
|
|
114
99
|
outputSchema: z.object({
|
|
@@ -117,9 +102,6 @@ export const agentNotifyToolConfig = {
|
|
|
117
102
|
}).shape,
|
|
118
103
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
119
104
|
};
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// orboto_agent_broadcast (ORB-964)
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
105
|
export const agentBroadcastToolConfig = {
|
|
124
106
|
title: 'Scoped broadcast to other agents',
|
|
125
107
|
description: 'Fan-out to every agent in a scope: workspace (all internal members), project (members of scopeId = project UUID) or topic (any string). Each recipient gets an inbox copy (orboto_messages, ackable); live subscribers also get a resources/updated push on orboto://broadcast/<scope>/<scope_id>. The scope replay keeps agent_broadcast_retention_days (default 7).',
|
|
@@ -146,13 +128,9 @@ export function makeAgentBroadcastHandler(client) {
|
|
|
146
128
|
}
|
|
147
129
|
export function makeAgentNotifyHandler(client) {
|
|
148
130
|
return async (args, extra) => {
|
|
149
|
-
// ORB-1742 - stamp the sender session automatically.
|
|
150
131
|
const senderRef = mcpInstanceToken(args.senderRef, extra);
|
|
151
132
|
const res = await client.post('/v1/agent/notify', { ...args, senderRef });
|
|
152
133
|
return {
|
|
153
|
-
// ORB-1727 - the message is durable now: it reaches the recipient's
|
|
154
|
-
// inbox even when they are offline (delivery via the pending-mail
|
|
155
|
-
// pointer on their next tool call).
|
|
156
134
|
content: [{ type: 'text', text: `notified ${args.targetEmail} (message ${res.messageId} - delivered live if connected, waits in their inbox otherwise)` }],
|
|
157
135
|
structuredContent: { ok: true, messageId: res.messageId },
|
|
158
136
|
};
|
|
@@ -115,8 +115,6 @@ describe('orboto_agent_notify', () => {
|
|
|
115
115
|
});
|
|
116
116
|
expect(result.content[0].text).toContain('notified bob@example.com');
|
|
117
117
|
expect(result.structuredContent).toMatchObject({ ok: true });
|
|
118
|
-
// ORB-1742 - the sender session's ref is stamped automatically (per-
|
|
119
|
-
// process fallback here, since no MCP sessionId rides along).
|
|
120
118
|
expect(capturedBody[0].senderRef).toMatch(/^mcp-/);
|
|
121
119
|
});
|
|
122
120
|
it('ORB-1742: senderRef prefers the per-connection MCP session id, and an explicit ref wins outright', async () => {
|
|
@@ -6,11 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { sizeBlockResult } from './shared.js';
|
|
9
|
-
// ORB-1819 - the writing contract, verbatim in every write-tool
|
|
10
|
-
// description so the writing agent sees it at the moment of writing.
|
|
11
9
|
const RULE_WRITING_CONTRACT = ' Rule: title = one imperative sentence; body <= 400 chars, Rule / Why / How, no long examples, nothing another block says.';
|
|
12
|
-
// ORB-1700 - a LIST answers "what exists"; the body belongs to the
|
|
13
|
-
// follow-up read (blockId input below). First line, hard-capped.
|
|
14
10
|
function excerptOf(body) {
|
|
15
11
|
const firstLine = body.split('\n', 1)[0] ?? '';
|
|
16
12
|
return firstLine.length > 200 ? `${firstLine.slice(0, 199)}\u2026` : firstLine;
|
|
@@ -42,7 +38,6 @@ function scopeQs(scope, projectId, customerId) {
|
|
|
42
38
|
export function makeListAgentInstructionsHandler(client) {
|
|
43
39
|
return async (input = {}) => {
|
|
44
40
|
const res = await client.get(`/agent-instructions/blocks?${scopeQs(input.scope ?? 'workspace', input.projectId, input.customerId)}`);
|
|
45
|
-
// ORB-1700 - full body for ONE explicitly named block, in one call.
|
|
46
41
|
if (input.blockId) {
|
|
47
42
|
const block = res.blocks.find((b) => b.id === input.blockId);
|
|
48
43
|
if (!block) {
|
|
@@ -57,17 +52,12 @@ export function makeListAgentInstructionsHandler(client) {
|
|
|
57
52
|
structuredContent: { block },
|
|
58
53
|
};
|
|
59
54
|
}
|
|
60
|
-
// ORB-1700 - list = metadata + excerpt. The assembled 24k rule text is
|
|
61
|
-
// NOT re-shipped here (it rode along on every management call and cost
|
|
62
|
-
// 238 Mtok over 3 calls); agents load the rules via orboto_session_start.
|
|
63
55
|
const text = res.blocks.length
|
|
64
56
|
? res.blocks.map(renderBlock).join('\n')
|
|
65
57
|
: 'No rule blocks configured.';
|
|
66
58
|
return {
|
|
67
59
|
content: [{ type: 'text', text }],
|
|
68
60
|
structuredContent: {
|
|
69
|
-
// Array order = sortOrder; builtinKey only when it is one; the
|
|
70
|
-
// descriptive TITLE is the excerpt (bodies via blockId).
|
|
71
61
|
blocks: res.blocks.map((b) => ({
|
|
72
62
|
id: b.id,
|
|
73
63
|
title: b.title,
|
|
@@ -73,8 +73,6 @@ describe('agent-instruction MCP tools (ORB-1089)', () => {
|
|
|
73
73
|
expect(calls[0]).toMatchObject({ method: 'DELETE', url: `https://orboto.example.com/agent-instructions/blocks/${BLOCK.id}` });
|
|
74
74
|
expect(res.structuredContent).toMatchObject({ deleted: true });
|
|
75
75
|
});
|
|
76
|
-
// ORB-1819 - the writing-for-tokens size contract, passed through from
|
|
77
|
-
// the REST route's warn (200 + sizeWarning) / block (422) responses.
|
|
78
76
|
describe('ORB-1819 size contract', () => {
|
|
79
77
|
it('create surfaces a soft-limit sizeWarning in the success text, not as an error', async () => {
|
|
80
78
|
const oversizeBlock = { ...BLOCK, body: 'x'.repeat(450), sizeWarning: { chars: 450, limit: 400, hint: 'move it to a doc' } };
|
|
@@ -16,7 +16,6 @@ export const agentMessagesToolConfig = {
|
|
|
16
16
|
all: z.boolean().default(false).describe('true = include already-read messages'),
|
|
17
17
|
limit: z.number().int().min(1).max(200).default(50),
|
|
18
18
|
project: z.string().min(1).max(64).optional().describe('Project key or UUID: scoped messages for this project plus unscoped ones. Ack only messages that are yours.'),
|
|
19
|
-
// ORB-1742 - self-echo exclusion, on by default for MCP sessions.
|
|
20
19
|
includeOwnSends: z.boolean().default(false).describe('true = also list messages this session sent (hidden by default so a shared identity never wakes itself).'),
|
|
21
20
|
ackIds: z.array(z.string().uuid()).max(200).optional().describe('Message ids to mark as read'),
|
|
22
21
|
}).shape,
|
package/dist/tools/ai-status.js
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-564 - `orboto_ai_status`.
|
|
3
|
-
*
|
|
4
|
-
* Pre-flight check for agents: is the workspace's AI provider configured?
|
|
5
|
-
* Wraps `GET /ai/status`. Two flags come back - chat features and
|
|
6
|
-
* embeddings - because Anthropic-only deployments have chat fully
|
|
7
|
-
* wired but cannot produce embeddings, and RAG-style features
|
|
8
|
-
* (`ask-docs`, similar-tickets rerank, partial-overlap detection)
|
|
9
|
-
* need both.
|
|
10
|
-
*
|
|
11
|
-
* Today no MCP tool requires AI directly - every `orboto_*` tool is a
|
|
12
|
-
* thin REST wrapper that does its own thing. The dependency lives on
|
|
13
|
-
* the skill side (`orboto ask-docs`) and on chat-only LLM calls the
|
|
14
|
-
* agent host might make. This tool exists so an agent can plan around
|
|
15
|
-
* the workspace shape before calling those skill shortcuts or before
|
|
16
|
-
* suggesting AI-gated features to the operator.
|
|
17
3
|
*/
|
|
18
4
|
import { z } from 'zod';
|
|
19
5
|
export const aiStatusToolConfig = {
|
package/dist/tools/ai-usage.js
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-1321 - `orboto_ai_usage`.
|
|
3
|
-
*
|
|
4
|
-
* Operator diagnostic for AI *consumption*: total calls / tokens in+out /
|
|
5
|
-
* errors over a date range, plus per-user, per-operation, and per-day
|
|
6
|
-
* breakdowns and the AI-Chat slice. Wraps GET /admin/ai/usage.
|
|
7
|
-
*
|
|
8
|
-
* Complements the other two AI diagnostics: `orboto_ai_status` says whether AI
|
|
9
|
-
* is configured; `orboto_embedding_status` covers the embedding pipeline's
|
|
10
|
-
* health; this one answers "how much are we spending / how many calls are
|
|
11
|
-
* erroring". Aggregates only (no per-row error text). admin:ai:read gated
|
|
12
|
-
* (403 for non-admin callers).
|
|
13
3
|
*/
|
|
14
4
|
import { z } from 'zod';
|
|
15
5
|
export const aiUsageToolConfig = {
|
package/dist/tools/analytics.js
CHANGED
|
@@ -25,8 +25,6 @@ export function makeAnalyticsHandler(client) {
|
|
|
25
25
|
const project = await resolveProjectByKey(client, input.projectKey);
|
|
26
26
|
let milestoneId;
|
|
27
27
|
if (input.milestone && (input.report === 'burndown' || input.report === 'earned-value' || input.report === 'forecast' || input.report === 'collaboration')) {
|
|
28
|
-
// ORB-1696 - shared resolver: key (ORB-M3), name or UUID, ambiguous
|
|
29
|
-
// name -> explicit error. Matches create_ticket/set_milestone/OQL.
|
|
30
28
|
const m = await resolveMilestoneByNameOrId(client, project.id, input.milestone);
|
|
31
29
|
milestoneId = m.id;
|
|
32
30
|
}
|
package/dist/tools/api-call.js
CHANGED
|
@@ -22,25 +22,13 @@ export const apiCallToolConfig = {
|
|
|
22
22
|
.describe('Route path, e.g. "/projects/<uuid>/milestones". No query string.'),
|
|
23
23
|
query: z.record(z.union([z.string(), z.number(), z.boolean(), z.array(z.string())])).optional()
|
|
24
24
|
.describe('Query parameters as an object.'),
|
|
25
|
-
// ORB-1710 - typed union, NOT z.unknown(): a typeless argument is
|
|
26
|
-
// serialized as a STRING by real MCP clients (the OCP-540 class),
|
|
27
|
-
// which used to reach the inner route without a JSON content type
|
|
28
|
-
// and 415. The union renders a typed anyOf so clients send real
|
|
29
|
-
// JSON; the handler still parses a string arrival as belt-and-braces.
|
|
30
25
|
body: z.union([z.record(z.unknown()), z.array(z.unknown()), z.string(), z.number(), z.boolean()]).optional()
|
|
31
26
|
.describe('JSON body - a real object/array, not a stringified one.'),
|
|
32
27
|
}).shape,
|
|
33
|
-
// Dispatches arbitrary methods - never advertise as read-only.
|
|
34
|
-
// ORB-1669 - the escape hatch can issue DELETE through the proxy: destructive by contract.
|
|
35
28
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
36
29
|
};
|
|
37
30
|
export function makeApiCallHandler(client) {
|
|
38
31
|
return async (input) => {
|
|
39
|
-
// ORB-1710 - belt-and-braces for clients that still stringify the
|
|
40
|
-
// body: a string that parses as JSON is un-stringified here, so the
|
|
41
|
-
// proxy receives real JSON even against an older API without the
|
|
42
|
-
// server-side coercion. An unparseable string passes through and
|
|
43
|
-
// gets the proxy's clear 400.
|
|
44
32
|
let body = input.body;
|
|
45
33
|
if (typeof body === 'string') {
|
|
46
34
|
try {
|
|
@@ -63,7 +63,7 @@ describe('orboto_api_call', () => {
|
|
|
63
63
|
},
|
|
64
64
|
}]);
|
|
65
65
|
const res = await makeApiCallHandler(client)({ method: 'POST', path: '/projects/p1/milestones', body: {} });
|
|
66
|
-
expect(res.isError).toBeUndefined();
|
|
66
|
+
expect(res.isError).toBeUndefined();
|
|
67
67
|
const text = res.content[0].text;
|
|
68
68
|
expect(text).toContain('HTTP 403');
|
|
69
69
|
expect(text).toMatch(/missing permission/i);
|
|
@@ -77,7 +77,6 @@ describe('orboto_api_call', () => {
|
|
|
77
77
|
path: '/docs/abc/append-section',
|
|
78
78
|
body: '{"content":"# heading"}',
|
|
79
79
|
});
|
|
80
|
-
// The proxy must receive real JSON, not a double-encoded string.
|
|
81
80
|
expect(calls[0].body.body).toEqual({ content: '# heading' });
|
|
82
81
|
});
|
|
83
82
|
it('passes an unparseable string body through for the proxy\'s clear 400', async () => {
|
package/dist/tools/api-search.js
CHANGED
|
@@ -2,12 +2,7 @@
|
|
|
2
2
|
* ORB-1518 - `orboto_api_search`: the discovery half of the Code-Mode
|
|
3
3
|
* escape hatch (epic ORB-1517).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* the live OpenAPI spec server-side. Lets an agent find any REST
|
|
7
|
-
* endpoint + its schema on demand instead of needing a named MCP tool
|
|
8
|
-
* per endpoint - the long tail of the API stays reachable even on a
|
|
9
|
-
* curated (small) tool manifest. Execute the endpoint you found with
|
|
10
|
-
* `orboto_api_call` (ORB-1519).
|
|
5
|
+
* @see ORB-1519
|
|
11
6
|
*/
|
|
12
7
|
import { z } from 'zod';
|
|
13
8
|
export const apiSearchToolConfig = {
|
package/dist/tools/approvals.js
CHANGED
|
@@ -12,9 +12,6 @@ import { resolveTicketByKey } from './shared.js';
|
|
|
12
12
|
function fmt(r) {
|
|
13
13
|
return ` ${r.id} → "${r.toStatusName}" [${r.status}] ${r.approveCount}/${r.requiredApprovals} approvals (${r.policyName})${r.canApprove ? ' - you can vote' : ''}`;
|
|
14
14
|
}
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// orboto_list_approvals
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
15
|
export const listApprovalsToolConfig = {
|
|
19
16
|
title: 'List a ticket\'s approval / sign-off requests',
|
|
20
17
|
description: 'List the approval / sign-off requests on a ticket (generic change-management gates on status transitions). Shows each request\'s target status, mode, approval progress, and whether YOU can vote on it right now. Use this before `orboto_approval_decide`.',
|
|
@@ -33,9 +30,6 @@ export function makeListApprovalsHandler(client) {
|
|
|
33
30
|
return { content: [{ type: 'text', text }], structuredContent: { ticketKey: ticket.ticketKey, requests: rows } };
|
|
34
31
|
};
|
|
35
32
|
}
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// orboto_approval_decide
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
33
|
export const approvalDecideToolConfig = {
|
|
40
34
|
title: 'Approve or reject a ticket sign-off request',
|
|
41
35
|
description: 'Cast an approve/reject vote on a pending approval / sign-off request that gates a ticket status transition. You must be an eligible approver (a policy role, a named user, or a RACI role on the ticket) - otherwise the API returns 403. A single reject resolves the request; enough approvals unblock the transition (the requester then re-applies the status move). If the ticket has several pending requests, pass `requestId`.',
|
package/dist/tools/attach.js
CHANGED
|
@@ -1,23 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-799 - attach files to a ticket.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `orboto.mjs attach <ticket> <path...> [--alt "text"] [--embed]`.
|
|
5
|
-
*
|
|
6
|
-
* MCP-side, the model doesn't have local FS access, so the bytes come
|
|
7
|
-
* over the wire as a base64 `contentBase64` field - same approach as
|
|
8
|
-
* `orboto_ingest_file`. To keep the surface simple, this tool uploads
|
|
9
|
-
* one file per call; the wrapper's multi-file shorthand is unrolled
|
|
10
|
-
* into N tool calls on the agent side.
|
|
11
|
-
*
|
|
12
|
-
* Two modes, controlled by `embed`:
|
|
13
|
-
*
|
|
14
|
-
* - embed=false (default): upload only, return the markdown image
|
|
15
|
-
* line + raw URL. The agent typically uses this to compose a
|
|
16
|
-
* follow-up `orboto_comment` body.
|
|
17
|
-
* - embed=true: upload, then PATCH the ticket's description to
|
|
18
|
-
* append the markdown image at the end. Idempotent only up to
|
|
19
|
-
* duplicate detection (re-running with the same file *does*
|
|
20
|
-
* append a fresh line; the API doesn't dedupe).
|
|
21
3
|
*/
|
|
22
4
|
import { z } from 'zod';
|
|
23
5
|
import { resolveTicketByKey } from './shared.js';
|
|
@@ -57,11 +39,6 @@ export function makeAttachToTicketHandler(client) {
|
|
|
57
39
|
return async ({ ticketKey, filename, contentBase64, altText, embed }) => {
|
|
58
40
|
let arrayBuffer;
|
|
59
41
|
try {
|
|
60
|
-
// Buffer → fresh ArrayBuffer copy. The Blob ctor's BlobPart type
|
|
61
|
-
// wants `ArrayBufferView<ArrayBuffer>` and Node @types reject a
|
|
62
|
-
// raw Buffer / Uint8Array because their backing buffer is the
|
|
63
|
-
// wider `ArrayBufferLike`. Copying the bytes into a fresh
|
|
64
|
-
// ArrayBuffer satisfies the constraint without losing data.
|
|
65
42
|
const buf = Buffer.from(contentBase64, 'base64');
|
|
66
43
|
arrayBuffer = new ArrayBuffer(buf.byteLength);
|
|
67
44
|
new Uint8Array(arrayBuffer).set(buf);
|
|
@@ -80,8 +57,6 @@ export function makeAttachToTicketHandler(client) {
|
|
|
80
57
|
const markdown = ``;
|
|
81
58
|
let embedded = false;
|
|
82
59
|
if (embed === true) {
|
|
83
|
-
// Read the current description first - the PATCH replaces, not
|
|
84
|
-
// appends, so we have to construct the new body.
|
|
85
60
|
const current = await client.get(`/projects/${ticket.projectId}/tickets/${ticket.id}`);
|
|
86
61
|
const existing = current.description ?? '';
|
|
87
62
|
const next = existing ? `${existing}\n\n${markdown}` : markdown;
|
package/dist/tools/backup.js
CHANGED
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
const mb = (n) => `${Math.round((n / 1024 / 1024) * 10) / 10} MB`;
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// orboto_create_full_backup
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
12
|
export const createFullBackupToolConfig = {
|
|
16
13
|
title: 'Create + download a full workspace backup',
|
|
17
14
|
description: 'Create an on-demand FULL workspace backup (database + storage) and return the ZIP as a base64 application/zip resource attachment - create, wait and download in one call (the server runs the export as a background run and this tool polls it; large workspaces can take minutes). Rows and file references share one snapshot; missing or incomplete files fail the run, never return a partial ZIP. Requires admin:backup:export. For the scheduled named jobs use orboto_trigger_backup instead.',
|
|
@@ -20,10 +17,6 @@ export const createFullBackupToolConfig = {
|
|
|
20
17
|
};
|
|
21
18
|
export function makeCreateFullBackupHandler(client) {
|
|
22
19
|
return async () => {
|
|
23
|
-
// ORB-1717 - the export is async server-side (the old synchronous
|
|
24
|
-
// stream aborted silently under load). Enqueue, poll the run row to a
|
|
25
|
-
// terminal state, then download the stored artifact - so an aborted
|
|
26
|
-
// wait never yields a partial ZIP.
|
|
27
20
|
const { runId } = await client.post('/admin/backup/full', {});
|
|
28
21
|
const t0 = Date.now();
|
|
29
22
|
for (;;) {
|
|
@@ -47,9 +40,6 @@ export function makeCreateFullBackupHandler(client) {
|
|
|
47
40
|
};
|
|
48
41
|
};
|
|
49
42
|
}
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// orboto_list_backups
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
43
|
export const listBackupsToolConfig = {
|
|
54
44
|
title: 'List backup runs',
|
|
55
45
|
description: 'List recent backup runs (id, status, size, time) so you can pick one to download with orboto_download_backup. Requires admin:backup:read.',
|
|
@@ -67,9 +57,6 @@ export function makeListBackupsHandler(client) {
|
|
|
67
57
|
};
|
|
68
58
|
};
|
|
69
59
|
}
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// orboto_download_backup
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
60
|
export const downloadBackupToolConfig = {
|
|
74
61
|
title: 'Download a stored backup run',
|
|
75
62
|
description: 'Download a stored backup run\'s ZIP by run id (from orboto_list_backups - e.g. the nightly job\'s run) as a base64 application/zip resource attachment. Requires admin:backup:read.',
|
|
@@ -37,7 +37,7 @@ function stubJson(body) {
|
|
|
37
37
|
describe('orboto_create_full_backup (ORB-1301 / async since ORB-1717)', () => {
|
|
38
38
|
it('enqueues, polls the run to success, then downloads the stored ZIP', async () => {
|
|
39
39
|
vi.useFakeTimers();
|
|
40
|
-
const zip = new Uint8Array([0x50, 0x4b, 0x03, 0x04]);
|
|
40
|
+
const zip = new Uint8Array([0x50, 0x4b, 0x03, 0x04]);
|
|
41
41
|
const calls = [];
|
|
42
42
|
let polls = 0;
|
|
43
43
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
@@ -49,7 +49,6 @@ describe('orboto_create_full_backup (ORB-1301 / async since ORB-1717)', () => {
|
|
|
49
49
|
if (u.includes('/admin/backup/runs/run-1/download')) {
|
|
50
50
|
return { ok: true, status: 200, statusText: 'OK', headers: new Headers({ 'content-type': 'application/zip' }), arrayBuffer: async () => zip.buffer.slice(0), text: async () => '' };
|
|
51
51
|
}
|
|
52
|
-
// poll: running twice, then success
|
|
53
52
|
polls += 1;
|
|
54
53
|
return { ok: true, status: 200, statusText: 'OK', headers: new Headers({ 'content-type': 'application/json' }), json: async () => ({ status: polls < 3 ? 'running' : 'success', errorMessage: null }), text: async () => '' };
|
|
55
54
|
});
|