@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.
Files changed (131) hide show
  1. package/dist/event-bridge.js +1 -47
  2. package/dist/event-bridge.test.js +0 -19
  3. package/dist/http-transport-binding.test.js +1 -4
  4. package/dist/http-transport.js +1 -213
  5. package/dist/http-transport.test.js +0 -56
  6. package/dist/index.js +1 -48
  7. package/dist/input-schema.js +2 -76
  8. package/dist/input-schema.test.js +1 -15
  9. package/dist/instructions-budget.test.js +0 -2
  10. package/dist/manifest-size.test.js +3 -72
  11. package/dist/oauth-bootstrap.js +0 -47
  12. package/dist/oauth-bootstrap.test.js +4 -16
  13. package/dist/orboto-client.js +0 -30
  14. package/dist/orboto-client.test.js +0 -12
  15. package/dist/prompts.js +0 -39
  16. package/dist/protect-text-usage.test.js +1 -9
  17. package/dist/required-rules-wrapper.test.js +0 -5
  18. package/dist/required-rules.js +0 -2
  19. package/dist/resources.js +0 -62
  20. package/dist/resources.test.js +0 -16
  21. package/dist/response-budget.js +2 -150
  22. package/dist/response-budget.test.js +1 -32
  23. package/dist/server.js +4 -180
  24. package/dist/session-nudge.js +0 -9
  25. package/dist/session-nudge.test.js +2 -10
  26. package/dist/session-nudge.transport.test.js +0 -23
  27. package/dist/tool-annotations.test.js +0 -44
  28. package/dist/tool-docs.js +1 -12
  29. package/dist/tool-docs.test.js +2 -8
  30. package/dist/tool-parameters.test.js +0 -19
  31. package/dist/tools/absence-writes.js +1 -17
  32. package/dist/tools/admin-translations.js +0 -11
  33. package/dist/tools/admin-writes.js +0 -28
  34. package/dist/tools/admin-writes.test.js +0 -1
  35. package/dist/tools/agent-coordination.js +2 -24
  36. package/dist/tools/agent-coordination.test.js +0 -2
  37. package/dist/tools/agent-instructions.js +0 -10
  38. package/dist/tools/agent-instructions.test.js +0 -2
  39. package/dist/tools/agent-messages.js +0 -1
  40. package/dist/tools/ai-status.js +0 -14
  41. package/dist/tools/ai-usage.js +0 -10
  42. package/dist/tools/analytics.js +0 -2
  43. package/dist/tools/api-call.js +0 -12
  44. package/dist/tools/api-call.test.js +1 -2
  45. package/dist/tools/api-search.js +1 -6
  46. package/dist/tools/approvals.js +0 -6
  47. package/dist/tools/attach.js +0 -25
  48. package/dist/tools/backup.js +0 -13
  49. package/dist/tools/backup.test.js +1 -2
  50. package/dist/tools/bulk-create.js +1 -25
  51. package/dist/tools/bulk-create.test.js +0 -4
  52. package/dist/tools/bulk-writes.js +69 -61
  53. package/dist/tools/bulk-writes.test.js +34 -4
  54. package/dist/tools/check-similar.js +0 -17
  55. package/dist/tools/checklist-writes.js +82 -27
  56. package/dist/tools/checklist-writes.test.js +69 -1
  57. package/dist/tools/claim.js +1 -71
  58. package/dist/tools/claim.test.js +0 -2
  59. package/dist/tools/critical-path.js +0 -5
  60. package/dist/tools/critical-path.test.js +0 -3
  61. package/dist/tools/cross-project-links.ee.js +1 -24
  62. package/dist/tools/doc-attachments.js +0 -28
  63. package/dist/tools/doc-attachments.test.js +1 -4
  64. package/dist/tools/doc-comments.js +1 -37
  65. package/dist/tools/doc-comments.test.js +1 -2
  66. package/dist/tools/doc-edits.js +1 -31
  67. package/dist/tools/doc-edits.test.js +0 -9
  68. package/dist/tools/doc-export.js +0 -24
  69. package/dist/tools/doc-export.test.js +0 -12
  70. package/dist/tools/doc-revisions.js +0 -23
  71. package/dist/tools/docs-ai.js +0 -34
  72. package/dist/tools/docs-ai.test.js +0 -11
  73. package/dist/tools/docs.js +1 -75
  74. package/dist/tools/docs.test.js +1 -3
  75. package/dist/tools/embedding-status.js +0 -13
  76. package/dist/tools/embedding-status.test.js +0 -1
  77. package/dist/tools/feedback.js +1 -15
  78. package/dist/tools/feedback.test.js +0 -1
  79. package/dist/tools/get-checklists.js +6 -14
  80. package/dist/tools/get-project-primer.js +1 -25
  81. package/dist/tools/get-project.js +0 -17
  82. package/dist/tools/get-ticket.js +1 -61
  83. package/dist/tools/get-timer.js +0 -11
  84. package/dist/tools/git-health-reasons.js +0 -1
  85. package/dist/tools/identity.js +0 -13
  86. package/dist/tools/list-projects.js +0 -20
  87. package/dist/tools/list-projects.test.js +0 -1
  88. package/dist/tools/list-tickets.js +10 -9
  89. package/dist/tools/milestones-crud.test.js +0 -4
  90. package/dist/tools/milestones.js +0 -43
  91. package/dist/tools/my-tickets.js +0 -7
  92. package/dist/tools/phase-b-tools.test.js +14 -21
  93. package/dist/tools/primer-facts.js +1 -49
  94. package/dist/tools/primer-facts.test.js +0 -20
  95. package/dist/tools/project-listings.js +0 -25
  96. package/dist/tools/query.js +1 -12
  97. package/dist/tools/query.test.js +0 -8
  98. package/dist/tools/raci.js +0 -11
  99. package/dist/tools/requirements-spec.js +1 -12
  100. package/dist/tools/response-expand.js +0 -19
  101. package/dist/tools/review-policy.js +0 -24
  102. package/dist/tools/review-policy.test.js +0 -2
  103. package/dist/tools/search.js +1 -13
  104. package/dist/tools/session-start.js +1 -136
  105. package/dist/tools/session-start.test.js +1 -44
  106. package/dist/tools/set-parent.js +1 -19
  107. package/dist/tools/shared.js +0 -41
  108. package/dist/tools/shared.test.js +0 -4
  109. package/dist/tools/similar-projection.js +0 -15
  110. package/dist/tools/ticket-attachments.js +1 -25
  111. package/dist/tools/ticket-schedules.js +0 -9
  112. package/dist/tools/ticket-schedules.test.js +0 -1
  113. package/dist/tools/ticket-writes.js +1 -110
  114. package/dist/tools/ticket-writes.test.js +1 -15
  115. package/dist/tools/time-writes.js +26 -46
  116. package/dist/tools/time-writes.test.js +18 -0
  117. package/dist/tools/update-project.js +1 -27
  118. package/dist/tools/update-project.test.js +1 -1
  119. package/dist/tools/wiki.js +0 -8
  120. package/dist/tools/work-next.js +0 -5
  121. package/dist/tools/work-sessions-start.test.js +0 -2
  122. package/dist/tools/work-sessions.js +0 -48
  123. package/dist/tools/work-sessions.test.js +0 -5
  124. package/dist/toolset.js +2 -81
  125. package/dist/toolset.test.js +0 -20
  126. package/dist/version-guard.test.js +0 -4
  127. package/dist/version.js +0 -12
  128. package/dist/with-metrics-output-schema.test.js +1 -14
  129. package/dist/with-metrics.js +2 -74
  130. package/dist/with-metrics.test.js +7 -29
  131. 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
- * Every tool description in the manifest is STANDING context cost: an
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. */
@@ -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\.\.\.$/); // never cuts inside a word
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); // the old 2.9k essay, intact
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
- * - orboto_update_public_holiday - PATCH /admin/public-holidays/:id
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
- * Three tools layered on the ORB-704 REST surface:
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. Call on startup and every ~30 s; rows older than 90 s count as offline. Persist the returned sessionToken and send it on later heartbeats. status: idle (default) | working (+workingOnTicketId) | blocked. capabilities: free-form strings for operator filters. clientInfo.name = the runtime (claude-code, cursor, ...).',
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,
@@ -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 = {
@@ -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 = {
@@ -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
  }
@@ -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(); // the tool call succeeded; the API said no
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 () => {
@@ -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
- * Wraps the authenticated `GET /system/api-catalog` routes, which search
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 = {
@@ -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`.',
@@ -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 = `![${alt}](${att.downloadUrl})`;
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;
@@ -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]); // PK\x03\x04
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
  });