@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,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-244 Phase B - `orboto_get_checklists`.
|
|
3
|
+
*
|
|
4
|
+
* Dedicated read tool for a ticket's checklists. Two callers want
|
|
5
|
+
* this without paying for the full ticket payload:
|
|
6
|
+
*
|
|
7
|
+
* 1. Models asking "what's still unchecked on ACME-42?" - don't
|
|
8
|
+
* need description + comments + git activity for that.
|
|
9
|
+
* 2. Phase-C write tools (`orboto_check` / `orboto_uncheck`) - the
|
|
10
|
+
* user usually wants to confirm the item exists before
|
|
11
|
+
* toggling it, and this tool is the cheap round-trip.
|
|
12
|
+
*
|
|
13
|
+
* ORB-234 detail exposed here: when a checklist item links to
|
|
14
|
+
* another ticket, `effectiveCompleted` mirrors that ticket's status
|
|
15
|
+
* category. We surface the linked ticket so the model can explain
|
|
16
|
+
* "item is done because [ORB-99] shipped" rather than just "done".
|
|
17
|
+
*/
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { resolveTicketByKey } from './shared.js';
|
|
20
|
+
export const getChecklistsToolConfig = {
|
|
21
|
+
title: 'Get ticket checklists',
|
|
22
|
+
description: 'Return all checklists on a ticket with per-item completion state. Items that link to another ticket carry that ticket\'s key + status so the model can explain why an item is (not) done.',
|
|
23
|
+
inputSchema: z.object({
|
|
24
|
+
ticketKey: z.string().min(3).describe('Ticket key like "ACME-42".'),
|
|
25
|
+
}).shape,
|
|
26
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
27
|
+
};
|
|
28
|
+
export function makeGetChecklistsHandler(client) {
|
|
29
|
+
return async ({ ticketKey }) => {
|
|
30
|
+
const ticket = await resolveTicketByKey(client, ticketKey);
|
|
31
|
+
const checklists = await client.get(`/tickets/${ticket.id}/checklists`);
|
|
32
|
+
if (checklists.length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: 'text', text: `[${ticket.ticketKey}] has no checklists.` }],
|
|
35
|
+
structuredContent: { ticketKey: ticket.ticketKey, checklists: [] },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const lines = [];
|
|
39
|
+
for (const cl of checklists) {
|
|
40
|
+
lines.push(`### ${cl.title} (${cl.progress.done}/${cl.progress.total})${cl.triggersDone ? ' · triggers ticket done' : ''}`);
|
|
41
|
+
for (const i of cl.items) {
|
|
42
|
+
const linkSuffix = i.linkedTicketKey
|
|
43
|
+
? ` ↪ [${i.linkedTicketKey}] ${i.linkedTicketTitle ?? ''} (${i.linkedTicketStatusCategory ?? 'unknown'})`
|
|
44
|
+
: '';
|
|
45
|
+
lines.push(`- [${i.effectiveCompleted ? 'x' : ' '}] ${i.content}${linkSuffix}`);
|
|
46
|
+
}
|
|
47
|
+
lines.push('');
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: 'text', text: lines.join('\n').trimEnd() }],
|
|
51
|
+
structuredContent: {
|
|
52
|
+
ticketKey: ticket.ticketKey,
|
|
53
|
+
checklists: checklists.map((cl) => ({
|
|
54
|
+
id: cl.id,
|
|
55
|
+
title: cl.title,
|
|
56
|
+
triggersDone: cl.triggersDone,
|
|
57
|
+
progress: cl.progress,
|
|
58
|
+
items: cl.items.map((i) => ({
|
|
59
|
+
id: i.id,
|
|
60
|
+
content: i.content,
|
|
61
|
+
done: i.effectiveCompleted,
|
|
62
|
+
linkedTicket: i.linkedTicketKey ? {
|
|
63
|
+
key: i.linkedTicketKey,
|
|
64
|
+
title: i.linkedTicketTitle,
|
|
65
|
+
statusCategory: i.linkedTicketStatusCategory,
|
|
66
|
+
} : null,
|
|
67
|
+
})),
|
|
68
|
+
})),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-408 (Phase 3 of ORB-406) - `orboto_get_project_primer`.
|
|
3
|
+
*
|
|
4
|
+
* Returns the project's auto-generated AI Context Pack as a single
|
|
5
|
+
* markdown blob, token-budget aware. AI agents call this as their
|
|
6
|
+
* first read per session.
|
|
7
|
+
*
|
|
8
|
+
* What's actually in the primer (ORB-563):
|
|
9
|
+
* - active milestones, ticket counts, recently-closed milestones
|
|
10
|
+
* - top docs from the project space
|
|
11
|
+
* - structured project facts (the `primer_facts` table - workspace +
|
|
12
|
+
* project rows)
|
|
13
|
+
* - recent activity, when the operator enabled it
|
|
14
|
+
* - repo briefings (e.g. CLAUDE.md / AGENTS.md) ONLY when the
|
|
15
|
+
* operator configured `repoFiles` AND the API host has those files
|
|
16
|
+
* on its local filesystem. Most Coolify / SaaS deployments do
|
|
17
|
+
* neither - agents must NOT assume CLAUDE.md / AGENTS.md content
|
|
18
|
+
* is in the primer. If the team relies on CLAUDE.md / AGENTS.md
|
|
19
|
+
* content cross-deployment, those conventions belong in
|
|
20
|
+
* `primer_facts` instead.
|
|
21
|
+
*
|
|
22
|
+
* Sections that didn't make the budget come back in
|
|
23
|
+
* `truncatedSections` so the agent can decide whether to bump the
|
|
24
|
+
* budget for a follow-up call.
|
|
25
|
+
*/
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
import { resolveProjectByKey } from './shared.js';
|
|
28
|
+
export const getProjectPrimerToolConfig = {
|
|
29
|
+
title: 'Get project primer (AI Context Pack)',
|
|
30
|
+
description: 'Returns the project\'s auto-generated session primer in a single call: active milestones, ticket counts, recently-closed milestones, top docs, structured primer facts, and recent activity. Primer facts merge three scopes in order Workspace -> Project -> Personal: the Personal block ("Your preferences") is the calling user\'s own personal-scope facts, included only when that user has opted in (ai_preferences_enabled) and never showing another user\'s. Repo briefings (CLAUDE.md / AGENTS.md) are included ONLY when the operator configured them AND the API host can read those files from disk - most deployments will not have them, so do not assume CLAUDE.md / AGENTS.md content is in the response. Token-budget aware - pass `maxTokens` to constrain the output. Input is the project key (e.g. "ORB"), case-insensitive.',
|
|
31
|
+
inputSchema: z.object({
|
|
32
|
+
projectKey: z.string().min(1).describe('Project key, case-insensitive.'),
|
|
33
|
+
maxTokens: z.number().int().positive().max(200000).optional()
|
|
34
|
+
.describe('Cap the output. Default: the project\'s max_tokens.'),
|
|
35
|
+
}).shape,
|
|
36
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
37
|
+
};
|
|
38
|
+
export function makeGetProjectPrimerHandler(client) {
|
|
39
|
+
return async ({ projectKey, maxTokens }) => {
|
|
40
|
+
const project = await resolveProjectByKey(client, projectKey);
|
|
41
|
+
const qs = new URLSearchParams({ format: 'json' });
|
|
42
|
+
if (maxTokens !== undefined)
|
|
43
|
+
qs.set('max_tokens', String(maxTokens));
|
|
44
|
+
const res = await client.get(`/projects/${project.id}/ai-primer?${qs.toString()}`);
|
|
45
|
+
// ORB-1104 - the trimmed-sections note is now appended in-band by
|
|
46
|
+
// assembleWithBudget, so res.markdown already carries it (and so do
|
|
47
|
+
// baked snapshots + the raw markdown route). No manual append here,
|
|
48
|
+
// otherwise it would double up.
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: res.markdown,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-244 Phase B - `orboto_get_project`.
|
|
3
|
+
*
|
|
4
|
+
* Returns metadata + milestones + labels + members for a project.
|
|
5
|
+
* Accepts the human-readable project key (`ACME`), case-insensitive
|
|
6
|
+
* - the API's `GET /projects/by-key/:key` does the lookup, the rest
|
|
7
|
+
* of the hydration (milestones + labels + members) comes from the
|
|
8
|
+
* `/projects/:id/*` endpoints that take the resolved UUID.
|
|
9
|
+
*
|
|
10
|
+
* Members endpoint returns a nested `{user: {...}, role: {...}}`
|
|
11
|
+
* shape; we flatten here into a card-friendly line.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { resolveProjectByKey } from './shared.js';
|
|
15
|
+
export const getProjectToolConfig = {
|
|
16
|
+
title: 'Get project details',
|
|
17
|
+
description: 'Return a single project with its milestones, labels, and members. Input is the project key like "ACME" (case-insensitive).',
|
|
18
|
+
inputSchema: z.object({
|
|
19
|
+
projectKey: z.string().min(1).describe('Project key (e.g. "ACME"). Case-insensitive.'),
|
|
20
|
+
}).shape,
|
|
21
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
22
|
+
};
|
|
23
|
+
export function makeGetProjectHandler(client) {
|
|
24
|
+
return async ({ projectKey }) => {
|
|
25
|
+
const project = await resolveProjectByKey(client, projectKey);
|
|
26
|
+
// Parallel fan-out - the API doesn't have a single aggregate route
|
|
27
|
+
// for milestones+labels+members, and three parallel fetches beat a
|
|
28
|
+
// chain on the default Node HTTP pool.
|
|
29
|
+
const [milestones, labels, members] = await Promise.all([
|
|
30
|
+
client.get(`/projects/${project.id}/milestones`),
|
|
31
|
+
client.get(`/projects/${project.id}/labels`),
|
|
32
|
+
client.get(`/projects/${project.id}/members`),
|
|
33
|
+
]);
|
|
34
|
+
const lines = [
|
|
35
|
+
`Project ${project.key} - ${project.name} (${project.status})`,
|
|
36
|
+
project.description ? `Description: ${project.description}` : null,
|
|
37
|
+
'',
|
|
38
|
+
`Milestones (${milestones.length}):`,
|
|
39
|
+
...milestones.map((m) => ` - ${m.milestoneKey ? `${m.milestoneKey} · ` : ''}${m.name} [${m.status}]${m.endDate ? ` due ${m.endDate}` : ''}`),
|
|
40
|
+
'',
|
|
41
|
+
`Labels: ${labels.map((l) => l.name).join(', ') || '(none)'}`,
|
|
42
|
+
// ORB-1040 - only mention RACI when the project opted in.
|
|
43
|
+
project.raciEnabled ? 'RACI: enabled' : null,
|
|
44
|
+
'',
|
|
45
|
+
`Members (${members.length}):`,
|
|
46
|
+
...members.map((m) => {
|
|
47
|
+
const name = m.user.fullName || m.user.email;
|
|
48
|
+
return ` - ${name} <${m.user.email}> - ${m.role.name}`;
|
|
49
|
+
}),
|
|
50
|
+
].filter((l) => l !== null);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
53
|
+
structuredContent: {
|
|
54
|
+
project: {
|
|
55
|
+
// ORB-1042 - surface the UUID for the rare APIs that still need it
|
|
56
|
+
// (most agent surfaces accept the key).
|
|
57
|
+
id: project.id,
|
|
58
|
+
key: project.key,
|
|
59
|
+
name: project.name,
|
|
60
|
+
status: project.status,
|
|
61
|
+
description: project.description,
|
|
62
|
+
// ORB-1040 - RACI is opt-in; agents must not raise/set RACI here
|
|
63
|
+
// unless this is true.
|
|
64
|
+
raciEnabled: project.raciEnabled ?? false,
|
|
65
|
+
},
|
|
66
|
+
milestones,
|
|
67
|
+
labels,
|
|
68
|
+
members: members.map((m) => ({
|
|
69
|
+
userId: m.userId,
|
|
70
|
+
fullName: m.user.fullName,
|
|
71
|
+
email: m.user.email,
|
|
72
|
+
roleName: m.role.name,
|
|
73
|
+
})),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-244 Phase B - `orboto_get_ticket`.
|
|
3
|
+
*
|
|
4
|
+
* Returns a ticket's full context - description, comments, assignees,
|
|
5
|
+
* labels, checklists, git activity - in a shape the model can
|
|
6
|
+
* reason about without follow-up calls. Comments and checklists are
|
|
7
|
+
* fetched in parallel with the ticket payload; git activity is
|
|
8
|
+
* skipped when the ticket's `gitActivityCount` is 0 so we don't waste
|
|
9
|
+
* a round-trip on the common case.
|
|
10
|
+
*
|
|
11
|
+
* ORB-272: `/tickets/:id/comments` is cursor-paginated. We pull the
|
|
12
|
+
* first page (50 by default) - if the ticket has more, a footer line
|
|
13
|
+
* nudges the user to open it in the UI. AI agents asking "give me
|
|
14
|
+
* the full history" beyond 50 is a rare enough case not to fan out.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { OrbotoApiError } from '../orboto-client.js';
|
|
18
|
+
import { resolveTicketByKey } from './shared.js';
|
|
19
|
+
const COMMENT_PAGE_SIZE = 50;
|
|
20
|
+
export const getTicketToolConfig = {
|
|
21
|
+
title: 'Get ticket details',
|
|
22
|
+
description: 'Return a ticket\'s decision card: description, status, priority, milestone, assignees, labels, dates, estimate/logged time, checklist progress, parent + sub-ticket count - plus COUNTS for everything omitted (commentCount, gitActivityCount, attachmentCount, childCount). Full blocks are opt-in via `include` (ORB-1698): pass e.g. include: ["comments"] to get the comment bodies, ["git","attachments","children","raci","checklistItems"] likewise. Input is the ticket key like "ACME-42".',
|
|
23
|
+
inputSchema: z.object({
|
|
24
|
+
ticketKey: z.string().min(3).describe('Ticket key like "ACME-42".'),
|
|
25
|
+
include: z.array(z.enum(['comments', 'git', 'attachments', 'children', 'raci', 'checklistItems']))
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Blocks to inline in full. Default: none (counts only).'),
|
|
28
|
+
}).shape,
|
|
29
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
30
|
+
};
|
|
31
|
+
export function makeGetTicketHandler(client) {
|
|
32
|
+
return async ({ ticketKey, include }) => {
|
|
33
|
+
const ticket = await resolveTicketByKey(client, ticketKey);
|
|
34
|
+
const inc = new Set(include ?? []);
|
|
35
|
+
const parentId = ticket.parentTicketId ?? null;
|
|
36
|
+
// ORB-1698 - the default response is the decision CARD: identity +
|
|
37
|
+
// description + progress + counts. The heavy blocks (comment bodies,
|
|
38
|
+
// git rows, checklist items) are fetched only when asked for via
|
|
39
|
+
// `include` - measured, they were the bulk of a 1.02 Mtok carry cost.
|
|
40
|
+
// Children + attachments have no count on the enriched row, so their
|
|
41
|
+
// (cheap, metadata-only) fetches stay - the RESPONSE carries only the
|
|
42
|
+
// count unless included.
|
|
43
|
+
const [enriched, parent, childrenPage, attachments, commentsPage, checklists, gitActivity] = await Promise.all([
|
|
44
|
+
// ORB-1023 - `resolveTicketByKey` hits the by-key endpoint, which
|
|
45
|
+
// returns a BARE ticket row (no statusCategory, assignees, labels,
|
|
46
|
+
// milestoneName, counts). Re-fetch the enriched by-id shape; falls
|
|
47
|
+
// back to the bare row on a 404.
|
|
48
|
+
client.get(`/projects/${ticket.projectId}/tickets/${ticket.id}`).catch(swallow404(null)),
|
|
49
|
+
// Parent ticket - only fetched when set. Lets the model say
|
|
50
|
+
// "this is sub-ticket of [ACME-10]" without a second tool call.
|
|
51
|
+
parentId
|
|
52
|
+
? client.get(`/projects/${ticket.projectId}/tickets/${parentId}`).catch(swallow404(null))
|
|
53
|
+
: Promise.resolve(null),
|
|
54
|
+
// Children via the parentTicketId filter (API-side, O(children)).
|
|
55
|
+
// Cap at 50; anything bigger should use `orboto_list_tickets
|
|
56
|
+
// --parentTicketKey` and paginate explicitly.
|
|
57
|
+
client.get(`/projects/${ticket.projectId}/tickets?parentTicketId=${ticket.id}&limit=50`).catch(swallow404({ items: [], nextCursor: null })),
|
|
58
|
+
// ORB-1455 - attachments metadata so the agent knows files exist.
|
|
59
|
+
client.get(`/tickets/${ticket.id}/attachments`).catch(swallow404([])),
|
|
60
|
+
inc.has('comments')
|
|
61
|
+
? client.get(`/tickets/${ticket.id}/comments?limit=${COMMENT_PAGE_SIZE}`).catch(swallow404({ items: [], nextCursor: null }))
|
|
62
|
+
: Promise.resolve({ items: [], nextCursor: null }),
|
|
63
|
+
inc.has('checklistItems')
|
|
64
|
+
? client.get(`/tickets/${ticket.id}/checklists`).catch(swallow404([]))
|
|
65
|
+
: Promise.resolve([]),
|
|
66
|
+
inc.has('git')
|
|
67
|
+
? client.get(`/tickets/${ticket.id}/git-activity`).catch(swallow404([]))
|
|
68
|
+
: Promise.resolve([]),
|
|
69
|
+
]);
|
|
70
|
+
const comments = commentsPage.items;
|
|
71
|
+
const hasMoreComments = !!commentsPage.nextCursor;
|
|
72
|
+
const children = childrenPage.items;
|
|
73
|
+
// ORB-1023 - prefer the enriched by-id row (statusCategory, assignees,
|
|
74
|
+
// labels, milestoneName); fall back to the bare resolver row.
|
|
75
|
+
const full = enriched ?? ticket;
|
|
76
|
+
const commentCount = full.commentCount ?? (inc.has('comments') ? comments.length : 0);
|
|
77
|
+
const gitCount = full.gitActivityCount ?? (inc.has('git') ? gitActivity.length : 0);
|
|
78
|
+
const omitted = [];
|
|
79
|
+
if (!inc.has('comments') && commentCount > 0)
|
|
80
|
+
omitted.push(`comments (${commentCount})`);
|
|
81
|
+
if (!inc.has('git') && gitCount > 0)
|
|
82
|
+
omitted.push(`git (${gitCount})`);
|
|
83
|
+
if (!inc.has('attachments') && attachments.length > 0)
|
|
84
|
+
omitted.push(`attachments (${attachments.length})`);
|
|
85
|
+
if (!inc.has('children') && children.length > 0)
|
|
86
|
+
omitted.push(`children (${children.length})`);
|
|
87
|
+
if (!inc.has('checklistItems') && (full.checklistProgress?.total ?? 0) > 0)
|
|
88
|
+
omitted.push(`checklistItems (${full.checklistProgress.total})`);
|
|
89
|
+
const includeHint = omitted.length > 0
|
|
90
|
+
? `Omitted blocks: ${omitted.join(', ')}. Re-call orboto_get_ticket with include: ["comments"|"git"|"attachments"|"children"|"raci"|"checklistItems"] for the ones you need.`
|
|
91
|
+
: undefined;
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: 'text', text: formatTicket(full, inc, comments, hasMoreComments, checklists, gitActivity, parent, children, attachments, includeHint) }],
|
|
94
|
+
structuredContent: {
|
|
95
|
+
// ORB-1179 - surface the uuid alongside the key.
|
|
96
|
+
id: full.id,
|
|
97
|
+
key: full.ticketKey,
|
|
98
|
+
title: full.title,
|
|
99
|
+
status: full.statusName ?? full.status,
|
|
100
|
+
statusCategory: full.statusCategory ?? null,
|
|
101
|
+
milestone: full.milestoneId
|
|
102
|
+
? { id: full.milestoneId, name: full.milestoneName ?? null }
|
|
103
|
+
: null,
|
|
104
|
+
priority: full.priority,
|
|
105
|
+
type: full.type,
|
|
106
|
+
// ORB-1608 - role-aware commit policy. The API defaults unset
|
|
107
|
+
// rows to 'implementation'.
|
|
108
|
+
deliveryMode: full.deliveryMode ?? 'implementation',
|
|
109
|
+
dueDate: full.dueDate,
|
|
110
|
+
startDate: full.startDate,
|
|
111
|
+
isPrivate: full.isPrivate,
|
|
112
|
+
estimatedTimeMinutes: full.estimatedTimeMinutes,
|
|
113
|
+
loggedMinutes: full.loggedMinutes ?? 0,
|
|
114
|
+
// ORB-1605 - true when in_review, zero ingested git_activities,
|
|
115
|
+
// but the project HAS an active git connection: closing
|
|
116
|
+
// verification may be blocked on stalled ingestion.
|
|
117
|
+
waitingForGitIngestion: full.waitingForGitIngestion ?? false,
|
|
118
|
+
description: full.description ?? null,
|
|
119
|
+
// Hierarchy - null when no parent, array of summary rows for
|
|
120
|
+
// children (empty array when none). Sub-ticket consumers can
|
|
121
|
+
// decide to call orboto_get_ticket on each for the full detail.
|
|
122
|
+
parentTicket: parent ? {
|
|
123
|
+
key: parent.ticketKey,
|
|
124
|
+
title: parent.title,
|
|
125
|
+
status: parent.statusName ?? parent.status,
|
|
126
|
+
statusCategory: parent.statusCategory ?? null,
|
|
127
|
+
} : null,
|
|
128
|
+
childCount: children.length,
|
|
129
|
+
...(inc.has('children') ? {
|
|
130
|
+
children: children.map((c) => ({
|
|
131
|
+
key: c.ticketKey,
|
|
132
|
+
title: c.title,
|
|
133
|
+
status: c.statusName ?? c.status,
|
|
134
|
+
statusCategory: c.statusCategory ?? null,
|
|
135
|
+
})),
|
|
136
|
+
} : {}),
|
|
137
|
+
assignees: full.assignees ?? [],
|
|
138
|
+
// ORB-1034 - RACI roster (R/A/C/I); opt-in via include: ["raci"]
|
|
139
|
+
// (assignees above stay the always-on Responsible+Accountable set).
|
|
140
|
+
...(inc.has('raci') ? {
|
|
141
|
+
raci: (full.raci ?? []).map((r) => ({ userId: r.userId, fullName: r.fullName, role: r.role })),
|
|
142
|
+
} : {}),
|
|
143
|
+
labels: (full.labels ?? []).map((l) => l.name),
|
|
144
|
+
// ORB-1698 - counts always; bodies opt-in. checklistProgress is the
|
|
145
|
+
// aggregate from the enriched row (items via include).
|
|
146
|
+
commentCount,
|
|
147
|
+
gitActivityCount: gitCount,
|
|
148
|
+
attachmentCount: attachments.length,
|
|
149
|
+
checklistProgress: full.checklistProgress ?? { done: 0, total: 0 },
|
|
150
|
+
...(includeHint ? { includeHint } : {}),
|
|
151
|
+
...(inc.has('comments') ? {
|
|
152
|
+
comments: comments.map((c) => ({
|
|
153
|
+
id: c.id, // ORB-1285 - needed to target a comment for edit/delete
|
|
154
|
+
author: c.userName ?? null,
|
|
155
|
+
body: c.content,
|
|
156
|
+
createdAt: c.createdAt,
|
|
157
|
+
editedAt: c.editedAt ?? null,
|
|
158
|
+
isInternal: c.isInternal,
|
|
159
|
+
})),
|
|
160
|
+
commentsHasMore: hasMoreComments,
|
|
161
|
+
} : {}),
|
|
162
|
+
...(inc.has('checklistItems') ? {
|
|
163
|
+
checklists: checklists.map((cl) => ({
|
|
164
|
+
title: cl.title,
|
|
165
|
+
triggersDone: cl.triggersDone,
|
|
166
|
+
progress: cl.progress,
|
|
167
|
+
items: cl.items.map((i) => ({
|
|
168
|
+
content: i.content,
|
|
169
|
+
done: i.effectiveCompleted,
|
|
170
|
+
// When the item links to another ticket, `effectiveCompleted`
|
|
171
|
+
// mirrors that ticket's status-category instead of this item's
|
|
172
|
+
// own checkbox. Surface the link so the model can explain
|
|
173
|
+
// why the item is/isn't done.
|
|
174
|
+
linkedTicket: i.linkedTicketKey ? {
|
|
175
|
+
key: i.linkedTicketKey,
|
|
176
|
+
title: i.linkedTicketTitle,
|
|
177
|
+
statusCategory: i.linkedTicketStatusCategory,
|
|
178
|
+
} : null,
|
|
179
|
+
})),
|
|
180
|
+
})),
|
|
181
|
+
} : {}),
|
|
182
|
+
...(inc.has('git') ? {
|
|
183
|
+
gitActivity: gitActivity.map((g) => ({
|
|
184
|
+
type: g.type,
|
|
185
|
+
state: g.state,
|
|
186
|
+
title: g.title,
|
|
187
|
+
url: g.url,
|
|
188
|
+
author: g.authorName,
|
|
189
|
+
createdAt: g.createdAt,
|
|
190
|
+
})),
|
|
191
|
+
} : {}),
|
|
192
|
+
// ORB-1455 - attachments (id + metadata). Feed an id to
|
|
193
|
+
// orboto_get_attachment to view an image or fetch the bytes.
|
|
194
|
+
...(inc.has('attachments') ? {
|
|
195
|
+
attachments: attachments.map((a) => ({
|
|
196
|
+
id: a.id,
|
|
197
|
+
filename: a.filename,
|
|
198
|
+
contentType: a.contentType,
|
|
199
|
+
sizeBytes: a.sizeBytes,
|
|
200
|
+
downloadUrl: a.downloadUrl ?? `/attachments/${a.id}`,
|
|
201
|
+
})),
|
|
202
|
+
} : {}),
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function swallow404(fallback) {
|
|
208
|
+
return (err) => {
|
|
209
|
+
if (err instanceof OrbotoApiError && err.status === 404)
|
|
210
|
+
return fallback;
|
|
211
|
+
throw err;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function formatTicket(ticket, inc, comments, hasMoreComments, checklists, gitActivity, parent, children, attachments, includeHint) {
|
|
215
|
+
const header = [
|
|
216
|
+
`[${ticket.ticketKey}] ${ticket.title}`,
|
|
217
|
+
`Status: ${ticket.statusName ?? ticket.status} Priority: ${ticket.priority} Type: ${ticket.type}`,
|
|
218
|
+
// ORB-1608 - only shown when it deviates from the 'implementation'
|
|
219
|
+
// default, so a fresh ticket's card stays uncluttered.
|
|
220
|
+
ticket.deliveryMode && ticket.deliveryMode !== 'implementation'
|
|
221
|
+
? `Delivery mode: ${ticket.deliveryMode}`
|
|
222
|
+
: null,
|
|
223
|
+
// ORB-1605 - surface the stalled-ingestion signal right in the
|
|
224
|
+
// header so an agent checking "is this really done?" sees it
|
|
225
|
+
// before reading the (currently empty) git-activity section.
|
|
226
|
+
ticket.waitingForGitIngestion
|
|
227
|
+
? '⏳ Waiting for Git ingestion - this project has an active git connection but no commits/PRs have landed for this ticket yet. Closing verification may be blocked on stalled ingestion, not on unfinished work - check manually before assuming it is unlinked.'
|
|
228
|
+
: null,
|
|
229
|
+
// ORB-1023 - show the milestone name (not the UUID) when set.
|
|
230
|
+
ticket.milestoneId ? `Milestone: ${ticket.milestoneName ?? '(unnamed)'}` : null,
|
|
231
|
+
ticket.dueDate ? `Due: ${ticket.dueDate}` : null,
|
|
232
|
+
parent ? `Parent: [${parent.ticketKey}] ${parent.title} (${parent.statusName ?? parent.status})` : null,
|
|
233
|
+
children.length > 0
|
|
234
|
+
? `Sub-tickets: ${children.length} (${children.filter((c) => c.statusCategory !== 'done' && c.statusCategory !== 'wont_fix').length} open)`
|
|
235
|
+
: null,
|
|
236
|
+
ticket.assignees && ticket.assignees.length > 0
|
|
237
|
+
? `Assignees: ${ticket.assignees.map((a) => a.fullName || a.email).join(', ')}`
|
|
238
|
+
: 'Assignees: (unassigned)',
|
|
239
|
+
// ORB-1034 - RACI summary, shown only when the project uses RACI and
|
|
240
|
+
// someone holds a non-Responsible role (Accountable / Consulted / Informed).
|
|
241
|
+
ticket.raci && ticket.raci.some((r) => r.role !== 'R')
|
|
242
|
+
? `RACI: ${['A', 'R', 'C', 'I']
|
|
243
|
+
.map((role) => {
|
|
244
|
+
const people = ticket.raci.filter((r) => r.role === role);
|
|
245
|
+
return people.length ? `${role}: ${people.map((p) => p.fullName || p.email).join(', ')}` : null;
|
|
246
|
+
})
|
|
247
|
+
.filter(Boolean)
|
|
248
|
+
.join(' · ')}`
|
|
249
|
+
: null,
|
|
250
|
+
ticket.labels && ticket.labels.length > 0
|
|
251
|
+
? `Labels: ${ticket.labels.map((l) => l.name).join(', ')}`
|
|
252
|
+
: null,
|
|
253
|
+
].filter((s) => s !== null);
|
|
254
|
+
if (inc.has('children') && children.length > 0) {
|
|
255
|
+
header.push('', 'Children:');
|
|
256
|
+
for (const c of children) {
|
|
257
|
+
header.push(` - [${c.ticketKey}] ${c.title} (${c.statusName ?? c.status})`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const description = ticket.description
|
|
261
|
+
? ['', '## Description', ticket.description]
|
|
262
|
+
: [];
|
|
263
|
+
const checklistLines = [];
|
|
264
|
+
if (!inc.has('checklistItems') && (ticket.checklistProgress?.total ?? 0) > 0) {
|
|
265
|
+
checklistLines.push('', `Checklists: ${ticket.checklistProgress.done}/${ticket.checklistProgress.total} done`);
|
|
266
|
+
}
|
|
267
|
+
if (inc.has('checklistItems') && checklists.length > 0) {
|
|
268
|
+
checklistLines.push('', '## Checklists');
|
|
269
|
+
for (const cl of checklists) {
|
|
270
|
+
const progressLabel = `${cl.progress.done}/${cl.progress.total}`;
|
|
271
|
+
checklistLines.push(`### ${cl.title} (${progressLabel})${cl.triggersDone ? ' · triggers done' : ''}`);
|
|
272
|
+
for (const i of cl.items) {
|
|
273
|
+
// Linked-ticket suffix on items that track another ticket -
|
|
274
|
+
// lets the model say "item X is done because [ACME-42] shipped".
|
|
275
|
+
const link = i.linkedTicketKey
|
|
276
|
+
? ` ↪ [${i.linkedTicketKey}] ${i.linkedTicketTitle ?? ''} (${i.linkedTicketStatusCategory ?? 'unknown'})`
|
|
277
|
+
: '';
|
|
278
|
+
checklistLines.push(`- [${i.effectiveCompleted ? 'x' : ' '}] ${i.content}${link}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const commentLines = [];
|
|
283
|
+
if (!inc.has('comments') && (ticket.commentCount ?? 0) > 0) {
|
|
284
|
+
commentLines.push('', `Comments: ${ticket.commentCount}`);
|
|
285
|
+
}
|
|
286
|
+
if (inc.has('comments') && comments.length > 0) {
|
|
287
|
+
const headerLine = hasMoreComments
|
|
288
|
+
? `## Comments (${comments.length} shown, more in the UI)`
|
|
289
|
+
: `## Comments (${comments.length})`;
|
|
290
|
+
commentLines.push('', headerLine);
|
|
291
|
+
for (const c of comments) {
|
|
292
|
+
commentLines.push(`**${c.userName ?? '(unknown author)'}** - ${c.createdAt}${c.isInternal ? ' [internal]' : ''}`, c.content, '');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const gitLines = [];
|
|
296
|
+
if (!inc.has('git') && (ticket.gitActivityCount ?? 0) > 0) {
|
|
297
|
+
gitLines.push('', `Git activity: ${ticket.gitActivityCount}`);
|
|
298
|
+
}
|
|
299
|
+
if (inc.has('git') && gitActivity.length > 0) {
|
|
300
|
+
gitLines.push('', `## Git activity (${gitActivity.length})`);
|
|
301
|
+
for (const g of gitActivity) {
|
|
302
|
+
gitLines.push(`- ${g.type} ${g.state ? `[${g.state}]` : ''} ${g.title}${g.url ? ` - ${g.url}` : ''}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// ORB-1455 - attachments so the agent knows files exist. Feed an id to
|
|
306
|
+
// orboto_get_attachment to view an image or fetch the bytes.
|
|
307
|
+
const attachmentLines = [];
|
|
308
|
+
if (!inc.has('attachments') && attachments.length > 0) {
|
|
309
|
+
attachmentLines.push('', `Attachments: ${attachments.length}`);
|
|
310
|
+
}
|
|
311
|
+
if (inc.has('attachments') && attachments.length > 0) {
|
|
312
|
+
attachmentLines.push('', `## Attachments (${attachments.length})`);
|
|
313
|
+
for (const a of attachments) {
|
|
314
|
+
const kb = Math.round(a.sizeBytes / 1024);
|
|
315
|
+
attachmentLines.push(`- ${a.filename} (${kb} KB, ${a.contentType}) id=${a.id}`);
|
|
316
|
+
}
|
|
317
|
+
attachmentLines.push('Use orboto_get_attachment with an id to view an image or fetch bytes.');
|
|
318
|
+
}
|
|
319
|
+
const hintLines = includeHint ? ['', includeHint] : [];
|
|
320
|
+
return [...header, ...description, ...checklistLines, ...commentLines, ...gitLines, ...attachmentLines, ...hintLines].join('\n');
|
|
321
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-244 Phase B - `orboto_get_timer`.
|
|
3
|
+
*
|
|
4
|
+
* Returns the caller's active stopwatch (or `null` when none is
|
|
5
|
+
* running). Useful for the model to answer "am I still tracking
|
|
6
|
+
* time on that bug from earlier?" without guessing.
|
|
7
|
+
*
|
|
8
|
+
* API shape: `GET /time/timer` returns `ActiveTimer | null` directly
|
|
9
|
+
* (not wrapped). ActiveTimer carries `ticketTitle` and `projectId`
|
|
10
|
+
* as joined fields but no `ticketKey` - so the ticket identifier is
|
|
11
|
+
* resolved the cheap way: first 8 chars of the UUID when no title
|
|
12
|
+
* is available, just the title otherwise.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
export const getTimerToolConfig = {
|
|
16
|
+
title: 'Get current timer',
|
|
17
|
+
description: 'Return the caller\'s currently-running stopwatch (or null if no timer is active).',
|
|
18
|
+
inputSchema: z.object({}).shape,
|
|
19
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
20
|
+
};
|
|
21
|
+
export function makeGetTimerHandler(client) {
|
|
22
|
+
return async () => {
|
|
23
|
+
// `/time/timer` returns the timer row directly, or `null`.
|
|
24
|
+
const timer = await client.get('/time/timer');
|
|
25
|
+
if (!timer) {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: 'No active timer.' }],
|
|
28
|
+
structuredContent: { timer: null },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const isPaused = timer.pausedAt !== null;
|
|
32
|
+
const elapsedSinceStart = isPaused
|
|
33
|
+
? 0
|
|
34
|
+
: Math.floor((Date.now() - new Date(timer.startedAt).getTime()) / 1000);
|
|
35
|
+
const totalSeconds = timer.accumulatedSeconds + elapsedSinceStart;
|
|
36
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
37
|
+
const ticketLabel = timer.ticketTitle ?? `(ticket ${timer.ticketId.slice(0, 8)})`;
|
|
38
|
+
const text = [
|
|
39
|
+
`Timer ${isPaused ? 'paused' : 'running'} on ${ticketLabel}`,
|
|
40
|
+
`Elapsed: ${minutes} min (${totalSeconds}s total)`,
|
|
41
|
+
`Started: ${timer.startedAt}`,
|
|
42
|
+
isPaused ? `Paused: ${timer.pausedAt}` : null,
|
|
43
|
+
timer.description ? `Note: ${timer.description}` : null,
|
|
44
|
+
].filter((l) => l !== null).join('\n');
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: 'text', text }],
|
|
47
|
+
structuredContent: {
|
|
48
|
+
timer: {
|
|
49
|
+
ticketId: timer.ticketId,
|
|
50
|
+
ticketTitle: timer.ticketTitle ?? null,
|
|
51
|
+
projectId: timer.projectId ?? null,
|
|
52
|
+
description: timer.description,
|
|
53
|
+
startedAt: timer.startedAt,
|
|
54
|
+
pausedAt: timer.pausedAt,
|
|
55
|
+
accumulatedSeconds: timer.accumulatedSeconds,
|
|
56
|
+
totalSeconds,
|
|
57
|
+
paused: isPaused,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-1741 - `orboto_help`: lazy full guidance for one tool.
|
|
3
|
+
*
|
|
4
|
+
* The manifest carries one-sentence summaries (the diet in
|
|
5
|
+
* tool-docs.ts); the complete guidance text - workflows, warnings, edge
|
|
6
|
+
* cases - is served HERE on demand, so a session only pays for the docs
|
|
7
|
+
* of tools it actually reasons about. Same pattern as deferred-tool
|
|
8
|
+
* schemas via ToolSearch.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { getToolDoc, listToolDocNames } from '../tool-docs.js';
|
|
12
|
+
export const helpToolConfig = {
|
|
13
|
+
title: 'Full guidance for one orboto tool',
|
|
14
|
+
description: 'Return the complete guidance text (workflows, warnings, edge cases) for one orboto tool by name - manifest descriptions are one-line summaries, this is the rest. Call it before first use of an unfamiliar write tool.',
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
tool: z.string().min(1).max(128).describe('Tool name, e.g. orboto_create_ticket.'),
|
|
17
|
+
}).shape,
|
|
18
|
+
outputSchema: z.object({
|
|
19
|
+
tool: z.string(),
|
|
20
|
+
guidance: z.string(),
|
|
21
|
+
}).shape,
|
|
22
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
23
|
+
};
|
|
24
|
+
export function makeHelpHandler() {
|
|
25
|
+
return async (args) => {
|
|
26
|
+
const name = args.tool.trim();
|
|
27
|
+
const doc = getToolDoc(name) ?? getToolDoc(`orboto_${name}`);
|
|
28
|
+
if (!doc) {
|
|
29
|
+
const known = listToolDocNames();
|
|
30
|
+
return {
|
|
31
|
+
content: [{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: `No guidance registered for "${name}". Known tools (${known.length}): ${known.join(', ')}`,
|
|
34
|
+
}],
|
|
35
|
+
structuredContent: { tool: name, guidance: '' },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const resolved = getToolDoc(name) ? name : `orboto_${name}`;
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: doc }],
|
|
41
|
+
structuredContent: { tool: resolved, guidance: doc },
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|