@orboto/mcp 0.227.0 → 0.229.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +13 -4
- package/dist/tools/agent-instructions.js +5 -4
- package/dist/tools/agent-who.js +12 -3
- 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/knowledge.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 +10 -10
- package/dist/tools/project-readiness-schema.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 +7 -6
- package/dist/tools/shared.js +49 -0
- package/dist/tools/ticket-writes.js +2 -2
- package/dist/tools/wiki.js +14 -6
- package/dist/tools/work-next.js +5 -5
- package/dist/tools/work-sessions.js +15 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -70,16 +70,27 @@ async function main() {
|
|
|
70
70
|
const { mcpProcessInstance } = await import('./tools/shared.js');
|
|
71
71
|
const instanceToken = mcpProcessInstance();
|
|
72
72
|
const { declareEnvProjectScope } = await import('./project-scope.js');
|
|
73
|
-
await declareEnvProjectScope(preflightClient, instanceToken);
|
|
73
|
+
const declared = await declareEnvProjectScope(preflightClient, instanceToken);
|
|
74
74
|
const inbox = new InboxChannel({ ...clientConfig, instanceToken, mcp: server, digestMinutes: digestMinutesFromEnv(process.env.ORBOTO_MCP_CHANNEL_DIGEST_MINUTES) });
|
|
75
75
|
inbox.start();
|
|
76
|
+
if (declared?.refusal)
|
|
77
|
+
void inbox.deliverNotice('session_exists', declared.refusal);
|
|
76
78
|
const outdated = cliOutdatedNotice(process.env.ORBOTO_MCP_CLI_OUTDATED);
|
|
77
79
|
if (outdated)
|
|
78
80
|
void inbox.deliverNotice(CLI_OUTDATED_NOTICE, outdated);
|
|
79
81
|
const { readClaudeContext, channelClientInfo } = await import('./claude-context.js');
|
|
82
|
+
const { presenceRefusal } = await import('./tools/shared.js');
|
|
83
|
+
let refusalLogged = false;
|
|
80
84
|
const beat = () => {
|
|
81
85
|
preflightClient.post('/v1/agent/heartbeat', { clientInfo: channelClientInfo(readClaudeContext(process.cwd()), undefined, channelStart ?? undefined) }, { instanceToken })
|
|
82
|
-
.
|
|
86
|
+
.then(() => { refusalLogged = false; })
|
|
87
|
+
.catch((err) => {
|
|
88
|
+
const refused = presenceRefusal(err);
|
|
89
|
+
if (refused && refusalLogged)
|
|
90
|
+
return;
|
|
91
|
+
refusalLogged = Boolean(refused);
|
|
92
|
+
console.error(`[orboto-mcp] channel heartbeat ${refused ? 'refused' : 'failed'}: ${refused?.message ?? err.message}`);
|
|
93
|
+
});
|
|
83
94
|
};
|
|
84
95
|
beat();
|
|
85
96
|
const beats = setInterval(beat, 60_000);
|
|
@@ -62,6 +62,9 @@ afterEach(() => { vi.restoreAllMocks(); });
|
|
|
62
62
|
* ORB-2563 gave `orboto_agent_notify` `remind` and `urgent` inside the
|
|
63
63
|
* ceiling: the docs `content` parameter description shrank to pay for it
|
|
64
64
|
* (full = 171,194).
|
|
65
|
+
* ORB-2579 added the four rule proposal tools to the full tier after
|
|
66
|
+
* tightening repeated parameter descriptions (doc ids, bulk selectors,
|
|
67
|
+
* work-session leases, primer facts) by 3.0k (full = 169,607).
|
|
65
68
|
*
|
|
66
69
|
* @see ORB-1805, ORB-1669, ORB-1910
|
|
67
70
|
*/
|
package/dist/project-scope.js
CHANGED
|
@@ -43,7 +43,21 @@ export async function declareEnvProjectScope(client, instanceToken, raw = proces
|
|
|
43
43
|
const current = await client.get('/v1/agent/session', { instanceToken }).catch(() => null);
|
|
44
44
|
if (current && scopeDeclared(current.scope))
|
|
45
45
|
return current;
|
|
46
|
-
const
|
|
46
|
+
const declaration = { scope, scopeDefault: true, start: true };
|
|
47
|
+
const row = await client.post('/v1/agent/heartbeat', declaration, { instanceToken })
|
|
48
|
+
.catch(async (err) => {
|
|
49
|
+
const { exitedPredecessor, presenceRefusal } = await import('./tools/shared.js');
|
|
50
|
+
const { machineId, shortHost } = await import('./session-origin.js');
|
|
51
|
+
const refused = presenceRefusal(err);
|
|
52
|
+
const dead = exitedPredecessor(refused, { machine: machineId(), host: shortHost() });
|
|
53
|
+
if (dead) {
|
|
54
|
+
const again = await client.post('/v1/agent/heartbeat', { ...declaration, previous: 'end' }, { instanceToken }).catch(() => null);
|
|
55
|
+
if (again)
|
|
56
|
+
console.error(`[orboto-mcp] ${dead.line}`);
|
|
57
|
+
return again;
|
|
58
|
+
}
|
|
59
|
+
return refused ? { refusal: refused.message } : null;
|
|
60
|
+
});
|
|
47
61
|
if (row && typeof row.sessionId === 'string')
|
|
48
62
|
rememberDeclaredScope(instanceToken, row.scope ?? scope);
|
|
49
63
|
return row;
|
|
@@ -18,7 +18,7 @@ function fakeClient(row) {
|
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
-
beforeEach(() => { _forgetDeclaredScopes(); });
|
|
21
|
+
beforeEach(() => { _forgetDeclaredScopes(); vi.stubEnv(PROJECT_KEY_ENV, ''); vi.stubEnv(SESSION_ROLE_ENV, ''); });
|
|
22
22
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); });
|
|
23
23
|
describe('ORB-2224 - ORBOTO_PROJECT_KEY', () => {
|
|
24
24
|
it('names the same variable the CLI writes into the entry and hands to the session', () => {
|
|
@@ -41,7 +41,7 @@ describe('ORB-2224 - ORBOTO_PROJECT_KEY', () => {
|
|
|
41
41
|
await declareEnvProjectScope(client, 'agent-abc', 'ACME');
|
|
42
42
|
expect(client.calls).toEqual([
|
|
43
43
|
{ method: 'GET', path: '/v1/agent/session', instanceToken: 'agent-abc' },
|
|
44
|
-
{ method: 'POST', path: '/v1/agent/heartbeat', body: { scope: { role: 'worker', projectKeys: ['ACME'] }, scopeDefault: true }, instanceToken: 'agent-abc' },
|
|
44
|
+
{ method: 'POST', path: '/v1/agent/heartbeat', body: { scope: { role: 'worker', projectKeys: ['ACME'] }, scopeDefault: true, start: true }, instanceToken: 'agent-abc' },
|
|
45
45
|
]);
|
|
46
46
|
expect(rememberedScope('agent-abc')).toEqual({ role: 'worker', projectKeys: ['ACME'] });
|
|
47
47
|
});
|
|
@@ -58,7 +58,7 @@ describe('ORB-2224 - ORBOTO_PROJECT_KEY', () => {
|
|
|
58
58
|
expect(await declareEnvProjectScope(none, 'agent-none', '')).toBeNull();
|
|
59
59
|
expect(none.calls).toEqual([]);
|
|
60
60
|
});
|
|
61
|
-
it('orboto_session_start without a scope
|
|
61
|
+
it('orboto_session_start without a scope sends the environment project as fallbackScope and declares it itself on an instance that ignores it', async () => {
|
|
62
62
|
vi.stubEnv(PROJECT_KEY_ENV, 'acme');
|
|
63
63
|
const beats = [];
|
|
64
64
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
@@ -79,7 +79,7 @@ describe('ORB-2224 - ORBOTO_PROJECT_KEY', () => {
|
|
|
79
79
|
});
|
|
80
80
|
const client = new OrbotoClient({ baseUrl: 'http://api.test', apiKey: 'k' });
|
|
81
81
|
const res = await makeSessionStartHandler(client)({}, { sessionId: 'scope1' });
|
|
82
|
-
expect(beats).toEqual([{}, { scope: { role: 'worker', projectKeys: ['ACME'] }, scopeDefault: true }]);
|
|
82
|
+
expect(beats).toEqual([{ start: true, fallbackScope: { role: 'worker', projectKeys: ['ACME'] } }, { scope: { role: 'worker', projectKeys: ['ACME'] }, scopeDefault: true, start: true }]);
|
|
83
83
|
expect(res.content[0].text).toContain('projects ACME');
|
|
84
84
|
expect(rememberedScope('mcp-scope1')).toEqual({ role: 'worker', projectKeys: ['ACME'] });
|
|
85
85
|
});
|
|
@@ -99,10 +99,10 @@ describe('ORB-2448 - ORBOTO_SESSION_ROLE', () => {
|
|
|
99
99
|
expect(envProjectScope(undefined, 'lead')).toBeNull();
|
|
100
100
|
const client = fakeClient({ sessionId: 'cbb52195-0000-4000-8000-000000000000', scope: null });
|
|
101
101
|
await declareEnvProjectScope(client, 'agent-control', undefined, 'coordinator');
|
|
102
|
-
expect(client.calls[1]).toEqual({ method: 'POST', path: '/v1/agent/heartbeat', body: { scope: { role: 'coordinator' }, scopeDefault: true }, instanceToken: 'agent-control' });
|
|
102
|
+
expect(client.calls[1]).toEqual({ method: 'POST', path: '/v1/agent/heartbeat', body: { scope: { role: 'coordinator' }, scopeDefault: true, start: true }, instanceToken: 'agent-control' });
|
|
103
103
|
expect(rememberedScope('agent-control')).toEqual({ role: 'coordinator' });
|
|
104
104
|
});
|
|
105
|
-
it('orboto_session_start without a scope
|
|
105
|
+
it('orboto_session_start without a scope sends the control session the environment names as fallbackScope in its one start beat', async () => {
|
|
106
106
|
vi.stubEnv(SESSION_ROLE_ENV, 'coordinator');
|
|
107
107
|
vi.stubEnv(PROJECT_KEY_ENV, 'ACME');
|
|
108
108
|
const beats = [];
|
|
@@ -112,7 +112,7 @@ describe('ORB-2448 - ORBOTO_SESSION_ROLE', () => {
|
|
|
112
112
|
if (u.pathname === '/v1/agent/heartbeat') {
|
|
113
113
|
const sent = init?.body ? JSON.parse(init.body) : {};
|
|
114
114
|
beats.push(sent);
|
|
115
|
-
body = { sessionToken: 'x', sessionId: 'cbb52195-0000-4000-8000-000000000000', scope: sent.scope ?? null };
|
|
115
|
+
body = { sessionToken: 'x', sessionId: 'cbb52195-0000-4000-8000-000000000000', scope: sent.scope ?? sent.fallbackScope ?? null };
|
|
116
116
|
}
|
|
117
117
|
else if (u.pathname === '/v1/agent/session') {
|
|
118
118
|
body = { sessionId: 'cbb52195-0000-4000-8000-000000000000', scope: null };
|
|
@@ -124,7 +124,7 @@ describe('ORB-2448 - ORBOTO_SESSION_ROLE', () => {
|
|
|
124
124
|
});
|
|
125
125
|
const client = new OrbotoClient({ baseUrl: 'http://api.test', apiKey: 'k' });
|
|
126
126
|
await makeSessionStartHandler(client)({}, { sessionId: 'control1' });
|
|
127
|
-
expect(beats).toEqual([{
|
|
127
|
+
expect(beats).toEqual([{ start: true, fallbackScope: { role: 'coordinator' } }]);
|
|
128
128
|
expect(rememberedScope('mcp-control1')).toEqual({ role: 'coordinator' });
|
|
129
129
|
});
|
|
130
130
|
});
|
|
@@ -145,6 +145,6 @@ describe('ORB-2593 - a picker-pending orboto claude start', () => {
|
|
|
145
145
|
writeFileSync(join(dir, 'state.json'), JSON.stringify({ pid: 4000 }));
|
|
146
146
|
const resolved = fakeClient({ sessionId: 'x', scope: null });
|
|
147
147
|
await declareEnvProjectScope(resolved, 'agent-abc', 'ACME', undefined, supervisorScopePending(env));
|
|
148
|
-
expect(resolved.calls.filter((c) => c.method === 'POST')).toEqual([{ method: 'POST', path: '/v1/agent/heartbeat', body: { scope: { role: 'worker', projectKeys: ['ACME'] }, scopeDefault: true }, instanceToken: 'agent-abc' }]);
|
|
148
|
+
expect(resolved.calls.filter((c) => c.method === 'POST')).toEqual([{ method: 'POST', path: '/v1/agent/heartbeat', body: { scope: { role: 'worker', projectKeys: ['ACME'] }, scopeDefault: true, start: true }, instanceToken: 'agent-abc' }]);
|
|
149
149
|
});
|
|
150
150
|
});
|
package/dist/response-budget.js
CHANGED
|
@@ -239,7 +239,7 @@ export const TruncationBlockSchema = z.object({
|
|
|
239
239
|
*/
|
|
240
240
|
export const TruncationBlockAdvertisedSchema = z.object({ handle: z.string() })
|
|
241
241
|
.passthrough()
|
|
242
|
-
.describe('
|
|
242
|
+
.describe('Cut: pass handle to orboto_response_expand.');
|
|
243
243
|
function stringMarker(omitted, handle, path) {
|
|
244
244
|
return `... [truncated ${omitted} chars - full value: orboto_response_expand handle="${handle}" path="${path}"]`;
|
|
245
245
|
}
|
package/dist/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { helpToolConfig, makeHelpHandler } from './tools/help.js';
|
|
|
24
24
|
import { apiSearchToolConfig, makeApiSearchHandler } from './tools/api-search.js';
|
|
25
25
|
import { apiCallToolConfig, makeApiCallHandler } from './tools/api-call.js';
|
|
26
26
|
import { listAgentInstructionsToolConfig, makeListAgentInstructionsHandler, createAgentInstructionToolConfig, makeCreateAgentInstructionHandler, updateAgentInstructionToolConfig, makeUpdateAgentInstructionHandler, resetAgentInstructionToolConfig, makeResetAgentInstructionHandler, deleteAgentInstructionToolConfig, makeDeleteAgentInstructionHandler, } from './tools/agent-instructions.js';
|
|
27
|
+
import { ruleProposeToolConfig, makeRuleProposeHandler, ruleProposalsToolConfig, makeRuleProposalsHandler, ruleProposalAcceptToolConfig, makeRuleProposalAcceptHandler, ruleProposalRejectToolConfig, makeRuleProposalRejectHandler, } from './tools/rule-proposals.js';
|
|
27
28
|
import { agentHeartbeatToolConfig, makeAgentHeartbeatHandler, agentPresenceToolConfig, makeAgentPresenceHandler, agentNotifyToolConfig, makeAgentNotifyHandler, } from './tools/agent-coordination.js';
|
|
28
29
|
import { agentBroadcastToolConfig, makeAgentBroadcastHandler, } from './tools/agent-coordination.js';
|
|
29
30
|
import { agentMessageWorkToolConfig, makeAgentMessageWorkHandler } from './tools/agent-message-work.js';
|
|
@@ -200,6 +201,10 @@ export async function buildOrbotoMcpServer(opts) {
|
|
|
200
201
|
reg('orboto_update_agent_instruction', updateAgentInstructionToolConfig, makeUpdateAgentInstructionHandler(client));
|
|
201
202
|
reg('orboto_reset_agent_instruction', resetAgentInstructionToolConfig, makeResetAgentInstructionHandler(client));
|
|
202
203
|
reg('orboto_delete_agent_instruction', deleteAgentInstructionToolConfig, makeDeleteAgentInstructionHandler(client));
|
|
204
|
+
reg('orboto_rule_propose', ruleProposeToolConfig, makeRuleProposeHandler(client));
|
|
205
|
+
reg('orboto_rule_proposals', ruleProposalsToolConfig, makeRuleProposalsHandler(client));
|
|
206
|
+
reg('orboto_rule_proposal_accept', ruleProposalAcceptToolConfig, makeRuleProposalAcceptHandler(client));
|
|
207
|
+
reg('orboto_rule_proposal_reject', ruleProposalRejectToolConfig, makeRuleProposalRejectHandler(client));
|
|
203
208
|
reg('orboto_agent_heartbeat', agentHeartbeatToolConfig, makeAgentHeartbeatHandler(client));
|
|
204
209
|
reg('orboto_agent_presence', agentPresenceToolConfig, makeAgentPresenceHandler(client));
|
|
205
210
|
reg('orboto_who', agentWhoToolConfig, makeAgentWhoHandler(client));
|
package/dist/session-origin.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
10
10
|
import { hostname } from 'node:os';
|
|
11
11
|
import { basename, dirname, join } from 'node:path';
|
|
12
12
|
import { VERSION } from './version.js';
|
|
13
13
|
const KEYS = [
|
|
14
|
-
['host', 'host'], ['machine', 'machineId'], ['repo', 'repo'], ['pid', 'pid'], ['start', 'startedAt'], ['client', 'client'], ['version', 'clientVersion'], ['harness', 'harness'], ['model', 'model'],
|
|
14
|
+
['host', 'host'], ['machine', 'machineId'], ['repo', 'repo'], ['pid', 'pid'], ['start', 'startedAt'], ['client', 'client'], ['version', 'clientVersion'], ['harness', 'harness'], ['model', 'model'], ['checkout', 'checkout'],
|
|
15
15
|
];
|
|
16
16
|
const WRAPPED_HARNESSES = { claude: 'claude-code', 'claude-code': 'claude-code', codex: 'codex', gemini: 'gemini', kimi: 'kimi', agy: 'agy', antigravity: 'agy' };
|
|
17
17
|
/** ORB-2571 - the harness that started a stdio proxy: the one `orboto <harness>` named, else the markers a harness sets. */
|
|
@@ -55,6 +55,26 @@ export function repoBasename(cwd = process.cwd()) {
|
|
|
55
55
|
}
|
|
56
56
|
return basename(cwd) || null;
|
|
57
57
|
}
|
|
58
|
+
/** ORB-2553 - the hashed working tree the directory sits in (a linked worktree is its own), the same value the CLI sends; never a path. */
|
|
59
|
+
export function checkoutHash(cwd = process.cwd()) {
|
|
60
|
+
let root = cwd;
|
|
61
|
+
for (let dir = cwd, i = 0; i < 40; i += 1) {
|
|
62
|
+
if (existsSync(join(dir, '.git'))) {
|
|
63
|
+
root = dir;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const parent = dirname(dir);
|
|
67
|
+
if (parent === dir)
|
|
68
|
+
break;
|
|
69
|
+
dir = parent;
|
|
70
|
+
}
|
|
71
|
+
let physical = root;
|
|
72
|
+
try {
|
|
73
|
+
physical = realpathSync(root);
|
|
74
|
+
}
|
|
75
|
+
catch { /* an unreadable path hashes as given */ }
|
|
76
|
+
return createHash('sha256').update(physical).digest('hex').slice(0, 16);
|
|
77
|
+
}
|
|
58
78
|
let machine;
|
|
59
79
|
/** A hashed stable machine id (the platform UUID on macOS, /etc/machine-id elsewhere); null when none is readable. */
|
|
60
80
|
export function machineId(read = readMachineId) {
|
|
@@ -87,7 +107,7 @@ export function stdioOrigin(env = process.env) {
|
|
|
87
107
|
return params.toString();
|
|
88
108
|
}
|
|
89
109
|
return formatOrigin({
|
|
90
|
-
host: shortHost(), machineId: machineId(), repo: repoBasename(), pid: process.ppid || null,
|
|
110
|
+
host: shortHost(), machineId: machineId(), repo: repoBasename(), checkout: checkoutHash(), pid: process.ppid || null,
|
|
91
111
|
client: 'orboto-mcp', clientVersion: VERSION,
|
|
92
112
|
harness: stdioHarness(env),
|
|
93
113
|
model: /^[\x21-\x7e]{1,80}$/.test((env.ORBOTO_MODEL ?? '').trim()) ? env.ORBOTO_MODEL.trim() : null,
|
|
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
import { AGENT_WAKE_LABELS, AgentInventoryKindSchema, AgentSessionWakeSchema, } from './agent-presence-schema.js';
|
|
8
8
|
import { AgentPresenceRowSchema } from './agent-presence-row.js';
|
|
9
9
|
import { OrbotoApiError } from '../orboto-client.js';
|
|
10
|
-
import { mcpInstanceToken } from './shared.js';
|
|
10
|
+
import { mcpInstanceToken, presenceRefusal } from './shared.js';
|
|
11
11
|
import { AgentSessionRoleSchema, AgentSessionScopeSchema } from './agent-session-scope.js';
|
|
12
12
|
export const agentHeartbeatToolConfig = {
|
|
13
13
|
title: 'Agent heartbeat (Multi-Agent Coordination)',
|
|
@@ -36,7 +36,16 @@ export const agentHeartbeatToolConfig = {
|
|
|
36
36
|
};
|
|
37
37
|
export function makeAgentHeartbeatHandler(client) {
|
|
38
38
|
return async (args, extra) => {
|
|
39
|
-
|
|
39
|
+
let res;
|
|
40
|
+
try {
|
|
41
|
+
res = await client.post('/v1/agent/heartbeat', args, { instanceToken: mcpInstanceToken(undefined, extra) });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const refused = presenceRefusal(err);
|
|
45
|
+
if (!refused)
|
|
46
|
+
throw err;
|
|
47
|
+
return { content: [{ type: 'text', text: `heartbeat refused (ORB-2553) - ${refused.message}` }], isError: true };
|
|
48
|
+
}
|
|
40
49
|
const scope = res.scope ? ` scope=${JSON.stringify(res.scope)}` : '';
|
|
41
50
|
const notes = (res.notes ?? []).map((n) => `\n${n}`).join('');
|
|
42
51
|
return {
|
|
@@ -176,7 +185,7 @@ export function noRecipientText(err) {
|
|
|
176
185
|
return null;
|
|
177
186
|
const p = body.errorParams ?? {};
|
|
178
187
|
const live = (p.liveSessions ?? []).map((s) => `${s.addresses[0] ?? s.email} (${s.email}, ${s.shortId})`);
|
|
179
|
-
const ways = 'a role address in `to` (integrator@KEY), `toSessionRef` with an instance short id, or `queue: true` with `project` to keep it until a session declares the project';
|
|
188
|
+
const ways = 'a role address in `to` (integrator@KEY), `toSessionRef` with an instance short id, or `queue: true` with `project` (a role address needs no project) to keep it until a session declares the project or the role';
|
|
180
189
|
if (p.reason === 'ambiguous_account') {
|
|
181
190
|
return `not sent: ${p.address ?? 'that account'} has ${live.length} live sessions you may address and a request needs one of them. Sessions: ${live.join('; ')}. Send again with \`toSessionRef\` set to the instance short id of one, or address a role (${ways}).`;
|
|
182
191
|
}
|
|
@@ -218,7 +227,7 @@ export function reminderTooSoonText(err) {
|
|
|
218
227
|
}
|
|
219
228
|
export const agentNotifyToolConfig = {
|
|
220
229
|
title: 'Notify an agent or user',
|
|
221
|
-
description: 'Message an agent or user. `to` takes a role address integrator@KEY (every live session with that role on the project, any account), an account email or a session id; targetEmail names the account (with a role address it narrows to that account). kind: request|error = work inside the scope the recipient holds (request needs a target + outcome); info = information; complete = result (needs outcome). outcome: one sentence, 10 to 1000 characters; body: the plain text of the message (any length, lands in payload.body; payload may carry other JSON); threadId links a reply to the message it answers. toSessionRef (instance short id, session ref or id) reaches ONE session of the account; alone (no to, no targetEmail) a session id or short id addresses that session; project scopes an account message to sessions declared on it. A request to an account email without project or toSessionRef goes to its one live session, with several it is refused listing them (pick one with toSessionRef). A request no live session can receive is refused (no_recipient, naming the live sessions of the project; see orboto_who) unless queue: true holds it until a session declares the project. The answer names routedTo; routingNote "no session declared KEY" means nothing wakes - escalate. A request on a topic another role owns (orboto_who for) is refused; route: true sends it to the owner, forceTarget: true keeps the target. project is a key or UUID; senderRef defaults to this MCP session. An identical send (same recipient, project, kind, subject, payload; no threadId, no attachments) within the dedupe window (default 24 h) is not delivered again: the text says so and names the existing message. remind: true is a deliberate reminder of it (notifies again, at most one per 15 min, faster is refused with retryAfter); urgent: true is never suppressed and counts as a reminder. Loops and retries never set remind or urgent.',
|
|
230
|
+
description: 'Message an agent or user. `to` takes a role address integrator@KEY (every live session with that role on the project, any account), an account email or a session id; targetEmail names the account (with a role address it narrows to that account). kind: request|error = work inside the scope the recipient holds (request needs a target + outcome); info = information; complete = result (needs outcome). outcome: one sentence, 10 to 1000 characters; body: the plain text of the message (any length, lands in payload.body; payload may carry other JSON); threadId links a reply to the message it answers. toSessionRef (instance short id, session ref or id) reaches ONE session of the account; alone (no to, no targetEmail) a session id or short id addresses that session; project scopes an account message to sessions declared on it. A request to an account email without project or toSessionRef goes to its one live session, with several it is refused listing them (pick one with toSessionRef). A request no live session can receive is refused (no_recipient, naming the live sessions of the project; see orboto_who) unless queue: true holds it until a session declares the project; queued to a role address (worker@KEY) it waits for the first session that declares that role, and a queued request to an account address on the project of the owner address goes to the owner role. The answer names routedTo; routingNote "no session declared KEY" means nothing wakes - escalate. A request on a topic another role owns (orboto_who for) is refused; route: true sends it to the owner, forceTarget: true keeps the target. project is a key or UUID; senderRef defaults to this MCP session. An identical send (same recipient, project, kind, subject, payload; no threadId, no attachments) within the dedupe window (default 24 h) is not delivered again: the text says so and names the existing message. remind: true is a deliberate reminder of it (notifies again, at most one per 15 min, faster is refused with retryAfter); urgent: true is never suppressed and counts as a reminder. Loops and retries never set remind or urgent.',
|
|
222
231
|
inputSchema: z.object({
|
|
223
232
|
targetEmail: z.string().email().optional(),
|
|
224
233
|
to: z.string().min(3).max(200).optional(),
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { sizeBlockResult } from './shared.js';
|
|
9
|
+
const AGENTS_PROPOSE = ' Agents propose shared rules with orboto_rule_propose (403 errors.agent_rules.write_needs_person).';
|
|
9
10
|
const RULE_WRITING_CONTRACT = ' Rule: title = one imperative sentence; body <= 400 chars, Rule / Why / How, no long examples, nothing another block says.';
|
|
10
11
|
function excerptOf(body) {
|
|
11
12
|
const firstLine = body.split('\n', 1)[0] ?? '';
|
|
@@ -74,7 +75,7 @@ export function makeListAgentInstructionsHandler(client) {
|
|
|
74
75
|
export const createAgentInstructionToolConfig = {
|
|
75
76
|
title: 'Add a custom coding-agent rule block',
|
|
76
77
|
description: 'Create a custom rule block at a scope (workspace, customer, project or personal). Workspace reaches every agent (admin:ai:write), customer every project of one customer (customer:write), project one project (project:edit), personal only you. For scope=customer pass customerId; for scope=project pass projectId.'
|
|
77
|
-
+ RULE_WRITING_CONTRACT,
|
|
78
|
+
+ AGENTS_PROPOSE + RULE_WRITING_CONTRACT,
|
|
78
79
|
inputSchema: z.object({
|
|
79
80
|
title: z.string().min(1).max(120),
|
|
80
81
|
body: z.string().min(1).max(8000).describe('The rule text the agent should follow.'),
|
|
@@ -109,7 +110,7 @@ export function makeCreateAgentInstructionHandler(client) {
|
|
|
109
110
|
export const updateAgentInstructionToolConfig = {
|
|
110
111
|
title: 'Edit / toggle / reorder a coding-agent rule block',
|
|
111
112
|
description: 'Patch a rule block: title, body, enabled (toggle a rule on/off), or sortOrder. Works on default AND custom blocks (defaults keep their builtinKey and can be reset later). Needs admin:ai:write.'
|
|
112
|
-
+ RULE_WRITING_CONTRACT,
|
|
113
|
+
+ AGENTS_PROPOSE + RULE_WRITING_CONTRACT,
|
|
113
114
|
inputSchema: z.object({
|
|
114
115
|
id: z.string().uuid(),
|
|
115
116
|
title: z.string().min(1).max(120).optional(),
|
|
@@ -141,7 +142,7 @@ export function makeUpdateAgentInstructionHandler(client) {
|
|
|
141
142
|
}
|
|
142
143
|
export const resetAgentInstructionToolConfig = {
|
|
143
144
|
title: 'Reset a default rule block to its shipped text',
|
|
144
|
-
description: 'Restore a seeded DEFAULT rule block (one with a builtinKey) to the text orboto ships. No-op for custom blocks. Needs admin:ai:write.',
|
|
145
|
+
description: 'Restore a seeded DEFAULT rule block (one with a builtinKey) to the text orboto ships. No-op for custom blocks. Needs admin:ai:write.' + AGENTS_PROPOSE,
|
|
145
146
|
inputSchema: z.object({ id: z.string().uuid() }).shape,
|
|
146
147
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
147
148
|
};
|
|
@@ -153,7 +154,7 @@ export function makeResetAgentInstructionHandler(client) {
|
|
|
153
154
|
}
|
|
154
155
|
export const deleteAgentInstructionToolConfig = {
|
|
155
156
|
title: 'Delete a custom coding-agent rule block',
|
|
156
|
-
description: 'Delete a CUSTOM rule block. Default (builtin) blocks cannot be deleted - disable them via update instead. Needs admin:ai:write.',
|
|
157
|
+
description: 'Delete a CUSTOM rule block. Default (builtin) blocks cannot be deleted - disable them via update instead. Needs admin:ai:write.' + AGENTS_PROPOSE,
|
|
157
158
|
inputSchema: z.object({ id: z.string().uuid() }).shape,
|
|
158
159
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
159
160
|
};
|
package/dist/tools/agent-who.js
CHANGED
|
@@ -3,15 +3,23 @@ import { z } from 'zod';
|
|
|
3
3
|
import { ownerLine } from './responsibilities.js';
|
|
4
4
|
import { nextPageLine, originText, PresenceFilterShape, presenceQuery, readPresence, unknownProjectText } from './agent-coordination.js';
|
|
5
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
|
+
}
|
|
6
14
|
export const agentWhoToolConfig = {
|
|
7
15
|
title: 'Who receives agent mail',
|
|
8
|
-
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. `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.',
|
|
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.',
|
|
9
17
|
inputSchema: z.object({
|
|
10
18
|
project: z.string().min(1).max(64).optional(),
|
|
11
19
|
for: z.string().min(2).max(2000).optional(),
|
|
12
20
|
...PresenceFilterShape,
|
|
13
21
|
}).shape,
|
|
14
|
-
outputSchema: z.object({ sessions: z.array(z.unknown()), owner: z.unknown().optional(), nextCursor: z.string().nullable() }).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,
|
|
15
23
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
16
24
|
};
|
|
17
25
|
/** One directory row as a line: address, account, session, projects, channel, ticket. */
|
|
@@ -35,9 +43,10 @@ export function makeAgentWhoHandler(client) {
|
|
|
35
43
|
const lines = res.sessions.length
|
|
36
44
|
? [...ownerLines, `${res.sessions.length} live session(s)${res.nextCursor ? ' on this page' : ''}:`, ...res.sessions.map(whoLine), ...nextPageLine(res.nextCursor)]
|
|
37
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));
|
|
38
47
|
return {
|
|
39
48
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
40
|
-
structuredContent: { sessions: res.sessions, ...(args.for ? { owner: (res.owner ?? null) } : {}), nextCursor: res.nextCursor ?? null },
|
|
49
|
+
structuredContent: { sessions: res.sessions, ...(args.for ? { owner: (res.owner ?? null) } : {}), nextCursor: res.nextCursor ?? null, ...(res.refusals?.length ? { refusals: res.refusals } : {}) },
|
|
41
50
|
};
|
|
42
51
|
};
|
|
43
52
|
}
|
|
@@ -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 },
|