@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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { OrbotoClient } from '../orboto-client.js';
|
|
6
|
-
import { makeCheckHandler, makeUncheckHandler, makeAddCheckHandler, makeNewChecklistHandler, makeRemoveCheckHandler, } from './checklist-writes.js';
|
|
6
|
+
import { makeCheckHandler, makeUncheckHandler, makeAddCheckHandler, makeNewChecklistHandler, makeRemoveCheckHandler, makeUpdateCheckHandler, } from './checklist-writes.js';
|
|
7
7
|
beforeEach(() => { vi.restoreAllMocks(); });
|
|
8
8
|
afterEach(() => { vi.restoreAllMocks(); });
|
|
9
9
|
function stub(responses) {
|
|
@@ -182,3 +182,71 @@ describe('orboto_new_checklist', () => {
|
|
|
182
182
|
await expect(makeNewChecklistHandler(client)({ ticketKey: 'ACME-1', title: 'x' })).rejects.toThrow(/ticket:edit/);
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
|
+
const MEMBERS = [
|
|
186
|
+
{ userId: '4b3e2c1d-6f7a-4b8c-9d0e-1f2a3b4c5d6e', user: { email: 'kim@example.test', fullName: 'Kim Novak' } },
|
|
187
|
+
{ userId: 'u-ada', user: { email: 'ada@example.test', fullName: 'Ada Byron' } },
|
|
188
|
+
];
|
|
189
|
+
describe('orboto_add_check with assignee + dueDate (ORB-235)', () => {
|
|
190
|
+
it('resolves the assignee by email against the project members and passes the due date through', async () => {
|
|
191
|
+
const calls = stub([
|
|
192
|
+
{ json: PROJ },
|
|
193
|
+
{ json: TICKET },
|
|
194
|
+
{ json: [SAMPLE_LIST] },
|
|
195
|
+
{ json: MEMBERS },
|
|
196
|
+
{ json: { id: 'i3', content: 'Tag the release', assignedTo: '4b3e2c1d-6f7a-4b8c-9d0e-1f2a3b4c5d6e', dueDate: '2026-10-01' } },
|
|
197
|
+
]);
|
|
198
|
+
const res = await makeAddCheckHandler(client)({ ticketKey: 'ACME-1', content: 'Tag the release', assignee: 'kim@example.test', dueDate: '2026-10-01' });
|
|
199
|
+
expect(calls[4].body).toEqual({ content: 'Tag the release', assignedTo: '4b3e2c1d-6f7a-4b8c-9d0e-1f2a3b4c5d6e', dueDate: '2026-10-01' });
|
|
200
|
+
expect(res.structuredContent).toMatchObject({ assignedTo: '4b3e2c1d-6f7a-4b8c-9d0e-1f2a3b4c5d6e', dueDate: '2026-10-01' });
|
|
201
|
+
expect(res.content[0].text).toContain('due 2026-10-01');
|
|
202
|
+
});
|
|
203
|
+
it('names the members when the assignee is not one, and refuses a malformed date before any write', async () => {
|
|
204
|
+
stub([{ json: PROJ }, { json: TICKET }, { json: [SAMPLE_LIST] }, { json: MEMBERS }]);
|
|
205
|
+
await expect(makeAddCheckHandler(client)({ ticketKey: 'ACME-1', content: 'x', assignee: 'nobody@example.test' })).rejects.toThrow(/not a member.*Kim Novak <kim@example.test>/);
|
|
206
|
+
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: [SAMPLE_LIST] }]);
|
|
207
|
+
await expect(makeAddCheckHandler(client)({ ticketKey: 'ACME-1', content: 'x', dueDate: 'next friday' })).rejects.toThrow(/dueDate must be YYYY-MM-DD/);
|
|
208
|
+
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe('orboto_update_check (ORB-235)', () => {
|
|
212
|
+
it('resolves the item by index, the assignee by full name, and PATCHes only what was given', async () => {
|
|
213
|
+
const calls = stub([
|
|
214
|
+
{ json: PROJ },
|
|
215
|
+
{ json: TICKET },
|
|
216
|
+
{ json: [SAMPLE_LIST] },
|
|
217
|
+
{ json: MEMBERS },
|
|
218
|
+
{ json: { item: { id: 'i2', content: 'Docs', assignedTo: 'u-ada', assigneeName: 'Ada Byron', dueDate: '2026-10-02' } } },
|
|
219
|
+
]);
|
|
220
|
+
const res = await makeUpdateCheckHandler(client)({ ticketKey: 'ACME-1', item: 2, assignee: 'Ada Byron', dueDate: '2026-10-02' });
|
|
221
|
+
expect(calls[4].method).toBe('PATCH');
|
|
222
|
+
expect(calls[4].url).toContain('/checklist-items/i2');
|
|
223
|
+
expect(calls[4].body).toEqual({ assignedTo: 'u-ada', dueDate: '2026-10-02' });
|
|
224
|
+
expect(res.structuredContent).toMatchObject({ itemId: 'i2', assigneeName: 'Ada Byron', dueDate: '2026-10-02' });
|
|
225
|
+
expect(res.content[0].text).toContain('@Ada Byron');
|
|
226
|
+
});
|
|
227
|
+
it('null unassigns and clears the date; a user id that is a member passes through unresolved', async () => {
|
|
228
|
+
const calls = stub([
|
|
229
|
+
{ json: PROJ },
|
|
230
|
+
{ json: TICKET },
|
|
231
|
+
{ json: [SAMPLE_LIST] },
|
|
232
|
+
{ json: { item: { id: 'i1', content: 'Tests', assignedTo: null, assigneeName: null, dueDate: null } } },
|
|
233
|
+
]);
|
|
234
|
+
const res = await makeUpdateCheckHandler(client)({ ticketKey: 'ACME-1', item: 1, assignee: null, dueDate: null, content: 'Tests' });
|
|
235
|
+
expect(calls[3].body).toEqual({ content: 'Tests', assignedTo: null, dueDate: null });
|
|
236
|
+
expect(res.content[0].text).toContain('unassigned');
|
|
237
|
+
const byId = stub([
|
|
238
|
+
{ json: PROJ },
|
|
239
|
+
{ json: TICKET },
|
|
240
|
+
{ json: [SAMPLE_LIST] },
|
|
241
|
+
{ json: MEMBERS },
|
|
242
|
+
{ json: { item: { id: 'i1', content: 'Tests', assignedTo: '4b3e2c1d-6f7a-4b8c-9d0e-1f2a3b4c5d6e', assigneeName: 'Kim Novak', dueDate: null } } },
|
|
243
|
+
]);
|
|
244
|
+
await makeUpdateCheckHandler(client)({ ticketKey: 'ACME-1', item: 1, assignee: '4b3e2c1d-6f7a-4b8c-9d0e-1f2a3b4c5d6e' });
|
|
245
|
+
expect(byId[4].body).toEqual({ assignedTo: '4b3e2c1d-6f7a-4b8c-9d0e-1f2a3b4c5d6e' });
|
|
246
|
+
});
|
|
247
|
+
it('refuses an empty update without touching the API', async () => {
|
|
248
|
+
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: [SAMPLE_LIST] }]);
|
|
249
|
+
await expect(makeUpdateCheckHandler(client)({ ticketKey: 'ACME-1', item: 1 })).rejects.toThrow(/Nothing to update/);
|
|
250
|
+
expect(calls.some((c) => c.method === 'PATCH')).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
package/dist/tools/claim.js
CHANGED
|
@@ -1,45 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-799 - composite ticket-lifecycle tools.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - orboto_claim ≈ assign_self + move(in_progress) + timer_start
|
|
7
|
-
* - orboto_unclaim ≈ unassign_self + move(todo)
|
|
8
|
-
*
|
|
9
|
-
* The composite-atomic behaviour is the real value here - these are
|
|
10
|
-
* the most-typed wrapper commands by agents during ticket pickup. The
|
|
11
|
-
* 1:1 mirror of `orboto.mjs claim` semantics matters:
|
|
12
|
-
*
|
|
13
|
-
* - `--sole` (`sole=true`) - destructive take-over: remove every
|
|
14
|
-
* other assignee before adding self. Use sparingly; the API has
|
|
15
|
-
* no atomic swap so this is a delete-loop + add.
|
|
16
|
-
* - `--force` (`force=true`) - allow re-claiming a `done` ticket
|
|
17
|
-
* (the wrapper refuses without it to prevent accidental reopen).
|
|
18
|
-
* - `--no-timer` (`noTimer=true`) - skip the timer_start side
|
|
19
|
-
* effect. Useful when the agent only wants ownership-by-assignee
|
|
20
|
-
* without committing time.
|
|
21
|
-
*
|
|
22
|
-
* Both tools are **idempotent** on the happy paths:
|
|
23
|
-
* - claim of a ticket where the caller is already an assignee and
|
|
24
|
-
* statusCategory is already `in_progress` → no PATCH, no extra
|
|
25
|
-
* assignee POST (the wrapper still POSTs once and 409s; we skip).
|
|
26
|
-
* - unclaim of a ticket where the caller isn't an assignee → 404 on
|
|
27
|
-
* DELETE is swallowed, status move still happens.
|
|
28
|
-
*
|
|
29
|
-
* Timer side-effects mirror ORB-179 (`claim`) + ORB-181 (`close`):
|
|
30
|
-
* - If a different ticket has an active timer, stop it first
|
|
31
|
-
* (commits the elapsed time-entry under the previous ticket's
|
|
32
|
-
* description), then start a fresh timer on this one.
|
|
33
|
-
* - Timer failures never roll back the assign/status work - we
|
|
34
|
-
* surface them as a `timerWarning` field on the structured
|
|
35
|
-
* response, same shape as the wrapper.
|
|
4
|
+
* @see ORB-179, ORB-181
|
|
36
5
|
*/
|
|
37
6
|
import { z } from 'zod';
|
|
38
7
|
import { OrbotoApiError } from '../orboto-client.js';
|
|
39
8
|
import { mcpInstanceToken, resolveTicketByKey } from './shared.js';
|
|
40
|
-
// `statusCategory` is the discriminator the API exposes on every
|
|
41
|
-
// ticket-read response; we map it (rather than `status` legacy enum)
|
|
42
|
-
// because that's what the wrapper inspects too.
|
|
43
9
|
const CATEGORY_TO_LEGACY = {
|
|
44
10
|
todo: 'TODO',
|
|
45
11
|
in_progress: 'IN_PROGRESS',
|
|
@@ -47,14 +13,9 @@ const CATEGORY_TO_LEGACY = {
|
|
|
47
13
|
done: 'DONE',
|
|
48
14
|
wont_fix: 'WONT_FIX',
|
|
49
15
|
};
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// orboto_claim
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
16
|
export const claimToolConfig = {
|
|
54
17
|
title: 'Claim a ticket (assign self + in_progress + timer)',
|
|
55
18
|
description: 'Composite of `assign self → move to in_progress → start timer` - the canonical "I am picking this up now" move. Idempotent: re-claiming an already-claimed in_progress ticket is a no-op. Set `sole=true` to remove every other assignee first (destructive take-over). Set `force=true` to allow re-claiming a `done` ticket (otherwise refuses, to prevent accidental reopens). Set `noTimer=true` to skip the timer start (e.g. when you only want ownership, not time tracking). If a different ticket has an active timer, it is stopped first (its elapsed time commits a time entry under the previous ticket). '
|
|
56
|
-
// ORB-1805 - moved out of the input schema, which every client pays
|
|
57
|
-
// for at connect; orboto_help serves this text in full.
|
|
58
19
|
+ '`agentSessionToken` is a stable per-agent-instance token: on a bot/service account it scopes the timer to YOUR instance - concurrent per-instance timers and NO auto-stop, so you own both start and stop. Omit it on human accounts, which keep the single-timer behaviour.',
|
|
59
20
|
inputSchema: z.object({
|
|
60
21
|
ticketKey: z.string().min(3),
|
|
@@ -63,9 +24,6 @@ export const claimToolConfig = {
|
|
|
63
24
|
noTimer: z.boolean().optional().describe('Skip the timer start.'),
|
|
64
25
|
agentSessionToken: z.string().optional().describe('Per-agent-instance token; scopes the timer on bot accounts.'),
|
|
65
26
|
}).shape,
|
|
66
|
-
// ORB-1669 - destructive because of `sole=true`, which strips every
|
|
67
|
-
// other assignee. Annotations are per-tool and cannot be conditioned on
|
|
68
|
-
// an argument, so the hint has to cover the worst call the tool allows.
|
|
69
27
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
70
28
|
};
|
|
71
29
|
export function makeClaimHandler(client) {
|
|
@@ -78,9 +36,6 @@ export function makeClaimHandler(client) {
|
|
|
78
36
|
if (currentCategory === 'done' && !force) {
|
|
79
37
|
throw new Error(`Refusing to claim [${current.ticketKey}]: status is "done". Pass force=true to reopen + claim.`);
|
|
80
38
|
}
|
|
81
|
-
// --sole: destructive take-over. Remove every other assignee
|
|
82
|
-
// first. Each DELETE tolerates a 404 (race condition where another
|
|
83
|
-
// process already unassigned them).
|
|
84
39
|
if (sole) {
|
|
85
40
|
for (const a of currentAssignees) {
|
|
86
41
|
if (a.id === me.id)
|
|
@@ -94,43 +49,25 @@ export function makeClaimHandler(client) {
|
|
|
94
49
|
}
|
|
95
50
|
}
|
|
96
51
|
}
|
|
97
|
-
// Additive self-add. Skip if we're already on the ticket - POSTing
|
|
98
|
-
// again would 409; idempotency here matters because agents call
|
|
99
|
-
// claim defensively at the start of every operation.
|
|
100
52
|
if (!alreadyAssigned) {
|
|
101
53
|
try {
|
|
102
54
|
await client.post(`/projects/${current.projectId}/tickets/${current.id}/assignees/${me.id}`, {});
|
|
103
55
|
}
|
|
104
56
|
catch (err) {
|
|
105
|
-
// 409 = race-added by another process while we were checking.
|
|
106
|
-
// Treat as already-assigned success.
|
|
107
57
|
if (!(err instanceof OrbotoApiError) || err.status !== 409)
|
|
108
58
|
throw err;
|
|
109
59
|
}
|
|
110
60
|
}
|
|
111
|
-
// Only PATCH if we're not already in_progress. Saves an audit row
|
|
112
|
-
// + websocket broadcast on the common re-claim path.
|
|
113
61
|
let finalTicket = current;
|
|
114
62
|
if (currentCategory !== 'in_progress') {
|
|
115
63
|
finalTicket = await client.patch(`/projects/${current.projectId}/tickets/${current.id}`, { status: CATEGORY_TO_LEGACY.in_progress });
|
|
116
64
|
}
|
|
117
|
-
// Timer side-effects. Mirrors the wrapper's ORB-179 path: stop a
|
|
118
|
-
// timer running on a different ticket, then start fresh here. A
|
|
119
|
-
// 409 on the start side is captured as a warning rather than
|
|
120
|
-
// raised, so the assign/status work still counts as success.
|
|
121
65
|
let timerStarted = false;
|
|
122
66
|
let timerWarning = null;
|
|
123
|
-
// Bot/service accounts own their timer (per-instance, no auto-stop). Instance
|
|
124
|
-
// id: explicit arg > per-connection MCP session id (distinct per client even
|
|
125
|
-
// on a shared HTTP server) > per-process id (stdio). Human accounts fall
|
|
126
|
-
// through to the legacy auto-stop path.
|
|
127
67
|
const effectiveToken = agentSessionToken ?? (me.isBot ? mcpInstanceToken(undefined, extra) : undefined);
|
|
128
68
|
if (!noTimer) {
|
|
129
69
|
try {
|
|
130
70
|
if (effectiveToken) {
|
|
131
|
-
// ORB-1252 - agent instance owns its timer: per-session, no auto-stop.
|
|
132
|
-
// Idempotent on the same ticket; 409 if this session already runs a
|
|
133
|
-
// different ticket (surfaced as a warning below).
|
|
134
71
|
await client.post('/time/timer/start', { ticketId: current.id, agentSessionToken: effectiveToken });
|
|
135
72
|
timerStarted = true;
|
|
136
73
|
}
|
|
@@ -141,13 +78,10 @@ export function makeClaimHandler(client) {
|
|
|
141
78
|
await client.post('/time/timer/stop', {
|
|
142
79
|
note: `Auto-stopped by claim of ${finalTicket.ticketKey ?? current.id}`,
|
|
143
80
|
}).catch(() => {
|
|
144
|
-
// Swallow - we'll try start below; if start 409s we surface
|
|
145
|
-
// the warning then.
|
|
146
81
|
timerWarning = `Failed to stop timer on ${other}; new timer not started.`;
|
|
147
82
|
});
|
|
148
83
|
}
|
|
149
84
|
else if (active && active.ticketId === current.id) {
|
|
150
|
-
// Same ticket - effectively already started.
|
|
151
85
|
timerStarted = true;
|
|
152
86
|
}
|
|
153
87
|
if (!timerStarted && !timerWarning) {
|
|
@@ -161,7 +95,6 @@ export function makeClaimHandler(client) {
|
|
|
161
95
|
timerWarning = 'A timer is already running on another ticket; could not start one here. Run `orboto_timer_stop` first.';
|
|
162
96
|
}
|
|
163
97
|
else {
|
|
164
|
-
// Any other timer error → keep the claim, surface the issue.
|
|
165
98
|
timerWarning = err instanceof Error ? err.message : String(err);
|
|
166
99
|
}
|
|
167
100
|
}
|
|
@@ -192,9 +125,6 @@ export function makeClaimHandler(client) {
|
|
|
192
125
|
};
|
|
193
126
|
};
|
|
194
127
|
}
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
|
-
// orboto_unclaim
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
128
|
export const unclaimToolConfig = {
|
|
199
129
|
title: 'Unclaim a ticket (composite: unassign self + move to todo)',
|
|
200
130
|
description: 'Inverse of `orboto_claim`: remove the calling user as an assignee and move the ticket back to `todo`. Idempotent - if the caller wasn\'t an assignee, the unassign step is a no-op and the status move still happens. Does not stop a running timer (use `orboto_timer_stop` if you want that side-effect; staying separate avoids surprising the next claimant).',
|
package/dist/tools/claim.test.js
CHANGED
|
@@ -86,8 +86,6 @@ describe('orboto_claim', () => {
|
|
|
86
86
|
{ json: { ticketId: 't1' } }, // GET /time/timer - already on same ticket
|
|
87
87
|
]);
|
|
88
88
|
const res = await makeClaimHandler(client)({ ticketKey: 'ACME-1' });
|
|
89
|
-
// 4 calls total: /users/me, by-key project, by-key ticket, GET /time/timer.
|
|
90
|
-
// No POST /assignees, no PATCH status, no POST /timer/start.
|
|
91
89
|
expect(calls).toHaveLength(4);
|
|
92
90
|
expect(calls.some((c) => c.method === 'PATCH')).toBe(false);
|
|
93
91
|
expect(calls.some((c) => c.url.includes('/timer/start'))).toBe(false);
|
|
@@ -21,8 +21,6 @@ export function makeCriticalPathHandler(client) {
|
|
|
21
21
|
const project = await resolveProjectByKey(client, input.projectKey);
|
|
22
22
|
const params = new URLSearchParams();
|
|
23
23
|
if (input.milestone) {
|
|
24
|
-
// ORB-1696 - shared resolver: key (ORB-M3), name or UUID, ambiguous
|
|
25
|
-
// name -> explicit error. Matches create_ticket/set_milestone/OQL.
|
|
26
24
|
const m = await resolveMilestoneByNameOrId(client, project.id, input.milestone);
|
|
27
25
|
params.set('milestoneId', m.id);
|
|
28
26
|
}
|
|
@@ -37,15 +35,12 @@ export function makeCriticalPathHandler(client) {
|
|
|
37
35
|
structuredContent: { cycle: res.cycle, criticalPath: [], projectDurationDays: 0, tickets: [] },
|
|
38
36
|
};
|
|
39
37
|
}
|
|
40
|
-
// ORB-1614 - flag cross-project neighbours pulled into the graph so the
|
|
41
|
-
// reader knows a key like "OVB-55" is not a typo for this project.
|
|
42
38
|
const externalKeys = new Set(res.tickets.filter((t) => t.external).map((t) => t.ticketKey));
|
|
43
39
|
const slack = res.tickets
|
|
44
40
|
.filter((t) => !t.isCritical && t.totalFloat > 0)
|
|
45
41
|
.sort((a, b) => a.totalFloat - b.totalFloat)
|
|
46
42
|
.map((t) => ` ${t.ticketKey}${t.external ? ' [external]' : ''}: ${t.totalFloat}d slack`)
|
|
47
43
|
.join('\n');
|
|
48
|
-
// ORB-1459 - surface tickets whose deadline can't be met (negative float).
|
|
49
44
|
const risks = (res.deadlineRisks ?? [])
|
|
50
45
|
.map((r) => {
|
|
51
46
|
const via = r.constrainingTicketKey && r.constrainingTicketKey !== r.ticketKey
|
|
@@ -75,9 +75,6 @@ describe('orboto_critical_path (ORB-1028)', () => {
|
|
|
75
75
|
expect(sc.deadlineRisks).toHaveLength(2);
|
|
76
76
|
expect(sc.deadlineRisks[0]).toMatchObject({ ticketKey: 'ACME-1', shortfallDays: 2 });
|
|
77
77
|
});
|
|
78
|
-
// ORB-1614 - a 1-hop cross-project neighbour is marked external:true by
|
|
79
|
-
// the API; the tool should flag it in both the text (so "OVB-55" isn't
|
|
80
|
-
// mistaken for a typo of this project's key) and the structured output.
|
|
81
78
|
it('flags a cross-project neighbour with [external] in text and structured output', async () => {
|
|
82
79
|
const calls = stub([
|
|
83
80
|
{ json: PROJ },
|
|
@@ -2,24 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* ORB-632 / ORB-945 - Cross-Project Linking MCP tools.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - orboto_add_cross_project_link - POST /tickets/:id/cross-project-links
|
|
7
|
-
* - orboto_update_cross_project_link - PATCH /tickets/:id/cross-project-links/:linkId
|
|
8
|
-
* - orboto_remove_cross_project_link - DELETE /tickets/:id/cross-project-links/:linkId
|
|
9
|
-
*
|
|
10
|
-
* Business-tier feature (`.ee.*`). The API records the EE soft-warn
|
|
11
|
-
* event on mutating calls; the tool surface here is identical to
|
|
12
|
-
* every other MCP tool and reads the existing license-state via the
|
|
13
|
-
* Authorization-header path.
|
|
14
|
-
*
|
|
15
|
-
* Accepts BOTH a UUID and a ticket-key (`OCP-42`, case-insensitive) on
|
|
16
|
-
* every input - the API resolves either form.
|
|
5
|
+
* @see OCP-42
|
|
17
6
|
*/
|
|
18
7
|
import { z } from 'zod';
|
|
19
8
|
const RELATION_TYPES = ['counterpart', 'depends_on', 'blocks', 'related'];
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// orboto_list_cross_project_links
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
9
|
export const listCrossProjectLinksToolConfig = {
|
|
24
10
|
title: 'List cross-project links on a ticket',
|
|
25
11
|
description: 'Return every cross-project link touching the ticket - both outgoing (this → other) and incoming (other → this). Each row carries the other end\'s project key, ticket key, title, and current status, so the model can see the linked work\'s state without a follow-up tool call. ACL-filtered: rows whose other-end project the caller cannot read are dropped silently.',
|
|
@@ -50,9 +36,6 @@ export function makeListCrossProjectLinksHandler(client) {
|
|
|
50
36
|
};
|
|
51
37
|
};
|
|
52
38
|
}
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// orboto_add_cross_project_link
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
39
|
export const addCrossProjectLinkToolConfig = {
|
|
57
40
|
title: 'Add a cross-project link between two tickets',
|
|
58
41
|
description: 'Create a formal relation between two tickets in different projects. Caller must be a member of BOTH the source and target projects. Relation types: counterpart (parallel work in two repos - bidirectional), depends_on (this ticket waits for the other), blocks (the other waits for this), related (loose association). Pass statusSyncEnabled=true to opt into auto-close: when a counterpart-linked ticket moves to done, the other auto-closes too. Off by default.',
|
|
@@ -76,9 +59,6 @@ export function makeAddCrossProjectLinkHandler(client) {
|
|
|
76
59
|
};
|
|
77
60
|
};
|
|
78
61
|
}
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// orboto_update_cross_project_link
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
62
|
export const updateCrossProjectLinkToolConfig = {
|
|
83
63
|
title: 'Toggle status-sync on a cross-project link',
|
|
84
64
|
description: 'Flip the statusSyncEnabled flag on an existing cross-project link. When enabled on a counterpart-typed link, the other end auto-closes when this one moves to done (and vice versa). Other relation types ignore the flag at runtime, so toggling it on them is a no-op as far as status-sync goes.',
|
|
@@ -101,9 +81,6 @@ export function makeUpdateCrossProjectLinkHandler(client) {
|
|
|
101
81
|
};
|
|
102
82
|
};
|
|
103
83
|
}
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
// orboto_remove_cross_project_link
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
84
|
export const removeCrossProjectLinkToolConfig = {
|
|
108
85
|
title: 'Remove a cross-project link',
|
|
109
86
|
description: 'DESTRUCTIVE - drops the cross-project link between two tickets. Caller must be a member of both source and target projects (or super-admin). The underlying tickets are unaffected; only the relation row is deleted.',
|
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-914 - doc-attachments MCP tools (epic ORB-911 Phase 3).
|
|
3
|
-
*
|
|
4
|
-
* Mirrors the ticket-attachment surface (apps/mcp/src/tools/attach.ts)
|
|
5
|
-
* for doc pages. Three tools:
|
|
6
|
-
*
|
|
7
|
-
* - orboto_upload_doc_attachment - multipart upload + optional embed
|
|
8
|
-
* - orboto_list_doc_attachments - flat list with download URLs
|
|
9
|
-
* - orboto_delete_doc_attachment - destructive
|
|
10
|
-
*
|
|
11
|
-
* The embed branch on upload PATCHes the doc body to append a Markdown
|
|
12
|
-
* image-or-link line, same pattern as orboto_attach_to_ticket. Useful
|
|
13
|
-
* when an agent wants to drop a screenshot into a page in one call.
|
|
14
|
-
*
|
|
15
|
-
* MIME / extension policy is enforced server-side via `isAttachmentAllowed`
|
|
16
|
-
* - the tool just surfaces the 415 as an OrbotoApiError so the caller
|
|
17
|
-
* can pick a different file.
|
|
18
3
|
*/
|
|
19
4
|
import { z } from 'zod';
|
|
20
5
|
import { resolveDocId } from './docs.js';
|
|
@@ -50,9 +35,6 @@ function mimetypeFor(filename) {
|
|
|
50
35
|
function isImage(mimetype) {
|
|
51
36
|
return mimetype.startsWith('image/');
|
|
52
37
|
}
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// orboto_upload_doc_attachment
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
38
|
export const uploadDocAttachmentToolConfig = {
|
|
57
39
|
title: 'Upload an attachment to a doc page',
|
|
58
40
|
description: 'Upload a file as an attachment on a wiki doc and return its Markdown image (or link) line + the stable download URL. Bytes come in as base64 (`contentBase64`) so this works without local FS access on the agent side. Set `embed=true` to also PATCH the doc body to append the Markdown line - useful for dropping a screenshot into a page in one call. MIME / extension policy is enforced server-side: blocked types (HTML, SVG, executables) surface a 415.',
|
|
@@ -70,8 +52,6 @@ export function makeUploadDocAttachmentHandler(client) {
|
|
|
70
52
|
docId = await resolveDocId(client, docId);
|
|
71
53
|
let arrayBuffer;
|
|
72
54
|
try {
|
|
73
|
-
// Buffer → fresh ArrayBuffer copy (see attach.ts for the Blob
|
|
74
|
-
// type-constraint rationale).
|
|
75
55
|
const buf = Buffer.from(contentBase64, 'base64');
|
|
76
56
|
arrayBuffer = new ArrayBuffer(buf.byteLength);
|
|
77
57
|
new Uint8Array(arrayBuffer).set(buf);
|
|
@@ -92,8 +72,6 @@ export function makeUploadDocAttachmentHandler(client) {
|
|
|
92
72
|
: `[${alt}](${url})`;
|
|
93
73
|
let embedded = false;
|
|
94
74
|
if (embed === true) {
|
|
95
|
-
// Read the current body first - PATCH /docs/:id replaces, not
|
|
96
|
-
// appends. Then write the new body in one go.
|
|
97
75
|
const current = await client.get(`/docs/${docId}`);
|
|
98
76
|
const existing = current.content ?? '';
|
|
99
77
|
const next = existing ? `${existing}\n\n${markdown}` : markdown;
|
|
@@ -118,9 +96,6 @@ export function makeUploadDocAttachmentHandler(client) {
|
|
|
118
96
|
};
|
|
119
97
|
};
|
|
120
98
|
}
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
// orboto_list_doc_attachments
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
99
|
export const listDocAttachmentsToolConfig = {
|
|
125
100
|
title: 'List attachments on a doc page',
|
|
126
101
|
description: 'Return the doc\'s attachments newest-first, with stable download URLs the agent can hand to the user. Empty list = no attachments.',
|
|
@@ -160,9 +135,6 @@ export function makeListDocAttachmentsHandler(client) {
|
|
|
160
135
|
};
|
|
161
136
|
};
|
|
162
137
|
}
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
// orboto_delete_doc_attachment
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
138
|
export const deleteDocAttachmentToolConfig = {
|
|
167
139
|
title: 'Delete an attachment from a doc page',
|
|
168
140
|
description: 'DESTRUCTIVE - drops the file row + deletes the underlying S3 object. Does NOT rewrite the doc body, so embedded Markdown lines pointing at the deleted URL will surface as broken images / links. Walk those by hand if needed.',
|
|
@@ -46,9 +46,7 @@ function stubFetch(responses) {
|
|
|
46
46
|
});
|
|
47
47
|
return calls;
|
|
48
48
|
}
|
|
49
|
-
const TINY_PNG_BASE64 =
|
|
50
|
-
// 1x1 red png - smallest valid bytes we can pass through
|
|
51
|
-
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAFhAJ/wlseKgAAAABJRU5ErkJggg==';
|
|
49
|
+
const TINY_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAFhAJ/wlseKgAAAABJRU5ErkJggg==';
|
|
52
50
|
describe('orboto_upload_doc_attachment', () => {
|
|
53
51
|
it('POSTs multipart to /docs/:id/attachments and returns markdown image line', async () => {
|
|
54
52
|
const calls = stubFetch([{ status: 201, json: ATTACHMENT }]);
|
|
@@ -61,7 +59,6 @@ describe('orboto_upload_doc_attachment', () => {
|
|
|
61
59
|
method: 'POST',
|
|
62
60
|
url: `https://orboto.example.com/docs/${DOC_ID}/attachments`,
|
|
63
61
|
});
|
|
64
|
-
// FormData reaches fetch as a FormData instance.
|
|
65
62
|
expect(calls[0].body).toBeInstanceOf(FormData);
|
|
66
63
|
const fd = calls[0].body;
|
|
67
64
|
expect(fd.get('file')).toBeTruthy();
|
|
@@ -1,25 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-917 - doc-comments MCP tools (epic ORB-911 Phase 6).
|
|
3
|
-
*
|
|
4
|
-
* - orboto_list_doc_comments - GET /docs/:id/comments (cursor-paged, oldest-first)
|
|
5
|
-
* - orboto_post_doc_comment - POST /docs/:id/comments
|
|
6
|
-
* - orboto_resolve_doc_comment - POST /docs/:id/comments/:cid/resolve
|
|
7
|
-
* - orboto_delete_doc_comment - DELETE /docs/:id/comments/:cid
|
|
8
|
-
*
|
|
9
|
-
* Comments support replies (single-level - replying to a reply lands as
|
|
10
|
-
* a sibling of the original reply because the API auto-flattens past
|
|
11
|
-
* one level) and optional anchors (a 3-tuple of {text, before, after}
|
|
12
|
-
* the frontend uses to re-locate a highlight even after the doc body
|
|
13
|
-
* has shifted).
|
|
14
|
-
*
|
|
15
|
-
* resolve acts on the thread root by design so resolving from a reply
|
|
16
|
-
* folds the whole conversation - mirrors the web UI's behaviour.
|
|
17
3
|
*/
|
|
18
4
|
import { z } from 'zod';
|
|
19
5
|
import { resolveDocId } from './docs.js';
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// orboto_list_doc_comments
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
6
|
export const listDocCommentsToolConfig = {
|
|
24
7
|
title: 'List comments on a doc page',
|
|
25
8
|
description: 'Return comments on a doc page, oldest-first so the reply tree reads top-to-bottom. Each row carries the author name + content + resolved state. Cursor-paged - pass `cursor` from the previous call\'s nextCursor to walk older pages.',
|
|
@@ -46,9 +29,6 @@ export function makeListDocCommentsHandler(client) {
|
|
|
46
29
|
structuredContent: { comments: [], nextCursor: null },
|
|
47
30
|
};
|
|
48
31
|
}
|
|
49
|
-
// Build a parent-keyed tree so the Markdown rendering can indent
|
|
50
|
-
// replies one level under their root. The API already flattens
|
|
51
|
-
// past one level, so depth is always 0 or 1.
|
|
52
32
|
const byParent = new Map();
|
|
53
33
|
for (const c of page.items) {
|
|
54
34
|
const key = c.parentCommentId ?? null;
|
|
@@ -62,23 +42,19 @@ export function makeListDocCommentsHandler(client) {
|
|
|
62
42
|
const author = c.userName ?? `user ${c.userId.slice(0, 8)}`;
|
|
63
43
|
const resolved = c.resolvedAt ? ' [resolved]' : '';
|
|
64
44
|
lines.push(`${indent}- ${author} · ${c.createdAt}${resolved} · id: ${c.id}`);
|
|
65
|
-
// Anchor preview helps the model understand what part of the
|
|
66
|
-
// doc the comment is anchored to. Truncate to keep the output
|
|
67
|
-
// compact.
|
|
68
45
|
if (c.anchor) {
|
|
69
46
|
const snippet = c.anchor.text.length > 80 ? c.anchor.text.slice(0, 77) + '...' : c.anchor.text;
|
|
70
47
|
lines.push(`${indent} anchored on: "${snippet}"`);
|
|
71
48
|
}
|
|
72
49
|
const body = c.content.split('\n').map((l) => `${indent} ${l}`).join('\n');
|
|
73
50
|
lines.push(body);
|
|
74
|
-
// Render direct replies one level deeper.
|
|
75
51
|
const replies = byParent.get(c.id) ?? [];
|
|
76
52
|
for (const r of replies)
|
|
77
53
|
renderOne(r, depth + 1);
|
|
78
54
|
};
|
|
79
55
|
for (const root of byParent.get(null) ?? []) {
|
|
80
56
|
renderOne(root, 0);
|
|
81
|
-
lines.push('');
|
|
57
|
+
lines.push('');
|
|
82
58
|
}
|
|
83
59
|
if (page.nextCursor) {
|
|
84
60
|
lines.push(`(more available - pass cursor: ${page.nextCursor})`);
|
|
@@ -104,9 +80,6 @@ export function makeListDocCommentsHandler(client) {
|
|
|
104
80
|
};
|
|
105
81
|
};
|
|
106
82
|
}
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// orboto_post_doc_comment
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
83
|
export const postDocCommentToolConfig = {
|
|
111
84
|
title: 'Post a comment on a doc page (or reply to one)',
|
|
112
85
|
description: 'Add a comment to a doc page. Pass `parentCommentId` to reply (the API flattens reply chains past one level so a reply-of-reply lands as a sibling of the original reply). Optional `anchor` (`{text, before, after}`) attaches the comment to a specific highlight in the body - the frontend uses the surrounding context to re-locate the anchor even after the doc has been edited. Mentions in the content body (`@username`) fire notifications automatically.',
|
|
@@ -147,9 +120,6 @@ export function makePostDocCommentHandler(client) {
|
|
|
147
120
|
};
|
|
148
121
|
};
|
|
149
122
|
}
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
// orboto_resolve_doc_comment
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
123
|
export const resolveDocCommentToolConfig = {
|
|
154
124
|
title: 'Mark a doc comment thread as resolved (or reopen it)',
|
|
155
125
|
description: 'Toggle a comment thread\'s resolved state. Pass `resolved=true` to fold the conversation, `resolved=false` to reopen. Always acts on the thread ROOT - resolving from a reply folds the whole conversation, same as the web UI.',
|
|
@@ -177,9 +147,6 @@ export function makeResolveDocCommentHandler(client) {
|
|
|
177
147
|
};
|
|
178
148
|
};
|
|
179
149
|
}
|
|
180
|
-
// ---------------------------------------------------------------------------
|
|
181
|
-
// orboto_update_doc_comment - ORB-933
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
150
|
export const updateDocCommentToolConfig = {
|
|
184
151
|
title: 'Edit your own doc comment',
|
|
185
152
|
description: 'Edit a doc comment\'s body. Author OR super-admin can edit; everyone else gets a 403. Doc-comments do not carry a revision history today (unlike ticket comments), so the prior content is overwritten in place. A no-op call (same content as before) returns the unchanged row.',
|
|
@@ -204,9 +171,6 @@ export function makeUpdateDocCommentHandler(client) {
|
|
|
204
171
|
};
|
|
205
172
|
};
|
|
206
173
|
}
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
// orboto_delete_doc_comment
|
|
209
|
-
// ---------------------------------------------------------------------------
|
|
210
174
|
export const deleteDocCommentToolConfig = {
|
|
211
175
|
title: 'Delete a doc comment',
|
|
212
176
|
description: 'DESTRUCTIVE - drops the comment + (via FK cascade) every reply under it. Only the author can delete their own comments; super-admins can delete anyone\'s. Returns success silently; 403 surfaces as an OrbotoApiError.',
|
|
@@ -62,7 +62,7 @@ describe('orboto_list_doc_comments', () => {
|
|
|
62
62
|
const res = await makeListDocCommentsHandler(client)({ docId: DOC_ID });
|
|
63
63
|
const text = res.content[0].text;
|
|
64
64
|
expect(text.indexOf('Alice')).toBeLessThan(text.indexOf('Bob'));
|
|
65
|
-
expect(text).toMatch(/\s\s- Bob/);
|
|
65
|
+
expect(text).toMatch(/\s\s- Bob/);
|
|
66
66
|
expect(text).toContain('Agreed.');
|
|
67
67
|
const comments = res.structuredContent.comments;
|
|
68
68
|
expect(comments).toHaveLength(2);
|
|
@@ -100,7 +100,6 @@ describe('orboto_post_doc_comment', () => {
|
|
|
100
100
|
url: `https://orboto.example.com/docs/${DOC_ID}/comments`,
|
|
101
101
|
body: { content: 'Looks good to me.' },
|
|
102
102
|
});
|
|
103
|
-
// No parentCommentId in the body for a root comment.
|
|
104
103
|
expect(calls[0].body.parentCommentId).toBeUndefined();
|
|
105
104
|
});
|
|
106
105
|
it('passes parentCommentId through for a reply', async () => {
|
package/dist/tools/doc-edits.js
CHANGED
|
@@ -1,33 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-1342 (epic ORB-1339) - doc snippet-search + targeted-edit MCP tools.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* ORB-1340 (GET /docs/search) and ORB-1341 (POST /docs/:id/edits). The
|
|
6
|
-
* whole point of the feature is context efficiency: an agent finds the
|
|
7
|
-
* right passage with one search (snippet + heading anchor, NOT the full
|
|
8
|
-
* doc body) and changes it with one edit that ships only the diff in both
|
|
9
|
-
* directions. Prefer these over orboto_get_doc + orboto_update_doc for
|
|
10
|
-
* small changes to a large doc.
|
|
11
|
-
*
|
|
12
|
-
* Three tools:
|
|
13
|
-
* - orboto_search_docs GET /docs/search
|
|
14
|
-
* - orboto_edit_doc POST /docs/:id/edits (string-replace edits)
|
|
15
|
-
* - orboto_edit_doc_section POST /docs/:id/edits (heading-addressed ops)
|
|
16
|
-
*
|
|
17
|
-
* The two edit tools hit the same endpoint with a different half of its
|
|
18
|
-
* body, so they share the 409-conflict translation below: a machine-
|
|
19
|
-
* readable conflict (no/ambiguous match, stale revision, heading not
|
|
20
|
-
* found / ambiguous) comes back as a NON-throwing tool result with
|
|
21
|
-
* `isError: true` so the model can self-correct instead of the raw API
|
|
22
|
-
* error bubbling up as an opaque failure.
|
|
4
|
+
* @see ORB-1340, ORB-1341
|
|
23
5
|
*/
|
|
24
6
|
import { z } from 'zod';
|
|
25
7
|
import { OrbotoApiError } from '../orboto-client.js';
|
|
26
8
|
import { resolveDocId } from './docs.js';
|
|
27
9
|
import { resolveProjectByKey } from './shared.js';
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// 409 translation - shared by both edit tools.
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
10
|
/** Turn a 409 from POST /docs/:id/edits into a clear, non-throwing tool
|
|
32
11
|
* result the model can act on. Returns null for any non-409 so the caller
|
|
33
12
|
* rethrows (a genuine transport/permission failure should still surface). */
|
|
@@ -98,9 +77,6 @@ function renderEditResult(res) {
|
|
|
98
77
|
},
|
|
99
78
|
};
|
|
100
79
|
}
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// orboto_search_docs (GET /docs/search - ORB-1340)
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
80
|
export const searchDocsToolConfig = {
|
|
105
81
|
title: 'Search docs for snippets (context-efficient)',
|
|
106
82
|
description: 'Full-text search across wiki docs that returns a highlighted SNIPPET (matched fragments, <mark>…</mark> around each hit) plus the nearest markdown heading path (section anchor) and char/line offset per hit - WITHOUT echoing the full doc body. Use this instead of orboto_get_doc when you only need to locate a passage: find the right doc + section, then change it with orboto_edit_doc / orboto_edit_doc_section, all without transferring the whole document. ACL-filtered in SQL (you only see docs you can read). Ranked by relevance.',
|
|
@@ -157,9 +133,6 @@ export function makeSearchDocsHandler(client) {
|
|
|
157
133
|
};
|
|
158
134
|
};
|
|
159
135
|
}
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
// orboto_edit_doc (POST /docs/:id/edits - string-replace, ORB-1341)
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
136
|
export const editDocToolConfig = {
|
|
164
137
|
title: 'Targeted string-replace edit of a doc (context-efficient)',
|
|
165
138
|
description: 'Change a doc by byte-precise string replacement (Edit-tool-like) - ship only the diff, not the whole doc. Each oldString must match EXACTLY ONCE in the current content (add surrounding context to make it unique) unless replaceAll=true (then >=1). The whole batch is atomic: any match failure applies nothing and returns a 409 you can act on. Prefer this over orboto_update_doc for small changes to a large doc. Pass baseRevisionId (from a previous edit\'s revisionId or orboto_list_doc_revisions) for optimistic concurrency - a stale token is rejected with the current revision id instead of clobbering a concurrent change. Returns a short context window around each change, not the full doc.',
|
|
@@ -192,9 +165,6 @@ export function makeEditDocHandler(client) {
|
|
|
192
165
|
}
|
|
193
166
|
};
|
|
194
167
|
}
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
|
-
// orboto_edit_doc_section (POST /docs/:id/edits - section ops, ORB-1341)
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
168
|
export const editDocSectionToolConfig = {
|
|
199
169
|
title: 'Edit a doc section by heading path (context-efficient)',
|
|
200
170
|
description: 'Change a doc by markdown-heading-addressed section operations - ship only the new section content, not the whole doc. A section is addressed by its heading path (e.g. ["Setup","Credentials"] = a "Credentials" heading nested under "Setup"). Heading matching is EXACT + case-sensitive; the leading path elements must be an ordered ancestor chain (not necessarily immediate parents). op=replace swaps the section body, op=append adds to its end, op=insertAfter inserts directly below the heading line (the heading line itself is never removed). A path that matches zero or multiple headings returns a 409 you can act on (narrow the path to disambiguate). Prefer this over orboto_update_doc for section-scoped rewrites. baseRevisionId gives optimistic concurrency. Returns a short context window per op.',
|
|
@@ -40,9 +40,6 @@ function stubJSON(responses) {
|
|
|
40
40
|
const client = new OrbotoClient({ baseUrl: 'https://orboto.example.com', apiKey: 'orb_x' });
|
|
41
41
|
const DOC_UUID = '33333333-3333-3333-3333-333333333333';
|
|
42
42
|
const REV_UUID = '44444444-4444-4444-4444-444444444444';
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// orboto_search_docs
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
43
|
describe('orboto_search_docs', () => {
|
|
47
44
|
const HIT = {
|
|
48
45
|
id: DOC_UUID,
|
|
@@ -91,9 +88,6 @@ describe('orboto_search_docs', () => {
|
|
|
91
88
|
expect(res.content[0].text).toContain('No docs matched');
|
|
92
89
|
});
|
|
93
90
|
});
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
// orboto_edit_doc - string-replace
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
91
|
describe('orboto_edit_doc', () => {
|
|
98
92
|
it('POSTs edits to /docs/:id/edits (UUID passthrough) and renders the window', async () => {
|
|
99
93
|
const calls = stubJSON([{
|
|
@@ -158,9 +152,6 @@ describe('orboto_edit_doc', () => {
|
|
|
158
152
|
await expect(makeEditDocHandler(client)({ docId: DOC_UUID, edits: [{ oldString: 'a', newString: 'b' }] })).rejects.toThrow(/403/);
|
|
159
153
|
});
|
|
160
154
|
});
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
// orboto_edit_doc_section - heading-addressed
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
155
|
describe('orboto_edit_doc_section', () => {
|
|
165
156
|
it('POSTs sectionOps to /docs/:id/edits', async () => {
|
|
166
157
|
const calls = stubJSON([{
|