@orboto/mcp 0.226.2 → 0.228.1
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/index.js +13 -2
- package/dist/manifest-size.test.js +3 -0
- package/dist/project-scope.js +15 -1
- package/dist/project-scope.test.js +9 -9
- package/dist/response-budget.js +1 -1
- package/dist/server.js +5 -0
- package/dist/session-origin.js +23 -3
- package/dist/tools/agent-coordination.js +101 -36
- package/dist/tools/agent-coordination.test.js +62 -22
- package/dist/tools/agent-instructions.js +5 -4
- package/dist/tools/agent-messages-compact.test.js +3 -3
- package/dist/tools/agent-messages.js +24 -4
- package/dist/tools/agent-presence-row.js +22 -0
- package/dist/tools/agent-who.js +18 -18
- package/dist/tools/agent-who.test.js +9 -2
- package/dist/tools/bulk-writes.js +3 -3
- package/dist/tools/checklist-writes.js +2 -2
- package/dist/tools/claim.js +2 -2
- package/dist/tools/doc-attachments.js +4 -4
- package/dist/tools/doc-comments.js +5 -5
- package/dist/tools/doc-export.js +3 -3
- package/dist/tools/doc-revisions.js +3 -3
- package/dist/tools/docs.js +7 -7
- package/dist/tools/get-timer.js +6 -1
- package/dist/tools/identity.js +10 -1
- package/dist/tools/identity.test.js +11 -0
- package/dist/tools/list-git-app-installations.js +1 -1
- package/dist/tools/phase-b-tools.test.js +7 -0
- package/dist/tools/presence-scope.test.js +170 -0
- package/dist/tools/primer-facts.js +15 -15
- package/dist/tools/project-readiness-schema.js +1 -1
- package/dist/tools/read-budget.test.js +161 -0
- package/dist/tools/responsibilities.test.js +1 -1
- package/dist/tools/rule-proposals.js +131 -0
- package/dist/tools/rule-proposals.test.js +82 -0
- package/dist/tools/semantic-degraded-wrapper.test.js +1 -1
- package/dist/tools/session-start.js +52 -16
- package/dist/tools/session-start.test.js +8 -7
- package/dist/tools/shared.js +49 -0
- package/dist/tools/ticket-writes.js +2 -2
- package/dist/tools/wiki.js +2 -2
- package/dist/tools/work-next.js +5 -5
- package/dist/tools/work-sessions.js +15 -15
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ORB-2502 - orboto_messages lists the newest
|
|
2
|
+
* ORB-2502 - orboto_messages lists the newest 4 unread compactly (10 before ORB-2548) with the exact count; all:true keeps every message in full.
|
|
3
3
|
*/
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { OrbotoClient } from '../orboto-client.js';
|
|
@@ -23,11 +23,11 @@ function stubInbox(urls) {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
describe('ORB-2502 - the compact inbox on MCP', () => {
|
|
26
|
-
it('asks for the newest
|
|
26
|
+
it('asks for the newest 4 compactly, prints the excerpt and the count, and leaves the payload out of the structured answer', async () => {
|
|
27
27
|
const urls = [];
|
|
28
28
|
stubInbox(urls);
|
|
29
29
|
const result = await makeAgentMessagesHandler(client)({ since: '2026-09-30T00:00:00Z' }, { sessionId: 'abc' });
|
|
30
|
-
expect(urls[0]).toContain('limit=
|
|
30
|
+
expect(urls[0]).toContain('limit=4');
|
|
31
31
|
expect(urls[0]).toContain('compact=true');
|
|
32
32
|
expect(urls[0]).toContain('since=2026-09-30T00%3A00%3A00Z');
|
|
33
33
|
const text = result.content[0].text;
|
|
@@ -22,10 +22,12 @@ export function repeatMarks(m) {
|
|
|
22
22
|
].filter(Boolean);
|
|
23
23
|
return marks.length ? ` [${marks.join(', ')}]` : '';
|
|
24
24
|
}
|
|
25
|
+
/** ORB-2548 - the compact page an MCP inbox read returns by default, sized so it fits the 4,000-character budget. */
|
|
26
|
+
export const COMPACT_PAGE_LIMIT = 4;
|
|
25
27
|
const UNREGISTERED_INBOX_HINT = 'Not registered: only account mail is listed. Register with orboto_session_start to see the mail addressed to your session.';
|
|
26
28
|
export const agentMessagesToolConfig = {
|
|
27
29
|
title: 'Agent inbox and message work',
|
|
28
|
-
description: 'Your inbox as THIS session sees it. It holds mail to this session, account mail inside your declared scope, broadcasts; sibling-session mail and own sends stay out (includeOwnSends lists your sends with recipient session, delivered/read state, thread), all:true is the account view in full (ORB-2136). Default: the newest
|
|
30
|
+
description: 'Your inbox as THIS session sees it. It holds mail to this session, account mail inside your declared scope, broadcasts; sibling-session mail and own sends stay out (includeOwnSends lists your sends with recipient session, delivered/read state, thread), all:true is the account view in full (ORB-2136). Default: the newest 4 unread, newest first, a header plus 300 payload characters each, then `N unread, M older than 7 days`; limit and since (ISO time or message id) narrow it. Server-side filters (ORB-2548): direction sent (only your sends, any read state), threadId (that message and its answers), kind; nextCursor continues the page (cursor). Before orboto_session_start only account mail is listed; register to see mail addressed to your session (ORB-2347). messageWork carries durable work ownership (mutation schema: orboto_api_search) - claim before executing. ackIds (message ids) acks: it completes an unclaimed request; claimed work with open steps is refused until messageWork completes or blocks it. dismiss: not_mine hides it here and tells the sender, obsolete/duplicate close it for the account (note or duplicateOf required). Never leave mail you read and judged. Reply via orboto_agent_notify with threadId (toSessionRef = from.sessionId).',
|
|
29
31
|
inputSchema: z.object({
|
|
30
32
|
messageWork: z.record(z.unknown()).optional()
|
|
31
33
|
.describe('Work envelope {messageId, mutation, cursor, limit, openOnly}.'),
|
|
@@ -34,6 +36,10 @@ export const agentMessagesToolConfig = {
|
|
|
34
36
|
since: z.string().optional(),
|
|
35
37
|
project: z.string().min(1).max(64).optional().describe('Key/UUID; includes unscoped mail.'),
|
|
36
38
|
includeOwnSends: z.boolean().default(false),
|
|
39
|
+
direction: z.enum(['received', 'sent']).optional(),
|
|
40
|
+
threadId: z.string().uuid().optional(),
|
|
41
|
+
kind: z.enum(['request', 'error', 'info', 'complete']).optional(),
|
|
42
|
+
cursor: z.string().max(500).optional(),
|
|
37
43
|
ackIds: z.array(z.string().uuid()).max(200).optional(),
|
|
38
44
|
dismiss: z.object({
|
|
39
45
|
ids: z.array(z.string().uuid()).min(1).max(200),
|
|
@@ -66,6 +72,7 @@ export const agentMessagesToolConfig = {
|
|
|
66
72
|
acked: z.number().int(),
|
|
67
73
|
dismissed: z.number().int().optional(),
|
|
68
74
|
closed: z.number().int().optional(),
|
|
75
|
+
nextCursor: z.string().nullable().optional(),
|
|
69
76
|
}).shape,
|
|
70
77
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
71
78
|
};
|
|
@@ -116,28 +123,40 @@ export function makeAgentMessagesHandler(client) {
|
|
|
116
123
|
const q = new URLSearchParams();
|
|
117
124
|
if (args.all)
|
|
118
125
|
q.set('all', 'true');
|
|
119
|
-
q.set('limit', String(args.limit ?? (compact ?
|
|
126
|
+
q.set('limit', String(args.limit ?? (compact ? COMPACT_PAGE_LIMIT : 50)));
|
|
120
127
|
if (compact)
|
|
121
128
|
q.set('compact', 'true');
|
|
122
129
|
if (args.since)
|
|
123
130
|
q.set('since', args.since);
|
|
124
131
|
if (args.project)
|
|
125
132
|
q.set('project', args.project);
|
|
133
|
+
if (args.direction)
|
|
134
|
+
q.set('direction', args.direction);
|
|
135
|
+
if (args.threadId)
|
|
136
|
+
q.set('threadId', args.threadId);
|
|
137
|
+
if (args.kind)
|
|
138
|
+
q.set('kind', args.kind);
|
|
139
|
+
if (args.cursor)
|
|
140
|
+
q.set('cursor', args.cursor);
|
|
126
141
|
const instanceToken = mcpInstanceToken(undefined, extra);
|
|
127
142
|
if (args.includeOwnSends)
|
|
128
143
|
q.set('includeOwn', 'true');
|
|
129
|
-
else
|
|
144
|
+
else if (args.direction !== 'sent')
|
|
130
145
|
q.set('excludeRef', instanceToken);
|
|
131
|
-
const { messages, sessionId, counts } = await client.get(`/v1/agent/messages?${q.toString()}`, { instanceToken });
|
|
146
|
+
const { messages, sessionId, counts, nextCursor } = await client.get(`/v1/agent/messages?${q.toString()}`, { instanceToken });
|
|
132
147
|
const header = (m) => `${m.own ? `[sent] ${sentState(m)} ` : ''}[${m.kind}]${m.projectKey ? ` [${m.projectKey}]` : ''}${repeatMarks(m)} ${m.subject} (from ${m.from?.label ?? m.fromUserId} to ${m.to?.label ?? m.toUserId}, ${m.createdAt}, id ${m.id}${m.threadId ? `, thread ${m.threadId}` : ''})`;
|
|
133
148
|
const lines = messages.length === 0
|
|
134
149
|
? [acked > 0 ? `Acknowledged ${acked} message(s). Inbox empty.` : 'Inbox empty.']
|
|
135
150
|
: messages.flatMap((m) => (compact ? compactLines(m, header(m)) : [`${header(m)}${m.payload ? ` payload: ${JSON.stringify(m.payload)}` : ''}`]));
|
|
151
|
+
if (messages.length === 0 && args.direction === 'sent')
|
|
152
|
+
lines[0] = 'Nothing sent matches.';
|
|
136
153
|
if (messages.length > 0) {
|
|
137
154
|
lines.push(`Acknowledge with ackIds once handled; reply via orboto_agent_notify with threadId (toSessionRef = the sender's instance short id reaches that session only).`);
|
|
138
155
|
}
|
|
139
156
|
if (compact && counts)
|
|
140
157
|
lines.push(`${counts.unread} unread, ${counts.olderThan7Days} older than 7 days (all, limit N, since)`);
|
|
158
|
+
if (nextCursor)
|
|
159
|
+
lines.push(`More messages: call again with cursor "${nextCursor}".`);
|
|
141
160
|
if (args.dismiss) {
|
|
142
161
|
lines.unshift(`Dismissed ${dismissedCount} message(s) as ${args.dismiss.reason}${closedCount > 0 ? `, ${closedCount} closed for the account` : ''}${feedbackSent > 0 ? `, ${feedbackSent} sender(s) told about the misroute` : ''}.`);
|
|
143
162
|
}
|
|
@@ -160,6 +179,7 @@ export function makeAgentMessagesHandler(client) {
|
|
|
160
179
|
acked,
|
|
161
180
|
dismissed: dismissedCount,
|
|
162
181
|
closed: closedCount,
|
|
182
|
+
nextCursor: nextCursor ?? null,
|
|
163
183
|
},
|
|
164
184
|
};
|
|
165
185
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AgentInventoryKindSchema, AgentSessionWakeSchema } from './agent-presence-schema.js';
|
|
3
|
+
import { AgentSessionScopeSchema } from './agent-session-scope.js';
|
|
4
|
+
/** ORB-2548 - one presence row as orboto_agent_presence returns it; the full inventory row is `orboto agents --json` and `GET /v1/agent/who?include=inventory`. */
|
|
5
|
+
export const AgentPresenceRowSchema = z.object({
|
|
6
|
+
sessionId: z.string().uuid(),
|
|
7
|
+
userEmail: z.string(),
|
|
8
|
+
userFullName: z.string().nullable(),
|
|
9
|
+
kind: AgentInventoryKindSchema,
|
|
10
|
+
owner: z.string().nullable(),
|
|
11
|
+
status: z.string(),
|
|
12
|
+
lane: z.string().nullable(),
|
|
13
|
+
scope: AgentSessionScopeSchema.nullable(),
|
|
14
|
+
memberProjects: z.number().int(),
|
|
15
|
+
connections: z.array(z.string()),
|
|
16
|
+
wake: AgentSessionWakeSchema.optional(),
|
|
17
|
+
state: z.string().optional(),
|
|
18
|
+
origin: z.object({ host: z.string().nullable(), repo: z.string().nullable(), pid: z.number().nullable(), harness: z.string().nullable(), client: z.string().nullable() }).nullable(),
|
|
19
|
+
workingOn: z.string().nullable(),
|
|
20
|
+
lastSeenAt: z.string(),
|
|
21
|
+
redacted: z.boolean().optional(),
|
|
22
|
+
});
|
package/dist/tools/agent-who.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
/** ORB-2264 - the live directory: who a sender can address before sending. */
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { AgentSessionRoleSchema } from './agent-session-scope.js';
|
|
4
3
|
import { ownerLine } from './responsibilities.js';
|
|
5
|
-
import { originText } from './agent-coordination.js';
|
|
4
|
+
import { nextPageLine, originText, PresenceFilterShape, presenceQuery, readPresence, unknownProjectText } from './agent-coordination.js';
|
|
6
5
|
import { AGENT_WAKE_LABELS } from './agent-presence-schema.js';
|
|
6
|
+
export function refusalLines(refusals) {
|
|
7
|
+
if (!refusals?.length)
|
|
8
|
+
return [];
|
|
9
|
+
return ['Refused heartbeats in the last 24 hours (no role address, not listed):', ...refusals.map((r) => {
|
|
10
|
+
const where = [r.lastHost, r.lastRepo].filter(Boolean).join('/') || 'unknown origin';
|
|
11
|
+
return `- ${r.email}: ${r.count} (no scope ${r.scopeRequired}, second start ${r.sessionExists}), last ${r.lastAt} from ${where}${r.lastHarness ? ` (${r.lastHarness})` : ''}`;
|
|
12
|
+
})];
|
|
13
|
+
}
|
|
7
14
|
export const agentWhoToolConfig = {
|
|
8
15
|
title: 'Who receives agent mail',
|
|
9
|
-
description: 'Live agent sessions and the role addresses (integrator@KEY) they answer to.
|
|
16
|
+
description: 'Live agent sessions and the role addresses (integrator@KEY) they answer to. GET /v1/agent/who; each row names account, role, declared projects, last seen, wake channel and current ticket; address a role from here, never a session id copied from a log. A session without a declared scope is not listed; the refused heartbeats of the last 24 hours follow per account (refusals). `for` (free text) names the topic owner and keeps its sessions; `state` keeps one state (orphaned: only another process touches it); email, sessionId (id or 8+ character prefix), kind and wake narrow on the server; 4 rows a page, nextCursor continues (cursor); a project that is unknown or not visible to you answers so in one line.',
|
|
10
17
|
inputSchema: z.object({
|
|
11
18
|
project: z.string().min(1).max(64).optional(),
|
|
12
|
-
role: AgentSessionRoleSchema.optional(),
|
|
13
19
|
for: z.string().min(2).max(2000).optional(),
|
|
14
|
-
|
|
20
|
+
...PresenceFilterShape,
|
|
15
21
|
}).shape,
|
|
16
|
-
outputSchema: z.object({ sessions: z.array(z.unknown()), owner: z.unknown().optional() }).shape,
|
|
22
|
+
outputSchema: z.object({ sessions: z.array(z.unknown()), owner: z.unknown().optional(), nextCursor: z.string().nullable(), refusals: z.array(z.unknown()).optional() }).shape,
|
|
17
23
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
18
24
|
};
|
|
19
25
|
/** One directory row as a line: address, account, session, projects, channel, ticket. */
|
|
@@ -28,25 +34,19 @@ export function whoLine(s) {
|
|
|
28
34
|
}
|
|
29
35
|
export function makeAgentWhoHandler(client) {
|
|
30
36
|
return async (args = {}) => {
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
if (args.project)
|
|
35
|
-
qs.set('project', args.project);
|
|
36
|
-
if (args.role)
|
|
37
|
-
qs.set('role', args.role);
|
|
38
|
-
if (args.for)
|
|
39
|
-
qs.set('for', args.for);
|
|
40
|
-
const res = await client.get(`/v1/agent/who${qs.size ? `?${qs.toString()}` : ''}`);
|
|
37
|
+
const res = await readPresence(client, `/v1/agent/who?${presenceQuery(args)}`, args.project);
|
|
38
|
+
if (!res)
|
|
39
|
+
return { content: [{ type: 'text', text: unknownProjectText(args.project) }], structuredContent: { sessions: [], nextCursor: null } };
|
|
41
40
|
const ownerLines = args.for
|
|
42
41
|
? [res.owner ? `Owner:\n${ownerLine(res.owner)}` : `No entry in the responsibility map matches "${args.for}".`]
|
|
43
42
|
: [];
|
|
44
43
|
const lines = res.sessions.length
|
|
45
|
-
? [...ownerLines, `${res.sessions.length} live session(s):`, ...res.sessions.map(whoLine)]
|
|
44
|
+
? [...ownerLines, `${res.sessions.length} live session(s)${res.nextCursor ? ' on this page' : ''}:`, ...res.sessions.map(whoLine), ...nextPageLine(res.nextCursor)]
|
|
46
45
|
: [...ownerLines, res.owner ? `No live session answers to ${res.owner.entry.ownerAddress} - send with queue: true and project ${res.owner.entry.ownerAddress.split('@')[1]} to keep it until one declares it.` : 'No live session matches - a request sent now answers no_recipient unless it is queued.'];
|
|
46
|
+
lines.push(...refusalLines(res.refusals));
|
|
47
47
|
return {
|
|
48
48
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
49
|
-
structuredContent: { sessions: res.sessions, ...(args.for ? { owner: (res.owner ?? null) } : {}) },
|
|
49
|
+
structuredContent: { sessions: res.sessions, ...(args.for ? { owner: (res.owner ?? null) } : {}), nextCursor: res.nextCursor ?? null, ...(res.refusals?.length ? { refusals: res.refusals } : {}) },
|
|
50
50
|
};
|
|
51
51
|
};
|
|
52
52
|
}
|
|
@@ -17,10 +17,17 @@ describe('orboto_who', () => {
|
|
|
17
17
|
return { ok: true, status: 200, statusText: 'OK', text: async () => '', json: async () => ({ sessions: [entry] }) };
|
|
18
18
|
});
|
|
19
19
|
const result = await makeAgentWhoHandler(client)({ project: 'ORB', role: 'integrator' });
|
|
20
|
-
expect(urls[0]).toBe('https://orboto.example.com/v1/agent/who?project=ORB&role=integrator');
|
|
20
|
+
expect(urls[0]).toBe('https://orboto.example.com/v1/agent/who?project=ORB&role=integrator&limit=4');
|
|
21
21
|
expect(result.content[0].text).toContain('integrator@ORB - claude@orboto.io session f072e1c5, working, claude-code online');
|
|
22
22
|
expect(result.structuredContent).toMatchObject({ sessions: [expect.objectContaining({ shortId: 'f072e1c5' })] });
|
|
23
23
|
});
|
|
24
|
+
it('ORB-2548: a project that is unknown or not visible answers in one line instead of a raw 404', async () => {
|
|
25
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ({ ok: false, status: 404, statusText: 'Not Found', headers: new Headers(), text: async () => '{"errorKey":"errors.agents.message_project_not_found"}', json: async () => ({}) }));
|
|
26
|
+
const result = await makeAgentWhoHandler(client)({ project: 'gone' });
|
|
27
|
+
expect(result.isError).toBeUndefined();
|
|
28
|
+
expect(result.content[0].text).toBe('Project "GONE" is unknown or not visible to you - no session is listed for it.');
|
|
29
|
+
expect(result.structuredContent).toEqual({ sessions: [], nextCursor: null });
|
|
30
|
+
});
|
|
24
31
|
it('says a request would be refused when nobody matches, and is read-only', async () => {
|
|
25
32
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ({ ok: true, status: 200, statusText: 'OK', text: async () => '', json: async () => ({ sessions: [] }) }));
|
|
26
33
|
const result = await makeAgentWhoHandler(client)({});
|
|
@@ -41,6 +48,6 @@ describe('orboto_who', () => {
|
|
|
41
48
|
return { ok: true, status: 200, statusText: 'OK', text: async () => '', json: async () => ({ sessions: [] }) };
|
|
42
49
|
});
|
|
43
50
|
await makeAgentWhoHandler(client)({ state: 'unknown_origin' });
|
|
44
|
-
expect(urls[0]).toBe('https://orboto.example.com/v1/agent/who?state=unknown_origin');
|
|
51
|
+
expect(urls[0]).toBe('https://orboto.example.com/v1/agent/who?state=unknown_origin&limit=4');
|
|
45
52
|
});
|
|
46
53
|
});
|
|
@@ -15,12 +15,12 @@ const CATEGORY_TO_LEGACY = {
|
|
|
15
15
|
wont_fix: 'WONT_FIX',
|
|
16
16
|
};
|
|
17
17
|
const TICKET_KEY_ARRAY = z.array(z.string().min(3)).min(1).max(200).optional()
|
|
18
|
-
.describe('Ticket keys,
|
|
18
|
+
.describe('Ticket keys, at most 200; or give query.');
|
|
19
19
|
/** ORB-2054 - the target set can be an OQL query instead of a key list. */
|
|
20
20
|
const QUERY_SOURCE = z.string().min(1).max(8000).optional()
|
|
21
|
-
.describe('OQL
|
|
21
|
+
.describe('OQL instead of ticketKeys (your ACL), at most 200 matches.');
|
|
22
22
|
const QUERY_PROJECT = z.string().optional()
|
|
23
|
-
.describe('Project key
|
|
23
|
+
.describe('Project key scoping `query`.');
|
|
24
24
|
/** Matches per call the MCP loop will process; above that the server-side bulk route is the right tool. */
|
|
25
25
|
export const BULK_QUERY_MAX = 200;
|
|
26
26
|
/** Resolve `query` to ticket keys through POST /query, paging until the cap. */
|
|
@@ -74,7 +74,7 @@ function makeToggleHandler(client, completed) {
|
|
|
74
74
|
const toggleInputSchema = z.object({
|
|
75
75
|
ticketKey: z.string().min(3),
|
|
76
76
|
item: z.union([
|
|
77
|
-
z.number().int().min(1).describe('1-based index
|
|
77
|
+
z.number().int().min(1).describe('1-based index across all checklists'),
|
|
78
78
|
z.string().describe('Item UUID - for callers that already have it'),
|
|
79
79
|
]),
|
|
80
80
|
}).shape;
|
|
@@ -169,7 +169,7 @@ export const updateCheckToolConfig = {
|
|
|
169
169
|
inputSchema: z.object({
|
|
170
170
|
ticketKey: z.string().min(3),
|
|
171
171
|
item: z.union([
|
|
172
|
-
z.number().int().min(1).describe('1-based index
|
|
172
|
+
z.number().int().min(1).describe('1-based index across all checklists'),
|
|
173
173
|
z.string().describe('Item UUID - for callers that already have it'),
|
|
174
174
|
]),
|
|
175
175
|
content: z.string().min(1).max(2000).optional().describe('New item text.'),
|
package/dist/tools/claim.js
CHANGED
|
@@ -16,7 +16,7 @@ const CATEGORY_TO_LEGACY = {
|
|
|
16
16
|
};
|
|
17
17
|
export const claimToolConfig = {
|
|
18
18
|
title: 'Claim a ticket (assign self + in_progress + timer)',
|
|
19
|
-
description: 'Assign yourself, move to in_progress and start a timer. Repeated claims recheck the specification gate. Worker bots need ready, or override=true with a reason; enabled epics are blocked. sole removes other assignees after the gate passes. force permits reopening done tickets. noTimer skips time tracking. On human accounts a different active timer is stopped first and its elapsed time booked to that ticket. Bot timers are per instance and never auto-stop each other: agentSessionToken defaults to this MCP connection; own both start and stop.',
|
|
19
|
+
description: 'Assign yourself, move to in_progress and start a timer. Repeated claims recheck the specification gate. Worker bots need ready, or override=true with a reason; enabled epics are blocked. sole removes other assignees after the gate passes. force permits reopening done tickets. noTimer skips time tracking. On human accounts a different active timer is stopped first and its elapsed time booked to that ticket. Bot timers are per instance and never auto-stop each other: agentSessionToken defaults to this MCP connection; own both start and stop. The server attributes the timer to the session of this connection only (ORB-2550): a token that is not its own is refused 403, a session another harness process owns 409.',
|
|
20
20
|
inputSchema: z.object({
|
|
21
21
|
ticketKey: z.string().min(3),
|
|
22
22
|
override: z.boolean().optional().describe("Spec override; reason required."),
|
|
@@ -24,7 +24,7 @@ export const claimToolConfig = {
|
|
|
24
24
|
sole: z.boolean().optional().describe('Remove other assignees after gate.'),
|
|
25
25
|
force: z.boolean().optional().describe('Reopen done.'),
|
|
26
26
|
noTimer: z.boolean().optional().describe('No timer.'),
|
|
27
|
-
agentSessionToken: z.string().optional().describe('Instance token for bot timers.'),
|
|
27
|
+
agentSessionToken: z.string().optional().describe('Instance token for bot timers; must be this connection\'s own.'),
|
|
28
28
|
}).shape,
|
|
29
29
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
30
30
|
};
|
|
@@ -39,11 +39,11 @@ export const uploadDocAttachmentToolConfig = {
|
|
|
39
39
|
title: 'Upload an attachment to a doc page',
|
|
40
40
|
description: 'Upload a file as a doc attachment: returns its Markdown image (or link) line + a 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.',
|
|
41
41
|
inputSchema: z.object({
|
|
42
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
42
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
43
43
|
filename: z.string().min(1).describe('Display filename, e.g. "architecture-diagram.png".'),
|
|
44
44
|
contentBase64: z.string().min(1).describe('File content, base64-encoded.'),
|
|
45
45
|
altText: z.string().optional().describe('Alt-text for the Markdown image line. Defaults to filename.'),
|
|
46
|
-
embed: z.boolean().optional().describe('
|
|
46
|
+
embed: z.boolean().optional().describe('Append the Markdown line to the doc body; a re-run appends another copy.'),
|
|
47
47
|
}).shape,
|
|
48
48
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
49
49
|
};
|
|
@@ -100,7 +100,7 @@ export const listDocAttachmentsToolConfig = {
|
|
|
100
100
|
title: 'List attachments on a doc page',
|
|
101
101
|
description: 'Return the doc\'s attachments newest-first, with stable download URLs the agent can hand to the user. Empty list = no attachments.',
|
|
102
102
|
inputSchema: z.object({
|
|
103
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
103
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
104
104
|
}).shape,
|
|
105
105
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
106
106
|
};
|
|
@@ -139,7 +139,7 @@ export const deleteDocAttachmentToolConfig = {
|
|
|
139
139
|
title: 'Delete an attachment from a doc page',
|
|
140
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.',
|
|
141
141
|
inputSchema: z.object({
|
|
142
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
142
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
143
143
|
attachmentId: z.string().uuid(),
|
|
144
144
|
}).shape,
|
|
145
145
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
@@ -7,7 +7,7 @@ export const listDocCommentsToolConfig = {
|
|
|
7
7
|
title: 'List comments on a doc page',
|
|
8
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.',
|
|
9
9
|
inputSchema: z.object({
|
|
10
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
10
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
11
11
|
limit: z.number().int().min(1).max(100).optional(),
|
|
12
12
|
cursor: z.string().optional(),
|
|
13
13
|
}).shape,
|
|
@@ -84,7 +84,7 @@ export const postDocCommentToolConfig = {
|
|
|
84
84
|
title: 'Post a comment on a doc page (or reply to one)',
|
|
85
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. Mention a person with the markup `@[Full Name](user:<uuid>)` - the UUIDs come from `orboto_list_users`. The response reports `mentions` (tokens that resolved to a user, who is then notified) and `unresolvedMentions` (tokens that named nobody); a mention is never silently dropped.',
|
|
86
86
|
inputSchema: z.object({
|
|
87
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
87
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
88
88
|
content: z.string().min(1).max(4000),
|
|
89
89
|
parentCommentId: z.string().uuid().optional().describe('Reply to an existing comment.'),
|
|
90
90
|
anchor: z.object({
|
|
@@ -136,7 +136,7 @@ export const resolveDocCommentToolConfig = {
|
|
|
136
136
|
title: 'Mark a doc comment thread as resolved (or reopen it)',
|
|
137
137
|
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.',
|
|
138
138
|
inputSchema: z.object({
|
|
139
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
139
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
140
140
|
commentId: z.string().uuid(),
|
|
141
141
|
resolved: z.boolean(),
|
|
142
142
|
}).shape,
|
|
@@ -163,7 +163,7 @@ export const updateDocCommentToolConfig = {
|
|
|
163
163
|
title: 'Edit your own doc comment',
|
|
164
164
|
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.',
|
|
165
165
|
inputSchema: z.object({
|
|
166
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
166
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
167
167
|
commentId: z.string().uuid(),
|
|
168
168
|
content: z.string().min(1).max(4000),
|
|
169
169
|
}).shape,
|
|
@@ -187,7 +187,7 @@ export const deleteDocCommentToolConfig = {
|
|
|
187
187
|
title: 'Delete a doc comment',
|
|
188
188
|
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.',
|
|
189
189
|
inputSchema: z.object({
|
|
190
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
190
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
191
191
|
commentId: z.string().uuid(),
|
|
192
192
|
}).shape,
|
|
193
193
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
package/dist/tools/doc-export.js
CHANGED
|
@@ -7,7 +7,7 @@ export const exportDocMdToolConfig = {
|
|
|
7
7
|
title: 'Export a doc page as Markdown',
|
|
8
8
|
description: 'Return the doc body as plain Markdown (text/markdown). Differs from orboto_get_doc.content in that the server-side export endpoint emits the canonical saved body - agents pulling docs to feed into another tool should prefer this over get_doc, which wraps the body in get-doc envelope text + backlinks.',
|
|
9
9
|
inputSchema: z.object({
|
|
10
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
10
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
11
11
|
}).shape,
|
|
12
12
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
13
13
|
};
|
|
@@ -27,9 +27,9 @@ export function makeExportDocMdHandler(client) {
|
|
|
27
27
|
}
|
|
28
28
|
export const exportDocPdfToolConfig = {
|
|
29
29
|
title: 'Export a doc page as PDF',
|
|
30
|
-
description: 'Render the doc page to PDF
|
|
30
|
+
description: 'Render the doc page to PDF and return it as a base64 MCP resource attachment. Requires the PDF engine to be configured - deployments without Chromium / Puppeteer return 503 (errors.pdf.engine_unavailable) which surfaces as an OrbotoApiError.',
|
|
31
31
|
inputSchema: z.object({
|
|
32
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
32
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
33
33
|
}).shape,
|
|
34
34
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
35
35
|
};
|
|
@@ -7,7 +7,7 @@ export const listDocRevisionsToolConfig = {
|
|
|
7
7
|
title: 'List the revision history of a doc page',
|
|
8
8
|
description: 'Return saved revisions for a doc page, newest-first. Each row carries the revision id, title at snapshot time, editor user id, and editedAt timestamp; the body content is NOT in this list (use orboto_get_doc_revision to fetch one). Cursor-paged - pass `cursor` to walk older pages.',
|
|
9
9
|
inputSchema: z.object({
|
|
10
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
10
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
11
11
|
limit: z.number().int().min(1).max(100).optional().describe('Page size. Defaults to API default (25).'),
|
|
12
12
|
cursor: z.string().optional().describe('Opaque cursor from a previous call\'s nextCursor.'),
|
|
13
13
|
}).shape,
|
|
@@ -56,7 +56,7 @@ export const getDocRevisionToolConfig = {
|
|
|
56
56
|
title: 'Get a single doc revision',
|
|
57
57
|
description: 'Return the full content + title of one saved revision. Use this to diff against the current doc before deciding whether to restore.',
|
|
58
58
|
inputSchema: z.object({
|
|
59
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
59
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
60
60
|
revisionId: z.string().uuid(),
|
|
61
61
|
}).shape,
|
|
62
62
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
@@ -85,7 +85,7 @@ export const restoreDocRevisionToolConfig = {
|
|
|
85
85
|
title: 'Restore a doc page to a saved revision',
|
|
86
86
|
description: 'Roll back the doc to the saved revision. The API snapshots the CURRENT body to a fresh revision row first, so the restore itself is also undoable. Returns the post-restore doc state.',
|
|
87
87
|
inputSchema: z.object({
|
|
88
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
88
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
89
89
|
revisionId: z.string().uuid(),
|
|
90
90
|
}).shape,
|
|
91
91
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
package/dist/tools/docs.js
CHANGED
|
@@ -152,7 +152,7 @@ export const updateDocSpaceToolConfig = {
|
|
|
152
152
|
description: z.string().nullish(),
|
|
153
153
|
icon: z.string().nullish(),
|
|
154
154
|
isPublic: z.boolean().optional().describe('DEPRECATED - use guestsVisible.'),
|
|
155
|
-
visibility: z.enum(['workspace', 'project_members', 'restricted']).optional().describe('
|
|
155
|
+
visibility: z.enum(['workspace', 'project_members', 'restricted']).optional().describe('Read gate: workspace = every internal member, project_members = members with doc:view (project spaces), restricted = allowlist. Needs manage rights.'),
|
|
156
156
|
guestsVisible: z.boolean().optional().describe('Explicit guest (external user) opt-in. Requires manage rights.'),
|
|
157
157
|
memberIds: z.array(z.string().uuid()).optional().describe('Replaces the restricted-space allowlist. Requires manage rights.'),
|
|
158
158
|
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional(),
|
|
@@ -276,7 +276,7 @@ export const createDocToolConfig = {
|
|
|
276
276
|
inputSchema: z.object({
|
|
277
277
|
spaceId: z.string().min(1).describe('Target doc space - key (e.g. ORB-S1), name, or UUID. Discover via orboto_list_doc_spaces.'),
|
|
278
278
|
title: z.string().min(1).max(255),
|
|
279
|
-
content: z.string().optional().describe('Markdown
|
|
279
|
+
content: z.string().optional().describe('Markdown (GFM); omitted: a blank page. Pills: `:badge[LABEL]{tone=success}` (neutral/info/success/warning/danger/accent, unknown = neutral, label <= 24 chars).'),
|
|
280
280
|
parentDocId: z.string().uuid().nullable().optional().describe('Nest under another doc.'),
|
|
281
281
|
visibility: z.enum(['public', 'workspace', 'members', 'specific']).optional().describe('Defaults to "workspace".'),
|
|
282
282
|
icon: z.string().nullish().describe('Single emoji shown in the tree.'),
|
|
@@ -313,9 +313,9 @@ export const updateDocToolConfig = {
|
|
|
313
313
|
title: 'Update a doc page',
|
|
314
314
|
description: 'Patch a doc; content is appended unless replace. ORB-2468: title, visibility, parent and icon are set as given; content goes in as a new section (identical content twice is a no-op); replace: true replaces the whole page and the answer names the revision holding the previous body plus its restore command. Every body or title change snapshots a revision; orboto_restore_doc_revision rolls back. For one passage or section use orboto_edit_doc.',
|
|
315
315
|
inputSchema: z.object({
|
|
316
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
316
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
317
317
|
title: z.string().min(1).max(255).optional(),
|
|
318
|
-
content: z.string().optional().describe('Markdown to append (with replace: the whole body).
|
|
318
|
+
content: z.string().optional().describe('Markdown to append (with replace: the whole body). Pills: `:badge[LABEL]{tone=success}` (neutral/info/success/warning/danger/accent, unknown = neutral, label <= 24 chars).'),
|
|
319
319
|
replace: z.boolean().optional(),
|
|
320
320
|
parentDocId: z.string().uuid().nullable().optional(),
|
|
321
321
|
visibility: z.enum(['public', 'workspace', 'members', 'specific']).optional(),
|
|
@@ -368,7 +368,7 @@ export const deleteDocToolConfig = {
|
|
|
368
368
|
title: 'Delete a doc page',
|
|
369
369
|
description: 'DESTRUCTIVE - removes the page from the tree. Revisions are NOT auto-cascaded; the API keeps them so an admin could in principle resurrect from history, but the page itself is gone from listings. System-generated primer docs refuse deletion (the regenerator re-creates them). Returns success silently; 404 is also silent (idempotent).',
|
|
370
370
|
inputSchema: z.object({
|
|
371
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
371
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
372
372
|
}).shape,
|
|
373
373
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
374
374
|
};
|
|
@@ -386,9 +386,9 @@ export const moveDocToolConfig = {
|
|
|
386
386
|
title: 'Move a doc page (reparent / reorder / cross-space)',
|
|
387
387
|
description: 'Reparent a doc, change its sort order, or move it into a different space. All three target fields are optional but at least one must be supplied. Distinct from orboto_update_doc\'s parentDocId because the API endpoint emits a different WS event for the tree-rebuild path connected clients care about.',
|
|
388
388
|
inputSchema: z.object({
|
|
389
|
-
docId: z.string().min(1).describe('Doc UUID or
|
|
389
|
+
docId: z.string().min(1).describe('Doc UUID or key (ORB-D12, DOC-5).'),
|
|
390
390
|
parentDocId: z.string().uuid().nullable().optional().describe('null = top-level; UUID = nest under that doc.'),
|
|
391
|
-
spaceId: z.string().uuid().optional().describe('
|
|
391
|
+
spaceId: z.string().uuid().optional().describe('Target space (member of both); the parent must be in it, else the doc lands at the top level.'),
|
|
392
392
|
sortOrder: z.number().int().optional(),
|
|
393
393
|
}).shape,
|
|
394
394
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
package/dist/tools/get-timer.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
export const getTimerToolConfig = {
|
|
6
6
|
title: 'Get current timer',
|
|
7
|
-
description: 'Return the caller\'s currently-running stopwatch (or null if no timer is active). The lane is resolved from this session\'s instance token, so a timer started by `orboto_claim` / `orboto_timer_start` in the same checkout reads back (ORB-2137); `laneFallback: true` means the timer was matched as the account\'s only running one because this instance\'s lane was empty - another instance started it.',
|
|
7
|
+
description: 'Return the caller\'s currently-running stopwatch (or null if no timer is active). The lane is resolved from this session\'s instance token, so a timer started by `orboto_claim` / `orboto_timer_start` in the same checkout reads back (ORB-2137); `laneFallback: true` means the timer was matched as the account\'s only running one because this instance\'s lane was empty - another instance started it. The answer names who runs the timer and its agent session (ORB-2550); the fallback never takes the timer of a session another harness process owns (409 `errors.time.timer_of_other_session` on stop).',
|
|
8
8
|
inputSchema: z.object({}).shape,
|
|
9
9
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
10
10
|
};
|
|
@@ -28,6 +28,7 @@ export function makeGetTimerHandler(client) {
|
|
|
28
28
|
`Timer ${isPaused ? 'paused' : 'running'} on ${ticketLabel}`,
|
|
29
29
|
`Elapsed: ${minutes} min (${totalSeconds}s total)`,
|
|
30
30
|
`Started: ${timer.startedAt}`,
|
|
31
|
+
`Owner: ${timer.userFullName ?? timer.userEmail ?? timer.userId}${timer.agentSessionLabel ? `, session ${timer.agentSessionLabel}` : ', no agent session'}`,
|
|
31
32
|
isPaused ? `Paused: ${timer.pausedAt}` : null,
|
|
32
33
|
timer.laneFallback ? 'Matched via the single-active-timer fallback - another instance started it.' : null,
|
|
33
34
|
timer.description ? `Note: ${timer.description}` : null,
|
|
@@ -46,6 +47,10 @@ export function makeGetTimerHandler(client) {
|
|
|
46
47
|
totalSeconds,
|
|
47
48
|
paused: isPaused,
|
|
48
49
|
laneFallback: timer.laneFallback === true,
|
|
50
|
+
userId: timer.userId,
|
|
51
|
+
userEmail: timer.userEmail ?? null,
|
|
52
|
+
agentSessionId: timer.agentSessionId ?? null,
|
|
53
|
+
agentSessionLabel: timer.agentSessionLabel ?? null,
|
|
49
54
|
},
|
|
50
55
|
},
|
|
51
56
|
};
|
package/dist/tools/identity.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
export const whoamiToolConfig = {
|
|
6
6
|
title: 'Show the authenticated MCP principal',
|
|
7
|
-
description: 'Return the authenticated user record `{id, email, fullName, isBot, workspaceLocale}` for the API key this server runs with. Useful for debugging which credential / bot identity is in use when multiple MCP configs are wired to different `orb_*` keys. **`workspaceLocale` (ORB-989)** is the language the workspace expects tickets / comments / docs in - check it before a mass-create so you write in the right language from the start instead of relying on the after-the-fact language-mismatch warning.',
|
|
7
|
+
description: 'Return the authenticated user record `{id, email, fullName, isBot, workspaceLocale}` for the API key this server runs with. Useful for debugging which credential / bot identity is in use when multiple MCP configs are wired to different `orb_*` keys. **`workspaceLocale` (ORB-989)** is the language the workspace expects tickets / comments / docs in - check it before a mass-create so you write in the right language from the start instead of relying on the after-the-fact language-mismatch warning. **`agentSession` (ORB-2550)** is the agent session every write of this connection is attributed to, null when the server knows none for its instance token; `foreign: true` means another harness process owns it, so writes carry no session and timers and work sessions are refused until this process has an instance token (`ORBOTO_AGENT_SESSION`) or a profile of its own.',
|
|
8
8
|
inputSchema: z.object({}).shape,
|
|
9
9
|
outputSchema: z.object({
|
|
10
10
|
id: z.string(),
|
|
@@ -13,6 +13,7 @@ export const whoamiToolConfig = {
|
|
|
13
13
|
isBot: z.boolean(),
|
|
14
14
|
workspaceLocale: z.string().nullable(),
|
|
15
15
|
actingAuthorizedBy: z.object({ id: z.string(), email: z.string(), fullName: z.string() }).nullable(),
|
|
16
|
+
agentSession: z.object({ id: z.string(), label: z.string(), harness: z.string().nullable(), pid: z.number().nullable(), foreign: z.boolean() }).nullable(),
|
|
16
17
|
}).shape,
|
|
17
18
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
18
19
|
};
|
|
@@ -32,6 +33,13 @@ export function makeWhoamiHandler(client) {
|
|
|
32
33
|
if (me.actingAuthorizedBy) {
|
|
33
34
|
lines.push(` acting as this bot, authorised by: ${me.actingAuthorizedBy.fullName} <${me.actingAuthorizedBy.email}>`);
|
|
34
35
|
}
|
|
36
|
+
const session = me.agentSession ?? null;
|
|
37
|
+
if (session) {
|
|
38
|
+
lines.push(` session: ${session.id} (${session.label})${session.foreign ? ` - owned by ${session.harness ?? 'unknown'} pid ${session.pid ?? 0}, another process: writes carry no session, timers and work sessions are refused; give this process an instance token of its own (ORBOTO_AGENT_SESSION=<own value>) or its own identity (orboto profile bind <harness> <profile>); after a restart of the same harness, orboto session-start registers the row again` : ''}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
lines.push(' session: none - the server knows no session for this connection\'s instance token yet');
|
|
42
|
+
}
|
|
35
43
|
return {
|
|
36
44
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
37
45
|
structuredContent: {
|
|
@@ -41,6 +49,7 @@ export function makeWhoamiHandler(client) {
|
|
|
41
49
|
isBot,
|
|
42
50
|
workspaceLocale,
|
|
43
51
|
actingAuthorizedBy: me.actingAuthorizedBy ?? null,
|
|
52
|
+
agentSession: session,
|
|
44
53
|
},
|
|
45
54
|
};
|
|
46
55
|
};
|
|
@@ -39,9 +39,20 @@ describe('orboto_whoami', () => {
|
|
|
39
39
|
isBot: true,
|
|
40
40
|
workspaceLocale: 'en',
|
|
41
41
|
actingAuthorizedBy: null,
|
|
42
|
+
agentSession: null,
|
|
42
43
|
});
|
|
43
44
|
expect(res.content[0].text).toContain('agent-e@orboto.io');
|
|
44
45
|
});
|
|
46
|
+
it('ORB-2550 - names the session the connection writes under, and a session another live process owns', async () => {
|
|
47
|
+
const session = { id: '0b0c3a1e-7f73-4c55-8a4e-1d0f0f0f0f0f', label: 'mcp-stdio#0b0c3a1e', harness: 'claude-code', pid: 4242, foreign: false };
|
|
48
|
+
stub([{ json: { id: 'u5', email: 'agent@orboto.io', isBot: true, agentSession: session } }, { json: { id: 'u5', email: 'agent@orboto.io', isBot: true, agentSession: { ...session, foreign: true } } }]);
|
|
49
|
+
const own = await makeWhoamiHandler(client)();
|
|
50
|
+
expect(own.structuredContent).toMatchObject({ agentSession: session });
|
|
51
|
+
expect(own.content[0].text).toContain(`session: ${session.id} (mcp-stdio#0b0c3a1e)`);
|
|
52
|
+
const foreign = await makeWhoamiHandler(client)();
|
|
53
|
+
expect(foreign.content[0].text).toContain('owned by claude-code pid 4242, another process: writes carry no session, timers and work sessions are refused; give this process an instance token of its own (ORBOTO_AGENT_SESSION=<own value>)');
|
|
54
|
+
expect(foreign.content[0].text).toContain('after a restart of the same harness, orboto session-start registers the row again');
|
|
55
|
+
});
|
|
45
56
|
it('surfaces workspaceLocale in the text output when present (ORB-989)', async () => {
|
|
46
57
|
stub([{ json: { id: 'u4', email: 'agent@orboto.io', fullName: 'Agent', isBot: true, workspaceLocale: 'de' } }]);
|
|
47
58
|
const res = await makeWhoamiHandler(client)();
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
export const listGitAppInstallationsToolConfig = {
|
|
13
13
|
title: 'List GitHub App installations',
|
|
14
|
-
description: 'Every GitHub App installation
|
|
14
|
+
description: 'Every known GitHub App installation (admin:system:read): account, install time, suspension, allowed project keys.',
|
|
15
15
|
inputSchema: z.object({}).shape,
|
|
16
16
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
17
17
|
};
|
|
@@ -611,4 +611,11 @@ describe('orboto_get_timer', () => {
|
|
|
611
611
|
expect(sc.timer.paused).toBe(false);
|
|
612
612
|
expect(sc.timer.ticketTitle).toBe('Bug');
|
|
613
613
|
});
|
|
614
|
+
it('ORB-2550 - names who runs the timer and in which agent session', async () => {
|
|
615
|
+
const startedAt = new Date().toISOString();
|
|
616
|
+
stub([{ json: { id: 'tm2', userId: 'u1', ticketId: 't1', startedAt, pausedAt: null, accumulatedSeconds: 0, description: null, userEmail: 'claude@orboto.io', userFullName: 'Claude Agent', agentSessionId: 's-1', agentSessionLabel: 'claude-code#0b0c3a1e' } }]);
|
|
617
|
+
const res = await makeGetTimerHandler(client)();
|
|
618
|
+
expect(res.content[0].text).toContain('Owner: Claude Agent, session claude-code#0b0c3a1e');
|
|
619
|
+
expect(res.structuredContent).toMatchObject({ timer: { userId: 'u1', userEmail: 'claude@orboto.io', agentSessionId: 's-1', agentSessionLabel: 'claude-code#0b0c3a1e' } });
|
|
620
|
+
});
|
|
614
621
|
});
|