@orboto/mcp 0.178.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/LICENSE.md +21 -0
- package/README.md +123 -0
- package/dist/event-bridge.js +250 -0
- package/dist/event-bridge.test.js +165 -0
- package/dist/http-transport-binding.test.js +151 -0
- package/dist/http-transport.js +608 -0
- package/dist/http-transport.test.js +408 -0
- package/dist/index.js +128 -0
- package/dist/input-schema.js +293 -0
- package/dist/input-schema.test.js +198 -0
- package/dist/instructions-budget.test.js +34 -0
- package/dist/manifest-size.test.js +192 -0
- package/dist/mcp-instrument.js +24 -0
- package/dist/oauth-bootstrap.js +413 -0
- package/dist/oauth-bootstrap.test.js +212 -0
- package/dist/orboto-client.js +250 -0
- package/dist/orboto-client.test.js +141 -0
- package/dist/prompts.js +219 -0
- package/dist/prompts.test.js +79 -0
- package/dist/protect-text-usage.test.js +38 -0
- package/dist/resources.js +260 -0
- package/dist/resources.test.js +191 -0
- package/dist/response-budget.js +515 -0
- package/dist/response-budget.test.js +343 -0
- package/dist/server.js +547 -0
- package/dist/session-nudge.js +81 -0
- package/dist/session-nudge.test.js +100 -0
- package/dist/session-nudge.transport.test.js +100 -0
- package/dist/tool-annotations.test.js +97 -0
- package/dist/tool-docs.js +64 -0
- package/dist/tool-docs.test.js +90 -0
- package/dist/tool-parameters.test.js +145 -0
- package/dist/tools/absence-writes.js +93 -0
- package/dist/tools/absence-writes.test.js +106 -0
- package/dist/tools/admin-translations.js +83 -0
- package/dist/tools/admin-writes.js +174 -0
- package/dist/tools/admin-writes.test.js +120 -0
- package/dist/tools/agent-coordination.js +160 -0
- package/dist/tools/agent-coordination.test.js +156 -0
- package/dist/tools/agent-drift.js +79 -0
- package/dist/tools/agent-drift.test.js +87 -0
- package/dist/tools/agent-instructions.js +172 -0
- package/dist/tools/agent-instructions.test.js +124 -0
- package/dist/tools/agent-messages.js +75 -0
- package/dist/tools/ai-status.js +55 -0
- package/dist/tools/ai-status.test.js +43 -0
- package/dist/tools/ai-usage.js +68 -0
- package/dist/tools/analytics.js +72 -0
- package/dist/tools/analytics.test.js +71 -0
- package/dist/tools/api-call.js +72 -0
- package/dist/tools/api-call.test.js +104 -0
- package/dist/tools/api-search.js +77 -0
- package/dist/tools/api-search.test.js +90 -0
- package/dist/tools/approvals.js +68 -0
- package/dist/tools/approvals.test.js +48 -0
- package/dist/tools/attach.js +108 -0
- package/dist/tools/attach.test.js +117 -0
- package/dist/tools/backup.js +93 -0
- package/dist/tools/backup.test.js +108 -0
- package/dist/tools/bulk-create.js +213 -0
- package/dist/tools/bulk-create.test.js +135 -0
- package/dist/tools/bulk-writes.js +339 -0
- package/dist/tools/bulk-writes.test.js +239 -0
- package/dist/tools/check-similar.js +84 -0
- package/dist/tools/check-similar.test.js +101 -0
- package/dist/tools/checklist-writes.js +202 -0
- package/dist/tools/checklist-writes.test.js +184 -0
- package/dist/tools/claim.js +236 -0
- package/dist/tools/claim.test.js +207 -0
- package/dist/tools/critical-path.js +83 -0
- package/dist/tools/critical-path.test.js +114 -0
- package/dist/tools/cross-project-links.ee.js +124 -0
- package/dist/tools/cross-project-links.ee.test.js +163 -0
- package/dist/tools/customer-report.js +88 -0
- package/dist/tools/customer-report.test.js +55 -0
- package/dist/tools/doc-attachments.js +184 -0
- package/dist/tools/doc-attachments.test.js +136 -0
- package/dist/tools/doc-comments.js +228 -0
- package/dist/tools/doc-comments.test.js +192 -0
- package/dist/tools/doc-edits.js +229 -0
- package/dist/tools/doc-edits.test.js +218 -0
- package/dist/tools/doc-export.js +87 -0
- package/dist/tools/doc-export.test.js +109 -0
- package/dist/tools/doc-revisions.js +133 -0
- package/dist/tools/doc-revisions.test.js +103 -0
- package/dist/tools/docs-ai.js +171 -0
- package/dist/tools/docs-ai.test.js +160 -0
- package/dist/tools/docs.js +557 -0
- package/dist/tools/docs.test.js +365 -0
- package/dist/tools/embedding-status.js +100 -0
- package/dist/tools/embedding-status.test.js +86 -0
- package/dist/tools/free-busy.js +49 -0
- package/dist/tools/free-busy.test.js +46 -0
- package/dist/tools/get-checklists.js +72 -0
- package/dist/tools/get-project-primer.js +58 -0
- package/dist/tools/get-project.js +77 -0
- package/dist/tools/get-ticket.js +321 -0
- package/dist/tools/get-timer.js +62 -0
- package/dist/tools/help.js +44 -0
- package/dist/tools/identity.js +60 -0
- package/dist/tools/identity.test.js +67 -0
- package/dist/tools/list-git-app-installations.js +45 -0
- package/dist/tools/list-projects.js +72 -0
- package/dist/tools/list-projects.test.js +67 -0
- package/dist/tools/list-tickets.js +86 -0
- package/dist/tools/milestones-crud.test.js +154 -0
- package/dist/tools/milestones.js +279 -0
- package/dist/tools/milestones.test.js +60 -0
- package/dist/tools/my-tickets.js +65 -0
- package/dist/tools/personal-facts.js +72 -0
- package/dist/tools/personal-facts.test.js +45 -0
- package/dist/tools/phase-b-tools.test.js +530 -0
- package/dist/tools/portfolio.js +56 -0
- package/dist/tools/primer-facts.js +346 -0
- package/dist/tools/primer-facts.test.js +353 -0
- package/dist/tools/project-listings.js +116 -0
- package/dist/tools/project-listings.test.js +106 -0
- package/dist/tools/query.js +65 -0
- package/dist/tools/query.test.js +128 -0
- package/dist/tools/raci.js +108 -0
- package/dist/tools/raci.test.js +70 -0
- package/dist/tools/requirements-spec.js +89 -0
- package/dist/tools/requirements-spec.test.js +62 -0
- package/dist/tools/response-expand.js +104 -0
- package/dist/tools/review-policy.js +105 -0
- package/dist/tools/review-policy.test.js +71 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/session-start.js +435 -0
- package/dist/tools/session-start.test.js +568 -0
- package/dist/tools/set-parent.js +76 -0
- package/dist/tools/set-parent.test.js +115 -0
- package/dist/tools/shared.js +226 -0
- package/dist/tools/shared.test.js +169 -0
- package/dist/tools/similar-projection.js +30 -0
- package/dist/tools/ticket-attachments.js +131 -0
- package/dist/tools/ticket-attachments.test.js +106 -0
- package/dist/tools/ticket-schedules.js +112 -0
- package/dist/tools/ticket-schedules.test.js +102 -0
- package/dist/tools/ticket-writes.js +778 -0
- package/dist/tools/ticket-writes.test.js +643 -0
- package/dist/tools/time-writes.js +238 -0
- package/dist/tools/time-writes.test.js +138 -0
- package/dist/tools/update-project.js +165 -0
- package/dist/tools/update-project.test.js +172 -0
- package/dist/tools/wiki.js +185 -0
- package/dist/tools/wiki.test.js +104 -0
- package/dist/tools/work-sessions-next.test.js +150 -0
- package/dist/tools/work-sessions-start.test.js +174 -0
- package/dist/tools/work-sessions.js +709 -0
- package/dist/tools/work-sessions.test.js +358 -0
- package/dist/toolset.js +149 -0
- package/dist/toolset.test.js +150 -0
- package/dist/version-guard.test.js +51 -0
- package/dist/version.js +28 -0
- package/dist/with-metrics-mail-nudge.test.js +41 -0
- package/dist/with-metrics-output-schema.test.js +91 -0
- package/dist/with-metrics.js +211 -0
- package/dist/with-metrics.test.js +230 -0
- package/package.json +66 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-510 / ORB-513 - primer-fact tool tests.
|
|
3
|
+
*
|
|
4
|
+
* Each tool is a thin wrapper around a REST endpoint, so the tests
|
|
5
|
+
* pin the wire shape: correct HTTP method, URL with key→UUID
|
|
6
|
+
* resolution, JSON body fields, and the source-coercion contract for
|
|
7
|
+
* the `add` tool's `observed` flag.
|
|
8
|
+
*/
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { OrbotoClient } from '../orboto-client.js';
|
|
11
|
+
import { makePrimerFactListHandler, makePrimerFactAddHandler, makePrimerFactUpdateHandler, makePrimerFactSupersedeHandler, makePrimerFactVerifyHandler, makePrimerFactDeleteHandler, primerFactAddToolConfig, primerFactSupersedeToolConfig, primerFactListToolConfig, } from './primer-facts.js';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
beforeEach(() => { vi.restoreAllMocks(); });
|
|
14
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
15
|
+
function stub(responses) {
|
|
16
|
+
const calls = [];
|
|
17
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
18
|
+
const u = url.toString();
|
|
19
|
+
const m = init?.method ?? 'GET';
|
|
20
|
+
const b = init?.body ? JSON.parse(init.body) : undefined;
|
|
21
|
+
calls.push({ url: u, method: m, body: b });
|
|
22
|
+
const r = responses.shift();
|
|
23
|
+
if (!r)
|
|
24
|
+
throw new Error(`unexpected extra fetch ${m} ${u}`);
|
|
25
|
+
return {
|
|
26
|
+
ok: r.ok ?? true,
|
|
27
|
+
status: r.status ?? 200,
|
|
28
|
+
statusText: 'OK',
|
|
29
|
+
json: async () => ('json' in r ? r.json : {}),
|
|
30
|
+
text: async () => '',
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
return calls;
|
|
34
|
+
}
|
|
35
|
+
const client = new OrbotoClient({ baseUrl: 'https://orboto.example.com', apiKey: 'orb_x' });
|
|
36
|
+
const projectStub = { id: 'p-uuid-1234', key: 'ORB', name: 'orboto', description: null, status: 'active' };
|
|
37
|
+
const factStub = {
|
|
38
|
+
id: 'f-uuid-1111',
|
|
39
|
+
projectId: 'p-uuid-1234',
|
|
40
|
+
category: 'tech_stack',
|
|
41
|
+
key: 'package-manager',
|
|
42
|
+
value: 'pnpm 9',
|
|
43
|
+
source: 'manual',
|
|
44
|
+
verified: false,
|
|
45
|
+
verifiedBy: null,
|
|
46
|
+
verifiedAt: null,
|
|
47
|
+
lastVerifiedAt: '2026-04-30T10:00:00Z',
|
|
48
|
+
supersededById: null,
|
|
49
|
+
createdBy: 'u-uuid-1',
|
|
50
|
+
createdAt: '2026-04-30T10:00:00Z',
|
|
51
|
+
updatedAt: '2026-04-30T10:00:00Z',
|
|
52
|
+
};
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// orboto_primer_fact_list
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
describe('orboto_primer_fact_list', () => {
|
|
57
|
+
it('resolves projectKey then GETs /projects/<id>/primer-facts with filters', async () => {
|
|
58
|
+
const calls = stub([
|
|
59
|
+
{ json: projectStub },
|
|
60
|
+
{
|
|
61
|
+
json: [
|
|
62
|
+
factStub,
|
|
63
|
+
{
|
|
64
|
+
...factStub,
|
|
65
|
+
id: 'f-2',
|
|
66
|
+
projectId: null,
|
|
67
|
+
category: 'conventions',
|
|
68
|
+
key: 'branching',
|
|
69
|
+
value: 'develop -> release',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
const res = await makePrimerFactListHandler(client)({
|
|
75
|
+
projectKey: 'ORB',
|
|
76
|
+
category: 'tech_stack',
|
|
77
|
+
verified: false,
|
|
78
|
+
includeWorkspace: true,
|
|
79
|
+
});
|
|
80
|
+
expect(calls[0].url).toContain('/projects/by-key/ORB');
|
|
81
|
+
expect(calls[1].url).toContain(`/projects/${projectStub.id}/primer-facts?`);
|
|
82
|
+
expect(calls[1].url).toContain('category=tech_stack');
|
|
83
|
+
expect(calls[1].url).toContain('verified=false');
|
|
84
|
+
expect(calls[1].url).toContain('includeWorkspace=true');
|
|
85
|
+
expect(calls[1].method).toBe('GET');
|
|
86
|
+
const text = res.content[0].text;
|
|
87
|
+
expect(text).toContain('[project]');
|
|
88
|
+
expect(text).toContain('[workspace]');
|
|
89
|
+
expect(text).toContain('tech_stack/package-manager');
|
|
90
|
+
expect(text).toContain('conventions/branching');
|
|
91
|
+
});
|
|
92
|
+
it('defaults includeWorkspace to true on the wire', async () => {
|
|
93
|
+
const calls = stub([
|
|
94
|
+
{ json: projectStub },
|
|
95
|
+
{ json: [] },
|
|
96
|
+
]);
|
|
97
|
+
await makePrimerFactListHandler(client)({ projectKey: 'ORB' });
|
|
98
|
+
expect(calls[1].url).toContain('includeWorkspace=true');
|
|
99
|
+
});
|
|
100
|
+
it('renders an empty-state message when no facts match', async () => {
|
|
101
|
+
stub([
|
|
102
|
+
{ json: projectStub },
|
|
103
|
+
{ json: [] },
|
|
104
|
+
]);
|
|
105
|
+
const res = await makePrimerFactListHandler(client)({ projectKey: 'ORB' });
|
|
106
|
+
expect(res.content[0].text).toBe('No primer facts in scope.');
|
|
107
|
+
});
|
|
108
|
+
it('rewrites a 403 into a permission-clarifying error', async () => {
|
|
109
|
+
stub([
|
|
110
|
+
{ json: projectStub },
|
|
111
|
+
{ ok: false, status: 403, json: { error: 'Forbidden' } },
|
|
112
|
+
]);
|
|
113
|
+
await expect(makePrimerFactListHandler(client)({ projectKey: 'ORB' })).rejects.toThrow(/project:edit|admin:ai:write/);
|
|
114
|
+
});
|
|
115
|
+
it('input schema rejects an unknown category', () => {
|
|
116
|
+
const schema = z.object(primerFactListToolConfig.inputSchema);
|
|
117
|
+
const result = schema.safeParse({ projectKey: 'ORB', category: 'nonsense' });
|
|
118
|
+
expect(result.success).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// orboto_primer_fact_add
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
describe('orboto_primer_fact_add', () => {
|
|
125
|
+
it('observed=false (default) sends source=manual', async () => {
|
|
126
|
+
const calls = stub([
|
|
127
|
+
{ json: projectStub },
|
|
128
|
+
{ json: factStub },
|
|
129
|
+
]);
|
|
130
|
+
await makePrimerFactAddHandler(client)({
|
|
131
|
+
projectKey: 'ORB',
|
|
132
|
+
category: 'tech_stack',
|
|
133
|
+
key: 'package-manager',
|
|
134
|
+
value: 'pnpm 9',
|
|
135
|
+
});
|
|
136
|
+
expect(calls[1].method).toBe('POST');
|
|
137
|
+
expect(calls[1].url).toContain(`/projects/${projectStub.id}/primer-facts`);
|
|
138
|
+
expect(calls[1].body).toMatchObject({
|
|
139
|
+
category: 'tech_stack',
|
|
140
|
+
key: 'package-manager',
|
|
141
|
+
value: 'pnpm 9',
|
|
142
|
+
source: 'manual',
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
it('observed=true sends source=agent_observed', async () => {
|
|
146
|
+
const calls = stub([
|
|
147
|
+
{ json: projectStub },
|
|
148
|
+
{ json: { ...factStub, source: 'agent_observed' } },
|
|
149
|
+
]);
|
|
150
|
+
await makePrimerFactAddHandler(client)({
|
|
151
|
+
projectKey: 'ORB',
|
|
152
|
+
category: 'gotchas',
|
|
153
|
+
key: 'firefox-cors',
|
|
154
|
+
value: 'Firefox rejects presigned S3 URLs from internal hosts',
|
|
155
|
+
observed: true,
|
|
156
|
+
});
|
|
157
|
+
expect(calls[1].body).toMatchObject({ source: 'agent_observed' });
|
|
158
|
+
});
|
|
159
|
+
it('rewrites a 409 into a "use supersede" hint', async () => {
|
|
160
|
+
stub([
|
|
161
|
+
{ json: projectStub },
|
|
162
|
+
{ ok: false, status: 409, json: { error: 'duplicate', errorKey: 'errors.primer_facts.duplicate_key' } },
|
|
163
|
+
]);
|
|
164
|
+
await expect(makePrimerFactAddHandler(client)({
|
|
165
|
+
projectKey: 'ORB',
|
|
166
|
+
category: 'tech_stack',
|
|
167
|
+
key: 'package-manager',
|
|
168
|
+
value: 'yarn',
|
|
169
|
+
})).rejects.toThrow(/orboto_primer_fact_supersede/);
|
|
170
|
+
});
|
|
171
|
+
it('input schema rejects an empty key', () => {
|
|
172
|
+
const schema = z.object(primerFactAddToolConfig.inputSchema);
|
|
173
|
+
const result = schema.safeParse({
|
|
174
|
+
projectKey: 'ORB',
|
|
175
|
+
category: 'tech_stack',
|
|
176
|
+
key: '',
|
|
177
|
+
value: 'v',
|
|
178
|
+
});
|
|
179
|
+
expect(result.success).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
it('input schema rejects an unknown category', () => {
|
|
182
|
+
const schema = z.object(primerFactAddToolConfig.inputSchema);
|
|
183
|
+
const result = schema.safeParse({
|
|
184
|
+
projectKey: 'ORB',
|
|
185
|
+
category: 'nonsense',
|
|
186
|
+
key: 'k',
|
|
187
|
+
value: 'v',
|
|
188
|
+
});
|
|
189
|
+
expect(result.success).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// orboto_primer_fact_update
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
describe('orboto_primer_fact_update', () => {
|
|
196
|
+
it('PATCHes /primer-facts/<id> with only the provided fields', async () => {
|
|
197
|
+
const calls = stub([{ json: { ...factStub, value: 'pnpm 10' } }]);
|
|
198
|
+
await makePrimerFactUpdateHandler(client)({
|
|
199
|
+
factId: factStub.id,
|
|
200
|
+
value: 'pnpm 10',
|
|
201
|
+
});
|
|
202
|
+
expect(calls[0].method).toBe('PATCH');
|
|
203
|
+
expect(calls[0].url).toContain(`/primer-facts/${factStub.id}`);
|
|
204
|
+
expect(calls[0].body).toEqual({ value: 'pnpm 10' });
|
|
205
|
+
});
|
|
206
|
+
it('throws when called with no fields to update', async () => {
|
|
207
|
+
await expect(makePrimerFactUpdateHandler(client)({ factId: factStub.id })).rejects.toThrow(/at least one of/);
|
|
208
|
+
});
|
|
209
|
+
it('rewrites a 404 into a "target not found" error', async () => {
|
|
210
|
+
stub([{ ok: false, status: 404, json: { error: 'Not found' } }]);
|
|
211
|
+
await expect(makePrimerFactUpdateHandler(client)({ factId: 'missing', value: 'v' })).rejects.toThrow(/not found/);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// orboto_primer_fact_supersede
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
describe('orboto_primer_fact_supersede', () => {
|
|
218
|
+
it('POSTs /primer-facts/<id>/supersede with category + key + value', async () => {
|
|
219
|
+
const calls = stub([{ json: { ...factStub, id: 'new-uuid', value: 'pnpm 10' } }]);
|
|
220
|
+
await makePrimerFactSupersedeHandler(client)({
|
|
221
|
+
oldFactId: factStub.id,
|
|
222
|
+
category: 'tech_stack',
|
|
223
|
+
key: 'package-manager',
|
|
224
|
+
value: 'pnpm 10',
|
|
225
|
+
});
|
|
226
|
+
expect(calls[0].method).toBe('POST');
|
|
227
|
+
expect(calls[0].url).toContain(`/primer-facts/${factStub.id}/supersede`);
|
|
228
|
+
expect(calls[0].body).toEqual({
|
|
229
|
+
category: 'tech_stack',
|
|
230
|
+
key: 'package-manager',
|
|
231
|
+
value: 'pnpm 10',
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
it('input schema rejects missing required fields', () => {
|
|
235
|
+
const schema = z.object(primerFactSupersedeToolConfig.inputSchema);
|
|
236
|
+
expect(schema.safeParse({ oldFactId: 'x', category: 'tech_stack', key: 'k' }).success).toBe(false);
|
|
237
|
+
expect(schema.safeParse({ oldFactId: 'x' }).success).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// orboto_primer_fact_verify
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
describe('orboto_primer_fact_verify', () => {
|
|
244
|
+
it('POSTs /primer-facts/<id>/verify with empty body', async () => {
|
|
245
|
+
const calls = stub([{ json: { ...factStub, verified: true, verifiedBy: 'u1', verifiedAt: 'now' } }]);
|
|
246
|
+
await makePrimerFactVerifyHandler(client)({ factId: factStub.id });
|
|
247
|
+
expect(calls[0].method).toBe('POST');
|
|
248
|
+
expect(calls[0].url).toContain(`/primer-facts/${factStub.id}/verify`);
|
|
249
|
+
expect(calls[0].body).toEqual({});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// orboto_primer_fact_delete
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
describe('orboto_primer_fact_delete', () => {
|
|
256
|
+
it('DELETEs /primer-facts/<id>', async () => {
|
|
257
|
+
const calls = stub([{ status: 204 }]);
|
|
258
|
+
const res = await makePrimerFactDeleteHandler(client)({ factId: factStub.id });
|
|
259
|
+
expect(calls[0].method).toBe('DELETE');
|
|
260
|
+
expect(calls[0].url).toContain(`/primer-facts/${factStub.id}`);
|
|
261
|
+
const text = res.content[0].text;
|
|
262
|
+
expect(text).toContain('Deleted primer fact');
|
|
263
|
+
});
|
|
264
|
+
it('echoes the optional reason in the text output', async () => {
|
|
265
|
+
stub([{ status: 204 }]);
|
|
266
|
+
const res = await makePrimerFactDeleteHandler(client)({
|
|
267
|
+
factId: factStub.id,
|
|
268
|
+
reason: 'fact was wrong from the start',
|
|
269
|
+
});
|
|
270
|
+
const text = res.content[0].text;
|
|
271
|
+
expect(text).toContain('fact was wrong from the start');
|
|
272
|
+
});
|
|
273
|
+
it('rewrites a 404 into a "target not found" error', async () => {
|
|
274
|
+
stub([{ ok: false, status: 404, json: { error: 'Not found' } }]);
|
|
275
|
+
await expect(makePrimerFactDeleteHandler(client)({ factId: 'missing' })).rejects.toThrow(/not found/);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
// ORB-1819 - the writing-for-tokens size contract, passed through from the
|
|
279
|
+
// REST route's warn (200/201 + sizeWarning) / block (422) responses.
|
|
280
|
+
describe('ORB-1819 size contract', () => {
|
|
281
|
+
it('add surfaces a soft-limit sizeWarning in the success text, not as an error', async () => {
|
|
282
|
+
stub([
|
|
283
|
+
{ json: projectStub },
|
|
284
|
+
{ status: 201, json: { ...factStub, value: 'x'.repeat(320), sizeWarning: { chars: 320, limit: 300, hint: 'move it to a doc' } } },
|
|
285
|
+
]);
|
|
286
|
+
const res = await makePrimerFactAddHandler(client)({
|
|
287
|
+
projectKey: 'ORB', category: 'tech_stack', key: 'long-fact', value: 'x'.repeat(320),
|
|
288
|
+
});
|
|
289
|
+
expect(res.isError).toBeUndefined();
|
|
290
|
+
const text = res.content[0].text;
|
|
291
|
+
expect(text).toContain('320 chars');
|
|
292
|
+
expect(text).toContain('move it to a doc');
|
|
293
|
+
});
|
|
294
|
+
it('add turns a hard-cap 422 into a non-throwing blocked result carrying the override recipe', async () => {
|
|
295
|
+
const blockBody = JSON.stringify({
|
|
296
|
+
error: 'oversize', errorKey: 'errors.agent_content.oversize',
|
|
297
|
+
sizeWarning: { chars: 650, limit: 600, hint: 'move it to a doc and keep the doc key here' },
|
|
298
|
+
});
|
|
299
|
+
let call = 0;
|
|
300
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
|
|
301
|
+
call += 1;
|
|
302
|
+
if (call === 1)
|
|
303
|
+
return { ok: true, status: 200, statusText: 'OK', json: async () => projectStub, text: async () => '' };
|
|
304
|
+
return { ok: false, status: 422, statusText: 'Unprocessable', json: async () => ({}), text: async () => blockBody };
|
|
305
|
+
});
|
|
306
|
+
const res = await makePrimerFactAddHandler(client)({
|
|
307
|
+
projectKey: 'ORB', category: 'tech_stack', key: 'way-too-long', value: 'x'.repeat(650),
|
|
308
|
+
});
|
|
309
|
+
expect(res.isError).toBe(true);
|
|
310
|
+
const text = res.content[0].text;
|
|
311
|
+
expect(text).toContain('650');
|
|
312
|
+
expect(text).toContain('allowOversize=true');
|
|
313
|
+
expect(text).toContain('oversizeReason');
|
|
314
|
+
const sc = res.structuredContent;
|
|
315
|
+
expect(sc.blocked).toBe(true);
|
|
316
|
+
expect(sc.sizeWarning.limit).toBe(600);
|
|
317
|
+
});
|
|
318
|
+
it('add passes allowOversize + oversizeReason through to the POST body', async () => {
|
|
319
|
+
const calls = stub([
|
|
320
|
+
{ json: projectStub },
|
|
321
|
+
{ status: 201, json: { ...factStub, sizeWarning: { chars: 650, limit: 600, hint: 'h' } } },
|
|
322
|
+
]);
|
|
323
|
+
await makePrimerFactAddHandler(client)({
|
|
324
|
+
projectKey: 'ORB', category: 'tech_stack', key: 'k', value: 'x'.repeat(650),
|
|
325
|
+
allowOversize: true, oversizeReason: 'genuinely needs the full spec',
|
|
326
|
+
});
|
|
327
|
+
expect(calls[1].body).toMatchObject({ allowOversize: true, oversizeReason: 'genuinely needs the full spec' });
|
|
328
|
+
});
|
|
329
|
+
it('update turns a hard-cap 422 into a non-throwing blocked result', async () => {
|
|
330
|
+
const blockBody = JSON.stringify({
|
|
331
|
+
error: 'oversize', errorKey: 'errors.agent_content.oversize',
|
|
332
|
+
sizeWarning: { chars: 700, limit: 600, hint: 'shorten it' },
|
|
333
|
+
});
|
|
334
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ({ ok: false, status: 422, statusText: 'Unprocessable', json: async () => ({}), text: async () => blockBody }));
|
|
335
|
+
const res = await makePrimerFactUpdateHandler(client)({ factId: factStub.id, value: 'x'.repeat(700) });
|
|
336
|
+
expect(res.isError).toBe(true);
|
|
337
|
+
const sc = res.structuredContent;
|
|
338
|
+
expect(sc.blocked).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
it('supersede turns a hard-cap 422 into a non-throwing blocked result', async () => {
|
|
341
|
+
const blockBody = JSON.stringify({
|
|
342
|
+
error: 'oversize', errorKey: 'errors.agent_content.oversize',
|
|
343
|
+
sizeWarning: { chars: 700, limit: 600, hint: 'shorten it' },
|
|
344
|
+
});
|
|
345
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ({ ok: false, status: 422, statusText: 'Unprocessable', json: async () => ({}), text: async () => blockBody }));
|
|
346
|
+
const res = await makePrimerFactSupersedeHandler(client)({
|
|
347
|
+
oldFactId: factStub.id, category: 'tech_stack', key: 'k', value: 'x'.repeat(700),
|
|
348
|
+
});
|
|
349
|
+
expect(res.isError).toBe(true);
|
|
350
|
+
const sc = res.structuredContent;
|
|
351
|
+
expect(sc.blocked).toBe(true);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-799 - per-project metadata listings.
|
|
3
|
+
*
|
|
4
|
+
* Two cheap read tools that round-trip the project-scoped enum tables
|
|
5
|
+
* agents repeatedly need:
|
|
6
|
+
*
|
|
7
|
+
* - orboto_list_ticket_statuses - the workflow's status rows with
|
|
8
|
+
* their IDs, categories, colors, and terminal flag. Needed for
|
|
9
|
+
* typed-transition flows where the standard `move_ticket` (which
|
|
10
|
+
* picks the first status per category) isn't precise enough.
|
|
11
|
+
*
|
|
12
|
+
* - orboto_list_labels - the per-project label catalogue with
|
|
13
|
+
* colors. Lets agents see what labels exist before calling
|
|
14
|
+
* `create_ticket` with a `labels` array (unknown names error).
|
|
15
|
+
*
|
|
16
|
+
* Both mirror the wrapper's `statuses <project>` / `labels <project>`
|
|
17
|
+
* subcommands.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { resolveProjectByKey } from './shared.js';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// orboto_list_ticket_statuses
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
export const listTicketStatusesToolConfig = {
|
|
25
|
+
title: 'List a project\'s ticket statuses',
|
|
26
|
+
description: 'Return the project\'s workflow rows - `{id, name, category, color, sortOrder, isTerminal}`. The `category` is the broad bucket (todo / in_progress / in_review / done / wont_fix); `name` is the workspace\'s display label (e.g. "Code Review"). Use the `id` to do typed transitions through `orboto_update_ticket` when the standard category-based move is too coarse.',
|
|
27
|
+
inputSchema: z.object({
|
|
28
|
+
projectKey: z.string().min(1).describe('Project key (e.g. "ACME").'),
|
|
29
|
+
}).shape,
|
|
30
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
31
|
+
};
|
|
32
|
+
export function makeListTicketStatusesHandler(client) {
|
|
33
|
+
return async ({ projectKey }) => {
|
|
34
|
+
const project = await resolveProjectByKey(client, projectKey);
|
|
35
|
+
const statuses = await client.get(`/projects/${project.id}/ticket-statuses`);
|
|
36
|
+
const text = statuses.length === 0
|
|
37
|
+
? `No statuses configured for ${project.key}.`
|
|
38
|
+
: statuses.map((s) => `${s.category.padEnd(12)} "${s.name}" ${s.color} sort=${s.sortOrder}${s.isTerminal ? ' (terminal)' : ''}`).join('\n');
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text }],
|
|
41
|
+
structuredContent: {
|
|
42
|
+
projectKey: project.key,
|
|
43
|
+
statuses: statuses.map((s) => ({
|
|
44
|
+
id: s.id,
|
|
45
|
+
name: s.name,
|
|
46
|
+
category: s.category,
|
|
47
|
+
color: s.color,
|
|
48
|
+
sortOrder: s.sortOrder,
|
|
49
|
+
isTerminal: s.isTerminal,
|
|
50
|
+
})),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// orboto_list_labels
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
export const listLabelsToolConfig = {
|
|
59
|
+
title: 'List a project\'s labels',
|
|
60
|
+
description: 'Return the project\'s label catalogue - `{id, name, color}`. Labels must be referenced by exact name when assigning via `orboto_create_ticket` (the API errors on unknown names rather than auto-creating); use this tool to discover what\'s available before assigning.',
|
|
61
|
+
inputSchema: z.object({
|
|
62
|
+
projectKey: z.string().min(1),
|
|
63
|
+
}).shape,
|
|
64
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
65
|
+
};
|
|
66
|
+
export function makeListLabelsHandler(client) {
|
|
67
|
+
return async ({ projectKey }) => {
|
|
68
|
+
const project = await resolveProjectByKey(client, projectKey);
|
|
69
|
+
const labels = await client.get(`/projects/${project.id}/labels`);
|
|
70
|
+
const text = labels.length === 0
|
|
71
|
+
? `No labels in ${project.key}.`
|
|
72
|
+
: labels.map((l) => `${l.name.padEnd(20)} ${l.color}`).join('\n');
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: 'text', text }],
|
|
75
|
+
structuredContent: {
|
|
76
|
+
projectKey: project.key,
|
|
77
|
+
labels: labels.map((l) => ({ id: l.id, name: l.name, color: l.color })),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// orboto_create_label - ORB-1041
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
export const createLabelToolConfig = {
|
|
86
|
+
title: 'Create a project label',
|
|
87
|
+
description: 'Create a label in a project so it can be assigned to tickets. Closes the gap where `orboto_create_ticket` errors on an unknown label name (labels are not auto-created). Idempotent: if a label with the same name already exists, it is returned instead of erroring. Needs project:edit.',
|
|
88
|
+
inputSchema: z.object({
|
|
89
|
+
projectKey: z.string().min(1).describe('Project key (e.g. "ACME").'),
|
|
90
|
+
name: z.string().min(1).max(100).describe('Label name.'),
|
|
91
|
+
color: z.string().optional().describe('Hex colour like "#6366f1". Defaults to indigo.'),
|
|
92
|
+
}).shape,
|
|
93
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
94
|
+
};
|
|
95
|
+
export function makeCreateLabelHandler(client) {
|
|
96
|
+
return async ({ projectKey, name, color }) => {
|
|
97
|
+
const project = await resolveProjectByKey(client, projectKey);
|
|
98
|
+
// Idempotent: return an existing same-name label rather than erroring.
|
|
99
|
+
const existing = await client.get(`/projects/${project.id}/labels`);
|
|
100
|
+
const match = existing.find((l) => l.name.toLowerCase() === name.toLowerCase());
|
|
101
|
+
if (match) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: 'text', text: `Label "${match.name}" already exists in ${project.key}.` }],
|
|
104
|
+
structuredContent: { projectKey: project.key, label: { name: match.name, color: match.color }, alreadyExists: true },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const body = { name };
|
|
108
|
+
if (color)
|
|
109
|
+
body.color = color;
|
|
110
|
+
const created = await client.post(`/projects/${project.id}/labels`, body);
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: 'text', text: `Created label "${created.name}" (${created.color}) in ${project.key}.` }],
|
|
113
|
+
structuredContent: { projectKey: project.key, label: { name: created.name, color: created.color } },
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-799 - `orboto_list_ticket_statuses` + `orboto_list_labels` tests.
|
|
3
|
+
*/
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { OrbotoApiError, OrbotoClient } from '../orboto-client.js';
|
|
6
|
+
import { makeListTicketStatusesHandler, makeListLabelsHandler, makeCreateLabelHandler } from './project-listings.js';
|
|
7
|
+
beforeEach(() => { vi.restoreAllMocks(); });
|
|
8
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
9
|
+
function stub(responses) {
|
|
10
|
+
const calls = [];
|
|
11
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
12
|
+
calls.push({ url: url.toString(), method: init?.method ?? 'GET' });
|
|
13
|
+
const r = responses.shift();
|
|
14
|
+
if (!r)
|
|
15
|
+
throw new Error(`unexpected extra fetch`);
|
|
16
|
+
return {
|
|
17
|
+
ok: r.ok ?? true,
|
|
18
|
+
status: r.status ?? 200,
|
|
19
|
+
statusText: 'OK',
|
|
20
|
+
json: async () => ('json' in r ? r.json : {}),
|
|
21
|
+
text: async () => '',
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return calls;
|
|
25
|
+
}
|
|
26
|
+
const client = new OrbotoClient({ baseUrl: 'https://orboto.example.com', apiKey: 'orb_x' });
|
|
27
|
+
const PROJ = { id: 'p1', key: 'ACME', name: 'Acme', description: null, status: 'active' };
|
|
28
|
+
describe('orboto_list_ticket_statuses', () => {
|
|
29
|
+
it('renders status rows + structured content', async () => {
|
|
30
|
+
stub([
|
|
31
|
+
{ json: PROJ },
|
|
32
|
+
{ json: [
|
|
33
|
+
{ id: 's1', name: 'To Do', category: 'todo', color: '#aaa', sortOrder: 0, isTerminal: false },
|
|
34
|
+
{ id: 's2', name: 'Done', category: 'done', color: '#0a0', sortOrder: 10, isTerminal: true },
|
|
35
|
+
] },
|
|
36
|
+
]);
|
|
37
|
+
const res = await makeListTicketStatusesHandler(client)({ projectKey: 'ACME' });
|
|
38
|
+
expect(res.content[0].text).toContain('To Do');
|
|
39
|
+
expect(res.content[0].text).toContain('(terminal)');
|
|
40
|
+
expect(res.structuredContent).toMatchObject({
|
|
41
|
+
projectKey: 'ACME',
|
|
42
|
+
statuses: [
|
|
43
|
+
{ name: 'To Do', category: 'todo', isTerminal: false },
|
|
44
|
+
{ name: 'Done', category: 'done', isTerminal: true },
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('empty project returns helpful text', async () => {
|
|
49
|
+
stub([{ json: PROJ }, { json: [] }]);
|
|
50
|
+
const res = await makeListTicketStatusesHandler(client)({ projectKey: 'ACME' });
|
|
51
|
+
expect(res.content[0].text).toContain('No statuses');
|
|
52
|
+
});
|
|
53
|
+
it('surfaces 404 on missing project key', async () => {
|
|
54
|
+
stub([{ ok: false, status: 404, json: { error: 'not found' } }]);
|
|
55
|
+
await expect(makeListTicketStatusesHandler(client)({ projectKey: 'NOPE' })).rejects.toThrow(/Project "NOPE" not found/);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('orboto_list_labels', () => {
|
|
59
|
+
it('renders label rows + structured content', async () => {
|
|
60
|
+
stub([
|
|
61
|
+
{ json: PROJ },
|
|
62
|
+
{ json: [
|
|
63
|
+
{ id: 'l1', name: 'bug', color: '#f00' },
|
|
64
|
+
{ id: 'l2', name: 'priority', color: '#ff0' },
|
|
65
|
+
] },
|
|
66
|
+
]);
|
|
67
|
+
const res = await makeListLabelsHandler(client)({ projectKey: 'ACME' });
|
|
68
|
+
expect(res.content[0].text).toContain('bug');
|
|
69
|
+
expect(res.structuredContent).toMatchObject({
|
|
70
|
+
projectKey: 'ACME',
|
|
71
|
+
labels: [
|
|
72
|
+
{ name: 'bug', color: '#f00' },
|
|
73
|
+
{ name: 'priority', color: '#ff0' },
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
it('surfaces an OrbotoApiError on 403', async () => {
|
|
78
|
+
stub([
|
|
79
|
+
{ json: PROJ },
|
|
80
|
+
{ ok: false, status: 403, json: { error: 'forbidden' } },
|
|
81
|
+
]);
|
|
82
|
+
await expect(makeListLabelsHandler(client)({ projectKey: 'ACME' })).rejects.toBeInstanceOf(OrbotoApiError);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('orboto_create_label (ORB-1041)', () => {
|
|
86
|
+
it('creates a new label via POST when the name is free', async () => {
|
|
87
|
+
const calls = stub([
|
|
88
|
+
{ json: PROJ },
|
|
89
|
+
{ json: [{ id: 'l1', name: 'bug', color: '#f00' }] }, // existing labels
|
|
90
|
+
{ json: { id: 'l2', name: 'security', color: '#6366f1' } }, // POST result
|
|
91
|
+
]);
|
|
92
|
+
const res = await makeCreateLabelHandler(client)({ projectKey: 'ACME', name: 'security' });
|
|
93
|
+
const post = calls.find((c) => c.method === 'POST');
|
|
94
|
+
expect(post.url).toContain('/projects/p1/labels');
|
|
95
|
+
expect(res.structuredContent).toMatchObject({ projectKey: 'ACME', label: { name: 'security' } });
|
|
96
|
+
});
|
|
97
|
+
it('is idempotent: returns the existing label without POSTing', async () => {
|
|
98
|
+
const calls = stub([
|
|
99
|
+
{ json: PROJ },
|
|
100
|
+
{ json: [{ id: 'l1', name: 'Bug', color: '#f00' }] },
|
|
101
|
+
]);
|
|
102
|
+
const res = await makeCreateLabelHandler(client)({ projectKey: 'ACME', name: 'bug' });
|
|
103
|
+
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
104
|
+
expect(res.structuredContent).toMatchObject({ alreadyExists: true, label: { name: 'Bug' } });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-273 Phase F - `orboto_query`.
|
|
3
|
+
*
|
|
4
|
+
* Lets the model reach for the OQL DSL when the per-entity list
|
|
5
|
+
* tools (`orboto_list_tickets`, `orboto_my_tickets`) get too narrow:
|
|
6
|
+
* combined `assignee + dueDate + label + statusCategory` filters
|
|
7
|
+
* with explicit ORDER BY are awkward to express through tool
|
|
8
|
+
* arguments but trivial in OQL. Falls back to JQL syntax via
|
|
9
|
+
* `syntax: 'jql'` for migrants who copy-pasted a Jira query.
|
|
10
|
+
*
|
|
11
|
+
* The endpoint is `POST /query` (ORB-531) which honours PBAC,
|
|
12
|
+
* cursor-paginates, and rate-limits at 60/min. We expose the
|
|
13
|
+
* underlying envelope unchanged so a future "give me the next page"
|
|
14
|
+
* tool can plumb the cursor through.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { ticketLine, agentTicketListRow } from './shared.js';
|
|
18
|
+
export const queryToolConfig = {
|
|
19
|
+
title: 'Run an OQL / JQL query',
|
|
20
|
+
description: 'Run a typed OQL (or Jira-shaped JQL) query against the workspace and return matching tickets. '
|
|
21
|
+
+ 'OQL grammar: `field operator value [AND|OR ...] [ORDER BY ...] [LIMIT n]`. '
|
|
22
|
+
+ 'Supported fields: key, status, statusName, statusCategory, priority, type, assignee, reporter, reporterType, assigneeType, labels, milestone, version, project, isPrivate, dueDate, startDate, createdAt, updatedAt, closedAt, estimatedTimeMinutes, loggedMinutes, parentKey. '
|
|
23
|
+
+ 'reporterType / assigneeType are guest|internal - use `reporterType = guest OR assigneeType = guest` to pull up everything reported by or assigned to guests without listing each guest email. '
|
|
24
|
+
+ 'Operators: = != < <= > >= ~ !~ IN IS [NOT] NULL IS [NOT] EMPTY. '
|
|
25
|
+
+ 'Functions: currentUser(), now(), startOfWeek(), endOfWeek(), startOfMonth(), endOfMonth(), daysAgo(n). '
|
|
26
|
+
+ 'Examples: '
|
|
27
|
+
+ '`project = ORB AND assignee = currentUser() AND statusCategory != done`; '
|
|
28
|
+
+ '`priority IN (blocker, high) AND dueDate <= now() ORDER BY dueDate ASC LIMIT 25`; '
|
|
29
|
+
+ '`labels = "bug" AND createdAt >= daysAgo(7)`. '
|
|
30
|
+
+ 'Reach for this tool when per-entity tools (orboto_list_tickets / orboto_my_tickets) cannot express the combined filter.',
|
|
31
|
+
inputSchema: z.object({
|
|
32
|
+
oql: z.string().min(1).max(8000).describe('The query string.'),
|
|
33
|
+
syntax: z.enum(['oql', 'jql']).default('oql').describe('"jql" accepts Jira-flavoured syntax.'),
|
|
34
|
+
cursor: z.string().optional().describe('Cursor from a previous response.'),
|
|
35
|
+
limit: z.number().int().min(1).max(100).default(25).describe('Max rows. Default 25.'),
|
|
36
|
+
verbose: z.boolean().default(false).describe('true = full rows; default is the decision fields only.'),
|
|
37
|
+
}).shape,
|
|
38
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
39
|
+
};
|
|
40
|
+
export function makeQueryHandler(client) {
|
|
41
|
+
return async (input) => {
|
|
42
|
+
const body = {
|
|
43
|
+
oql: input.oql,
|
|
44
|
+
syntax: input.syntax ?? 'oql',
|
|
45
|
+
cursor: input.cursor,
|
|
46
|
+
limit: input.limit ?? 25,
|
|
47
|
+
};
|
|
48
|
+
const page = await client.post('/query', body);
|
|
49
|
+
const text = page.items.length === 0
|
|
50
|
+
? 'No tickets matched.'
|
|
51
|
+
: page.items.map((t) => `- ${ticketLine(t)}`).join('\n');
|
|
52
|
+
const moreHint = page.nextCursor
|
|
53
|
+
? `\n\n(${page.items.length} shown; pass cursor="${page.nextCursor}" to fetch the next page.)`
|
|
54
|
+
: '';
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: 'text', text: text + moreHint }],
|
|
57
|
+
structuredContent: {
|
|
58
|
+
count: page.items.length,
|
|
59
|
+
nextCursor: page.nextCursor,
|
|
60
|
+
// ORB-1699 - shared lean row; verbose restores uuid/labels/minutes.
|
|
61
|
+
tickets: page.items.map((t) => agentTicketListRow(t, input.verbose ?? false)),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
}
|