@orboto/mcp 0.201.0 → 0.202.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/claude-context.js +1 -0
- package/dist/claude-context.test.js +6 -1
- package/dist/connect-script.js +1 -1
- package/dist/inbox-channel.js +32 -0
- package/dist/inbox-channel.test.js +42 -1
- package/dist/index.js +4 -1
- package/dist/session-restart.js +59 -0
- package/dist/session-restart.test.js +56 -0
- package/dist/tools/agent-presence-schema.js +4 -0
- package/dist/tools/agent-wakes.test.js +50 -0
- package/dist/tools/docs.js +2 -2
- package/dist/tools/shared.test.js +7 -1
- package/package.json +1 -1
package/dist/claude-context.js
CHANGED
|
@@ -41,6 +41,7 @@ export function readClaudeContext(dir, now = Date.now(), root = claudeContextDir
|
|
|
41
41
|
...(typeof parsed.contextWindowSize === 'number' && Number.isFinite(parsed.contextWindowSize)
|
|
42
42
|
? { contextWindowSize: Math.max(0, Math.round(parsed.contextWindowSize)) }
|
|
43
43
|
: {}),
|
|
44
|
+
...(typeof parsed.sessionId === 'string' && parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
return fresh.length === 1 ? fresh[0] : null;
|
|
@@ -6,7 +6,7 @@ import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
|
|
6
6
|
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import { readClaudeContext } from './claude-context.js';
|
|
9
|
+
import { claudeContextDir, readClaudeContext } from './claude-context.js';
|
|
10
10
|
let root;
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
root = mkdtempSync(path.join(tmpdir(), 'orboto-ctx-'));
|
|
@@ -36,3 +36,8 @@ describe('ORB-2149 - claude statusline context reports', () => {
|
|
|
36
36
|
expect(readClaudeContext('/work/repo', Date.now(), path.join(root, 'missing'))).toBeNull();
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
|
+
describe('ORB-2149 - the report directory', () => {
|
|
40
|
+
it('lives under the orboto state directory of the user', () => {
|
|
41
|
+
expect(claudeContextDir().endsWith(path.join('.orboto', 'claude-context'))).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
package/dist/connect-script.js
CHANGED
|
@@ -8,7 +8,7 @@ export const CONNECT_SCRIPT_HEADING = 'If you are an AI agent helping a person s
|
|
|
8
8
|
export const CONNECT_SCRIPT_STEPS = [
|
|
9
9
|
'Work one step per message: say in one sentence what the step does and why, run it, show the result, and only then move on.',
|
|
10
10
|
'Run every step you can run yourself - `orboto connect --yes` checks Node and Claude Code, writes the `orboto` server entry for this directory and prints the exact start command with the standing arguments.',
|
|
11
|
-
'Hand the person only the steps that need a human: the browser login, quitting and restarting the session with that command, confirming the servers from `.mcp.json` and the local-development prompt, and, on a Team or Enterprise plan, the one Owner switch that allows channels.',
|
|
11
|
+
'Hand the person only the steps that need a human: the browser login, the two `/plugin` commands that install the orboto plugin (a CLI cannot type into a running session), quitting and restarting the session with that command, confirming the servers from `.mcp.json` and, in the development form, the local-development prompt, and, on a Team or Enterprise plan, the one Owner switch that allows channels and lists the plugin.',
|
|
12
12
|
'Give every command the person runs verbatim in its own code block, never paraphrased and never merged into prose.',
|
|
13
13
|
'Verify before you continue: `orboto whoami` for the identity, `orboto mcp status` for the entry of this directory, `orboto connect --verify` for the channel.',
|
|
14
14
|
'Never paste a reference page into the chat - link it and keep the message to the step you are on.',
|
package/dist/inbox-channel.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RESTART_REQUESTED_NOTICE, writeRestartRequest } from './session-restart.js';
|
|
2
|
+
export { RESTART_REQUESTED_NOTICE };
|
|
1
3
|
export const CHANNEL_METHOD = 'notifications/claude/channel';
|
|
2
4
|
export const CHANNEL_CAPABILITY = 'claude/channel';
|
|
3
5
|
export const DEFAULT_DIGEST_MINUTES = 15;
|
|
@@ -171,8 +173,24 @@ export class InboxChannel {
|
|
|
171
173
|
if (!content)
|
|
172
174
|
return;
|
|
173
175
|
this.stats.notices += 1;
|
|
176
|
+
if (notice === RESTART_REQUESTED_NOTICE) {
|
|
177
|
+
await this.emit({ content: this.takeRestartRequest(content), meta: { kind: 'notice', notice } });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
174
180
|
await this.emit({ content, meta: { kind: 'notice', notice } });
|
|
175
181
|
}
|
|
182
|
+
/** ORB-2181 - an admin restart notice becomes the supervisor's request file. */
|
|
183
|
+
takeRestartRequest(reason) {
|
|
184
|
+
try {
|
|
185
|
+
const result = (this.opts.writeRestart ?? writeRestartRequest)(process.cwd(), { reason, source: 'channel' });
|
|
186
|
+
return result.written
|
|
187
|
+
? `restart requested by the operator (${reason}) - the orboto claude supervisor restarts this session as soon as this turn ends and resumes the same conversation; Claude Code asks for the development-channel confirmation in that terminal.`
|
|
188
|
+
: `restart requested by the operator (${reason}), but nothing could be written: ${result.detail}. Run \`orboto claude --restart --reason "${reason}"\` in this session's terminal instead.`;
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
return `restart requested by the operator (${reason}), but the request file could not be written: ${err.message}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
176
194
|
async flushDigest() {
|
|
177
195
|
if (this.batch.length === 0)
|
|
178
196
|
return;
|
|
@@ -279,6 +297,20 @@ export class InboxChannel {
|
|
|
279
297
|
}
|
|
280
298
|
}
|
|
281
299
|
}
|
|
300
|
+
/** ORB-2175 - the notice name the stale-CLI finding is delivered under. */
|
|
301
|
+
export const CLI_OUTDATED_NOTICE = 'cli_outdated';
|
|
302
|
+
/**
|
|
303
|
+
* ORB-2175 - `orboto mcp serve` hands its version-skew finding to the proxy in
|
|
304
|
+
* ORBOTO_MCP_CLI_OUTDATED; the channel says it once on connect, because a CLI
|
|
305
|
+
* older than the instance leaves this session under a second identity.
|
|
306
|
+
*/
|
|
307
|
+
export function cliOutdatedNotice(raw) {
|
|
308
|
+
const finding = (raw ?? '').trim();
|
|
309
|
+
if (!finding)
|
|
310
|
+
return '';
|
|
311
|
+
return `${finding}. Until then this checkout runs under two instance tokens, so this session is woken by its own edits. `
|
|
312
|
+
+ 'Fix it yourself: run `orboto self-update`, then `orboto claude --restart --reason "CLI updated"` - the supervisor restarts this session on the same conversation as soon as your turn ends.';
|
|
313
|
+
}
|
|
282
314
|
/** ORB-2140 - the digest window from the environment; NaN or negative fall back to the default. */
|
|
283
315
|
export function digestMinutesFromEnv(raw) {
|
|
284
316
|
if (raw === undefined || raw.trim() === '')
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
7
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
8
|
-
import { CHANNEL_CAPABILITY, CHANNEL_INSTRUCTIONS, CHANNEL_METHOD, InboxChannel, digestMinutesFromEnv, isImmediate, renderEvent } from './inbox-channel.js';
|
|
8
|
+
import { CHANNEL_CAPABILITY, CHANNEL_INSTRUCTIONS, CHANNEL_METHOD, CLI_OUTDATED_NOTICE, InboxChannel, RESTART_REQUESTED_NOTICE, cliOutdatedNotice, digestMinutesFromEnv, isImmediate, renderEvent } from './inbox-channel.js';
|
|
9
9
|
import { buildOrbotoMcpServer } from './server.js';
|
|
10
10
|
function mockMcp() {
|
|
11
11
|
const notification = vi.fn().mockResolvedValue(undefined);
|
|
@@ -115,6 +115,47 @@ describe('InboxChannel', () => {
|
|
|
115
115
|
expect(String(fetchFn.mock.calls[1][0])).toContain('since=m1');
|
|
116
116
|
channel.close();
|
|
117
117
|
});
|
|
118
|
+
it('ORB-2181 - a restart_requested notice becomes the supervisor request file and says so', async () => {
|
|
119
|
+
const { mcp, notification } = mockMcp();
|
|
120
|
+
const writeRestart = vi.fn().mockReturnValue({ written: true, sessionId: 'sess-1', path: '/home/a/.orboto/claude/sess-1/restart.json', detail: 'ok' });
|
|
121
|
+
const fetchFn = vi.fn().mockImplementation(() => new Promise(() => { }));
|
|
122
|
+
const channel = new InboxChannel({ baseUrl: 'https://x.test', apiKey: 'orb_k', instanceToken: 'mcp-proc', mcp, fetchFn, log: () => { }, digestMinutes: 0, writeRestart });
|
|
123
|
+
await channel.deliverNotice(RESTART_REQUESTED_NOTICE, 'the CLI was updated');
|
|
124
|
+
expect(writeRestart).toHaveBeenCalledWith(process.cwd(), { reason: 'the CLI was updated', source: 'channel' });
|
|
125
|
+
const sent = notification.mock.calls[0][0];
|
|
126
|
+
expect(sent.params.meta).toEqual({ kind: 'notice', notice: 'restart_requested' });
|
|
127
|
+
expect(sent.params.content).toContain('resumes the same conversation');
|
|
128
|
+
expect(channel.stats).toMatchObject({ notices: 1, delivered: 0 });
|
|
129
|
+
channel.close();
|
|
130
|
+
});
|
|
131
|
+
it('ORB-2181 - a restart_requested notice that cannot be written says why instead of pretending', async () => {
|
|
132
|
+
const { mcp, notification } = mockMcp();
|
|
133
|
+
const writeRestart = vi.fn().mockReturnValue({ written: false, sessionId: null, path: null, detail: 'no status line report' });
|
|
134
|
+
const fetchFn = vi.fn().mockImplementation(() => new Promise(() => { }));
|
|
135
|
+
const channel = new InboxChannel({ baseUrl: 'https://x.test', apiKey: 'orb_k', instanceToken: 'mcp-proc', mcp, fetchFn, log: () => { }, digestMinutes: 0, writeRestart });
|
|
136
|
+
await channel.deliverNotice(RESTART_REQUESTED_NOTICE, 'rotated key');
|
|
137
|
+
const sent = notification.mock.calls[0][0];
|
|
138
|
+
expect(sent.params.content).toContain('no status line report');
|
|
139
|
+
channel.close();
|
|
140
|
+
});
|
|
141
|
+
it('ORB-2175 - the stale-CLI finding from `mcp serve` becomes one cli_outdated notice on connect', async () => {
|
|
142
|
+
expect(cliOutdatedNotice(undefined)).toBe('');
|
|
143
|
+
expect(cliOutdatedNotice(' ')).toBe('');
|
|
144
|
+
const finding = 'CLI 0.200.0 is older than the instance 0.201.0 - run `orboto self-update` and restart this session';
|
|
145
|
+
const content = cliOutdatedNotice(finding);
|
|
146
|
+
expect(content).toContain(finding);
|
|
147
|
+
expect(content).toContain('two instance tokens');
|
|
148
|
+
const { mcp, notification } = mockMcp();
|
|
149
|
+
const fetchFn = vi.fn().mockImplementation(() => new Promise(() => { }));
|
|
150
|
+
const channel = new InboxChannel({ baseUrl: 'https://x.test', apiKey: 'orb_k', instanceToken: 'mcp-proc', mcp, fetchFn, log: () => { }, digestMinutes: 0 });
|
|
151
|
+
await channel.deliverNotice(CLI_OUTDATED_NOTICE, content);
|
|
152
|
+
expect(notification).toHaveBeenCalledTimes(1);
|
|
153
|
+
const sent = notification.mock.calls[0][0];
|
|
154
|
+
expect(sent.params.meta).toEqual({ kind: 'notice', notice: 'cli_outdated' });
|
|
155
|
+
expect(sent.params.content).toBe(content);
|
|
156
|
+
expect(channel.stats).toMatchObject({ notices: 1, delivered: 0 });
|
|
157
|
+
channel.close();
|
|
158
|
+
});
|
|
118
159
|
});
|
|
119
160
|
describe('server capability', () => {
|
|
120
161
|
async function connect(channel) {
|
package/dist/index.js
CHANGED
|
@@ -59,11 +59,14 @@ async function main() {
|
|
|
59
59
|
const stdio = new StdioServerTransport();
|
|
60
60
|
await server.connect(stdio);
|
|
61
61
|
if (channel) {
|
|
62
|
-
const { InboxChannel, digestMinutesFromEnv } = await import('./inbox-channel.js');
|
|
62
|
+
const { InboxChannel, digestMinutesFromEnv, cliOutdatedNotice, CLI_OUTDATED_NOTICE } = await import('./inbox-channel.js');
|
|
63
63
|
const { mcpProcessInstance } = await import('./tools/shared.js');
|
|
64
64
|
const instanceToken = mcpProcessInstance();
|
|
65
65
|
const inbox = new InboxChannel({ ...clientConfig, instanceToken, mcp: server, digestMinutes: digestMinutesFromEnv(process.env.ORBOTO_MCP_CHANNEL_DIGEST_MINUTES) });
|
|
66
66
|
inbox.start();
|
|
67
|
+
const outdated = cliOutdatedNotice(process.env.ORBOTO_MCP_CLI_OUTDATED);
|
|
68
|
+
if (outdated)
|
|
69
|
+
void inbox.deliverNotice(CLI_OUTDATED_NOTICE, outdated);
|
|
67
70
|
const { readClaudeContext } = await import('./claude-context.js');
|
|
68
71
|
const beat = () => {
|
|
69
72
|
const context = readClaudeContext(process.cwd());
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-2181 - the proxy half of the session restart: whatever asked for it -
|
|
3
|
+
* the agent through `orboto_session_restart` or the operator through the
|
|
4
|
+
* `restart_requested` channel notice - ends as the same request file the
|
|
5
|
+
* `orboto claude` supervisor consumes. No socket, no signal, no network
|
|
6
|
+
* listener: one 0600 file in a 0700 directory of the caller's own home.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { claudeContextDir, readClaudeContext } from './claude-context.js';
|
|
12
|
+
export const RESTART_REQUESTED_NOTICE = 'restart_requested';
|
|
13
|
+
export function claudeSessionsDir() {
|
|
14
|
+
return path.join(homedir(), '.orboto', 'claude');
|
|
15
|
+
}
|
|
16
|
+
/** Keeps a session id from escaping the session root. */
|
|
17
|
+
export function safeSessionName(id) {
|
|
18
|
+
const cleaned = [...id].filter((c) => /[A-Za-z0-9_-]/.test(c)).join('').slice(0, 80);
|
|
19
|
+
return cleaned || 'session';
|
|
20
|
+
}
|
|
21
|
+
export function writeRestartRequest(dir, input, roots = {}) {
|
|
22
|
+
const now = roots.now ?? Date.now();
|
|
23
|
+
const root = roots.root ?? claudeSessionsDir();
|
|
24
|
+
const report = readClaudeContext(dir, now, roots.contextRoot ?? claudeContextDir());
|
|
25
|
+
const sessionId = report?.sessionId;
|
|
26
|
+
if (!sessionId) {
|
|
27
|
+
return {
|
|
28
|
+
written: false,
|
|
29
|
+
sessionId: null,
|
|
30
|
+
path: null,
|
|
31
|
+
detail: 'no status line report identifies a Claude Code session in this directory - wire the reporter up with `orboto mcp install --statusline`, then ask again',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const target = path.join(root, safeSessionName(sessionId));
|
|
35
|
+
const body = JSON.stringify({
|
|
36
|
+
reason: input.reason,
|
|
37
|
+
source: input.source,
|
|
38
|
+
requestedAt: new Date(now).toISOString(),
|
|
39
|
+
...(input.requestedBy ? { requestedBy: input.requestedBy } : {}),
|
|
40
|
+
idleTimeoutSeconds: input.idleTimeoutSeconds ?? 0,
|
|
41
|
+
});
|
|
42
|
+
const staging = path.join(target, `.restart-${process.pid}.tmp`);
|
|
43
|
+
const file = path.join(target, 'restart.json');
|
|
44
|
+
mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
45
|
+
writeFileSync(staging, body, { mode: 0o600 });
|
|
46
|
+
try {
|
|
47
|
+
renameSync(staging, file);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
rmSync(staging, { force: true });
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
written: true,
|
|
55
|
+
sessionId,
|
|
56
|
+
path: file,
|
|
57
|
+
detail: 'the supervisor restarts this session as soon as no turn is running; it never interrupts one',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-2181 - the proxy writes one 0600 request file into a 0700 directory
|
|
3
|
+
* named after the Claude Code session, and refuses when no status line
|
|
4
|
+
* report names one.
|
|
5
|
+
*/
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { claudeSessionsDir, safeSessionName, writeRestartRequest } from './session-restart.js';
|
|
11
|
+
let contexts;
|
|
12
|
+
let sessions;
|
|
13
|
+
const NOW = Date.parse('2026-09-19T10:00:00Z');
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
contexts = mkdtempSync(path.join(tmpdir(), 'orboto-ctx-'));
|
|
16
|
+
sessions = mkdtempSync(path.join(tmpdir(), 'orboto-sessions-'));
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(contexts, { recursive: true, force: true });
|
|
20
|
+
rmSync(sessions, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
function report(body, root = contexts) {
|
|
23
|
+
mkdirSync(root, { recursive: true });
|
|
24
|
+
writeFileSync(path.join(root, 'report.json'), JSON.stringify(body));
|
|
25
|
+
}
|
|
26
|
+
describe('ORB-2181 - the restart request file', () => {
|
|
27
|
+
it('writes the request under the reported session id, owner only', () => {
|
|
28
|
+
report({ cwd: '/work/repo', sessionId: 'sess-7f3a2b1c', contextTokens: 10, updatedAt: new Date(NOW).toISOString() });
|
|
29
|
+
const result = writeRestartRequestHere('/work/repo', 'the CLI was updated');
|
|
30
|
+
expect(result.written).toBe(true);
|
|
31
|
+
expect(result.sessionId).toBe('sess-7f3a2b1c');
|
|
32
|
+
const file = path.join(sessions, 'sess-7f3a2b1c', 'restart.json');
|
|
33
|
+
expect(result.path).toBe(file);
|
|
34
|
+
expect(statSync(file).mode & 0o777).toBe(0o600);
|
|
35
|
+
expect(statSync(path.dirname(file)).mode & 0o777).toBe(0o700);
|
|
36
|
+
expect(JSON.parse(readFileSync(file, 'utf8'))).toMatchObject({ reason: 'the CLI was updated', source: 'mcp' });
|
|
37
|
+
});
|
|
38
|
+
it('refuses without a status line report and names the fix', () => {
|
|
39
|
+
const result = writeRestartRequestHere('/work/repo', 'rotated key');
|
|
40
|
+
expect(result.written).toBe(false);
|
|
41
|
+
expect(result.sessionId).toBeNull();
|
|
42
|
+
expect(result.detail).toContain('orboto mcp install --statusline');
|
|
43
|
+
});
|
|
44
|
+
it('keeps a session id from escaping the session root', () => {
|
|
45
|
+
expect(safeSessionName('../../etc/passwd')).toBe('etcpasswd');
|
|
46
|
+
expect(safeSessionName('!!!')).toBe('session');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
function writeRestartRequestHere(dir, reason) {
|
|
50
|
+
return writeRestartRequest(dir, { reason, source: 'mcp' }, { now: NOW, root: sessions, contextRoot: contexts });
|
|
51
|
+
}
|
|
52
|
+
describe('ORB-2181 - the session root', () => {
|
|
53
|
+
it('lives under the orboto state directory of the user', () => {
|
|
54
|
+
expect(claudeSessionsDir().endsWith(path.join('.orboto', 'claude'))).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -45,6 +45,10 @@ export const AgentInventoryEntrySchema = z.object({
|
|
|
45
45
|
wrongWakeContextTokens: z.number().int(),
|
|
46
46
|
backlogSeen: z.number().int(),
|
|
47
47
|
}),
|
|
48
|
+
/** ORB-2181 - a restart was requested for this session and its supervisor has not reconnected yet. */
|
|
49
|
+
restartPending: z.boolean(),
|
|
50
|
+
/** ORB-2181 - the reason the requester gave; null when nothing is pending. */
|
|
51
|
+
restartReason: z.string().nullable(),
|
|
48
52
|
lastSeenAt: z.string(),
|
|
49
53
|
createdAt: z.string(),
|
|
50
54
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { makeAgentWakesHandler } from './agent-wakes.js';
|
|
3
|
+
function clientReturning(body) {
|
|
4
|
+
const get = vi.fn().mockResolvedValue(body);
|
|
5
|
+
return { client: { get }, get };
|
|
6
|
+
}
|
|
7
|
+
function textOf(result) {
|
|
8
|
+
return result.content[0].text;
|
|
9
|
+
}
|
|
10
|
+
describe('orboto_agent_wakes', () => {
|
|
11
|
+
it('renders the stats with precision, wrong-wake cost and backlog per session and sender', async () => {
|
|
12
|
+
const { client, get } = clientReturning({
|
|
13
|
+
hours: 12,
|
|
14
|
+
sessions: [
|
|
15
|
+
{ label: 'lead (ab12cd34)', wakes: 4, precision: 0.75, medianLatencyMs: 20, topRule: 'addressed', wrongWakeContextTokens: 1200, backlogSeen: 3 },
|
|
16
|
+
{ label: 'worker (ef56ab78)', wakes: 1, precision: null, medianLatencyMs: null, topRule: null, wrongWakeContextTokens: 0, backlogSeen: 0 },
|
|
17
|
+
],
|
|
18
|
+
senders: [],
|
|
19
|
+
});
|
|
20
|
+
const result = await makeAgentWakesHandler(client)({ stats: true, hours: 12, sessionId: 'ab12cd34' });
|
|
21
|
+
expect(get).toHaveBeenCalledWith('/admin/agents/wake-stats?sessionId=ab12cd34&hours=12');
|
|
22
|
+
const text = textOf(result);
|
|
23
|
+
expect(text).toContain('Last 12 h - woken sessions:');
|
|
24
|
+
expect(text).toContain('- lead (ab12cd34): 4 wakes, precision 75%, median 20 ms, top rule addressed, wrong wakes cost 1200 tokens, 3 backlog seen');
|
|
25
|
+
expect(text).toContain('- worker (ef56ab78): 1 wakes, precision unjudged, median ? ms, top rule -');
|
|
26
|
+
expect(text).toContain('Senders:\n- (none)');
|
|
27
|
+
});
|
|
28
|
+
it('says so when nothing woke anyone', async () => {
|
|
29
|
+
const { client } = clientReturning({ hours: 24, sessions: [], senders: [] });
|
|
30
|
+
expect(textOf(await makeAgentWakesHandler(client)({ stats: true }))).toContain('- (no wakes)');
|
|
31
|
+
});
|
|
32
|
+
it('lists the ledger rows with latency, context cost and the next cursor', async () => {
|
|
33
|
+
const { client, get } = clientReturning({
|
|
34
|
+
rows: [
|
|
35
|
+
{ wokenAt: '2026-01-02T03:04:05Z', subject: 'probe', senderLabel: 'lead (ab12cd34)', rule: 'addressed', transport: 'channel', outcome: 'handled', latencyMs: 18, contextTokens: 900 },
|
|
36
|
+
{ wokenAt: '2026-01-02T03:05:05Z', subject: 'digest', senderLabel: 'worker (ef56ab78)', rule: 'scope_project', transport: 'poll', outcome: 'pending', latencyMs: null, contextTokens: null },
|
|
37
|
+
],
|
|
38
|
+
nextCursor: 'abc',
|
|
39
|
+
});
|
|
40
|
+
const text = textOf(await makeAgentWakesHandler(client)({ project: 'ACME', outcome: 'handled', limit: 2, cursor: 'c0' }));
|
|
41
|
+
expect(get).toHaveBeenCalledWith('/v1/agent/wakes?project=ACME&outcome=handled&limit=2&cursor=c0');
|
|
42
|
+
expect(text).toContain('- 2026-01-02T03:04:05Z probe - from lead (ab12cd34) via addressed/channel -> handled (18 ms, 900 ctx tokens)');
|
|
43
|
+
expect(text).toContain('via scope_project/poll -> pending');
|
|
44
|
+
expect(text).toContain('(next cursor: abc)');
|
|
45
|
+
});
|
|
46
|
+
it('reports an empty ledger', async () => {
|
|
47
|
+
const { client } = clientReturning({ rows: [], nextCursor: null });
|
|
48
|
+
expect(textOf(await makeAgentWakesHandler(client)({}))).toBe('(no wakes recorded)');
|
|
49
|
+
});
|
|
50
|
+
});
|
package/dist/tools/docs.js
CHANGED
|
@@ -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 body. Pass an empty string or omit for a blank page.'),
|
|
279
|
+
content: z.string().optional().describe('Markdown body (CommonMark + GFM). Pass an empty string or omit for a blank page. Images are inline elements; status pills use `:badge[LABEL]{tone=success}` (neutral/info/success/warning/danger/accent, unknown reads as 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.'),
|
|
@@ -315,7 +315,7 @@ export const updateDocToolConfig = {
|
|
|
315
315
|
inputSchema: z.object({
|
|
316
316
|
docId: z.string().min(1).describe('Doc UUID or human-readable doc key (ORB-D12 / DOC-5).'),
|
|
317
317
|
title: z.string().min(1).max(255).optional(),
|
|
318
|
-
content: z.string().optional().describe('Markdown body. Send the full new content; the API replaces, not appends.'),
|
|
318
|
+
content: z.string().optional().describe('Markdown body (CommonMark + GFM). Send the full new content; the API replaces, not appends. Images are inline elements; status pills use `:badge[LABEL]{tone=success}` (neutral/info/success/warning/danger/accent, unknown reads as neutral, label <= 24 chars).'),
|
|
319
319
|
parentDocId: z.string().uuid().nullable().optional(),
|
|
320
320
|
visibility: z.enum(['public', 'workspace', 'members', 'specific']).optional(),
|
|
321
321
|
icon: z.string().nullish(),
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
|
10
10
|
import { OrbotoClient } from '../orboto-client.js';
|
|
11
|
-
import { resolveProjectByKey, resolveTicketByKey, ticketLine, normalizeName, resolveByName } from './shared.js';
|
|
11
|
+
import { resolveProjectByKey, resolveTicketByKey, ticketLine, normalizeName, resolveByName, mcpProcessInstance, checkoutInstanceToken } from './shared.js';
|
|
12
12
|
beforeEach(() => { vi.restoreAllMocks(); });
|
|
13
13
|
afterEach(() => { vi.restoreAllMocks(); });
|
|
14
14
|
function sequence(responses) {
|
|
@@ -171,3 +171,9 @@ describe('ORB-2171 - the stdio proxy shares the checkout token with the CLI and
|
|
|
171
171
|
expect(checkoutInstanceToken({}, 'build-host', '/srv/other')).not.toBe('agent-e0b18afc9cec');
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
|
+
describe('mcpProcessInstance', () => {
|
|
175
|
+
it('is the checkout token this process derived at start', () => {
|
|
176
|
+
expect(mcpProcessInstance()).toBe(checkoutInstanceToken());
|
|
177
|
+
expect(mcpProcessInstance().length).toBeGreaterThan(0);
|
|
178
|
+
});
|
|
179
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orboto/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.202.0",
|
|
4
4
|
"description": "Model Context Protocol server for orboto - exposes tickets, projects, milestones, time, docs, primer-facts, alerts and absences as MCP tools for Claude Desktop / Cursor / Copilot Chat / any MCP-aware AI client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|