@hualinge/relay-mcp-server 0.1.3

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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/collab.d.ts +8 -0
  4. package/dist/collab.d.ts.map +1 -0
  5. package/dist/collab.js +47 -0
  6. package/dist/collab.js.map +1 -0
  7. package/dist/dare.d.ts +4 -0
  8. package/dist/dare.d.ts.map +1 -0
  9. package/dist/dare.js +46 -0
  10. package/dist/dare.js.map +1 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +47 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/memory.d.ts +8 -0
  16. package/dist/memory.d.ts.map +1 -0
  17. package/dist/memory.js +47 -0
  18. package/dist/memory.js.map +1 -0
  19. package/dist/server-toolsets.d.ts +13 -0
  20. package/dist/server-toolsets.d.ts.map +1 -0
  21. package/dist/server-toolsets.js +119 -0
  22. package/dist/server-toolsets.js.map +1 -0
  23. package/dist/tools/callback-memory-tools.d.ts +46 -0
  24. package/dist/tools/callback-memory-tools.d.ts.map +1 -0
  25. package/dist/tools/callback-memory-tools.js +59 -0
  26. package/dist/tools/callback-memory-tools.js.map +1 -0
  27. package/dist/tools/callback-outbox.d.ts +15 -0
  28. package/dist/tools/callback-outbox.d.ts.map +1 -0
  29. package/dist/tools/callback-outbox.js +162 -0
  30. package/dist/tools/callback-outbox.js.map +1 -0
  31. package/dist/tools/callback-retry.d.ts +14 -0
  32. package/dist/tools/callback-retry.d.ts.map +1 -0
  33. package/dist/tools/callback-retry.js +58 -0
  34. package/dist/tools/callback-retry.js.map +1 -0
  35. package/dist/tools/callback-tools.d.ts +444 -0
  36. package/dist/tools/callback-tools.d.ts.map +1 -0
  37. package/dist/tools/callback-tools.js +771 -0
  38. package/dist/tools/callback-tools.js.map +1 -0
  39. package/dist/tools/evidence-tools.d.ts +36 -0
  40. package/dist/tools/evidence-tools.d.ts.map +1 -0
  41. package/dist/tools/evidence-tools.js +87 -0
  42. package/dist/tools/evidence-tools.js.map +1 -0
  43. package/dist/tools/file-tools.d.ts +78 -0
  44. package/dist/tools/file-tools.d.ts.map +1 -0
  45. package/dist/tools/file-tools.js +178 -0
  46. package/dist/tools/file-tools.js.map +1 -0
  47. package/dist/tools/index.d.ts +13 -0
  48. package/dist/tools/index.d.ts.map +1 -0
  49. package/dist/tools/index.js +18 -0
  50. package/dist/tools/index.js.map +1 -0
  51. package/dist/tools/limb-tools.d.ts +113 -0
  52. package/dist/tools/limb-tools.d.ts.map +1 -0
  53. package/dist/tools/limb-tools.js +120 -0
  54. package/dist/tools/limb-tools.js.map +1 -0
  55. package/dist/tools/reflect-tools.d.ts +23 -0
  56. package/dist/tools/reflect-tools.d.ts.map +1 -0
  57. package/dist/tools/reflect-tools.js +52 -0
  58. package/dist/tools/reflect-tools.js.map +1 -0
  59. package/dist/tools/rich-block-rules-tool.d.ts +17 -0
  60. package/dist/tools/rich-block-rules-tool.d.ts.map +1 -0
  61. package/dist/tools/rich-block-rules-tool.js +36 -0
  62. package/dist/tools/rich-block-rules-tool.js.map +1 -0
  63. package/dist/tools/schedule-tools.d.ts +134 -0
  64. package/dist/tools/schedule-tools.d.ts.map +1 -0
  65. package/dist/tools/schedule-tools.js +397 -0
  66. package/dist/tools/schedule-tools.js.map +1 -0
  67. package/dist/tools/session-chain-tools.d.ts +99 -0
  68. package/dist/tools/session-chain-tools.d.ts.map +1 -0
  69. package/dist/tools/session-chain-tools.js +288 -0
  70. package/dist/tools/session-chain-tools.js.map +1 -0
  71. package/dist/utils/index.d.ts +6 -0
  72. package/dist/utils/index.d.ts.map +1 -0
  73. package/dist/utils/index.js +11 -0
  74. package/dist/utils/index.js.map +1 -0
  75. package/dist/utils/path-utils.d.ts +6 -0
  76. package/dist/utils/path-utils.d.ts.map +1 -0
  77. package/dist/utils/path-utils.js +63 -0
  78. package/dist/utils/path-utils.js.map +1 -0
  79. package/dist/utils/path-validator.d.ts +45 -0
  80. package/dist/utils/path-validator.d.ts.map +1 -0
  81. package/dist/utils/path-validator.js +126 -0
  82. package/dist/utils/path-validator.js.map +1 -0
  83. package/package.json +44 -0
@@ -0,0 +1,771 @@
1
+ /*
2
+ * *
3
+ * * Copyright (C) Huawei Technologies Co., Ltd. 2026. All rights reserved.
4
+ *
5
+ */
6
+ /**
7
+ * MCP Callback Tools — core callbacks
8
+ * 鉴权: process.env OFFICE_CLAW_INVOCATION_ID + OFFICE_CLAW_CALLBACK_TOKEN
9
+ */
10
+ import { randomUUID } from 'node:crypto';
11
+ import { z } from 'zod';
12
+ import { sendCallbackRequest } from './callback-outbox.js';
13
+ import { errorResult, successResult } from './file-tools.js';
14
+ const VALID_RICH_BLOCK_KINDS = new Set([
15
+ 'card',
16
+ 'diff',
17
+ 'checklist',
18
+ 'media_gallery',
19
+ 'audio',
20
+ 'interactive',
21
+ 'html_widget',
22
+ 'file',
23
+ ]);
24
+ function normalizeRichBlock(raw) {
25
+ if (!raw || typeof raw !== 'object')
26
+ return raw;
27
+ const obj = raw;
28
+ const rawType = obj['type'];
29
+ if (typeof rawType === 'string' && !('kind' in obj) && VALID_RICH_BLOCK_KINDS.has(rawType)) {
30
+ obj['kind'] = rawType;
31
+ delete obj['type'];
32
+ }
33
+ if (!('v' in obj) && 'kind' in obj) {
34
+ obj['v'] = 1;
35
+ }
36
+ return obj;
37
+ }
38
+ export function getCallbackConfig() {
39
+ const apiUrl = process.env['OFFICE_CLAW_API_URL'];
40
+ const invocationId = process.env['OFFICE_CLAW_INVOCATION_ID'];
41
+ const callbackToken = process.env['OFFICE_CLAW_CALLBACK_TOKEN'];
42
+ if (!apiUrl || !invocationId || !callbackToken)
43
+ return null;
44
+ return { apiUrl, invocationId, callbackToken };
45
+ }
46
+ export const NO_CONFIG_ERROR = 'OfficeClaw callback not configured. Missing OFFICE_CLAW_API_URL, OFFICE_CLAW_INVOCATION_ID, or OFFICE_CLAW_CALLBACK_TOKEN environment variables.';
47
+ // ============ HTTP helpers ============
48
+ export async function callbackPost(path, body, options) {
49
+ const config = getCallbackConfig();
50
+ if (!config)
51
+ return errorResult(NO_CONFIG_ERROR);
52
+ const requestBody = {
53
+ invocationId: config.invocationId,
54
+ callbackToken: config.callbackToken,
55
+ ...body,
56
+ };
57
+ const result = await sendCallbackRequest({ apiUrl: config.apiUrl, path, body: requestBody }, { enableOutbox: options?.enableOutbox === true });
58
+ if (result.ok)
59
+ return successResult(JSON.stringify(result.data));
60
+ return errorResult(result.error);
61
+ }
62
+ export async function callbackGet(path, params) {
63
+ const config = getCallbackConfig();
64
+ if (!config)
65
+ return errorResult(NO_CONFIG_ERROR);
66
+ const query = new URLSearchParams({
67
+ invocationId: config.invocationId,
68
+ callbackToken: config.callbackToken,
69
+ ...params,
70
+ });
71
+ try {
72
+ const response = await fetch(`${config.apiUrl}${path}?${query.toString()}`);
73
+ if (!response.ok) {
74
+ const text = await response.text();
75
+ return errorResult(`Callback failed (${response.status}): ${text}`);
76
+ }
77
+ return successResult(JSON.stringify(await response.json()));
78
+ }
79
+ catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ return errorResult(`Callback request failed: ${message}`);
82
+ }
83
+ }
84
+ export const postMessageInputSchema = {
85
+ content: z.string().min(1).describe('The message content to post'),
86
+ replyTo: z.string().optional().describe('Optional message ID to reply to'),
87
+ clientMessageId: z
88
+ .string()
89
+ .min(1)
90
+ .max(200)
91
+ .optional()
92
+ .describe('Optional idempotency key for at-least-once delivery de-duplication'),
93
+ targetAgents: z
94
+ .array(z.string().min(1))
95
+ .optional()
96
+ .describe('Optional explicit target agent IDs (e.g. ["codex","gpt52"]). Merged with @mentions parsed from content. Used for direction rendering in frontend.'),
97
+ };
98
+ const postMessageRuntimeInputSchema = z.object({
99
+ content: z.string().trim().min(1).max(50000),
100
+ replyTo: z.string().trim().min(1).optional(),
101
+ clientMessageId: z.string().trim().min(1).max(200).optional(),
102
+ targetAgents: z.array(z.string().trim().min(1)).optional(),
103
+ });
104
+ function normalizeOptionalString(value) {
105
+ if (typeof value !== 'string')
106
+ return value;
107
+ const trimmed = value.trim();
108
+ return trimmed.length > 0 ? trimmed : undefined;
109
+ }
110
+ function normalizeTargetAgents(value) {
111
+ if (Array.isArray(value)) {
112
+ return value
113
+ .filter((entry) => typeof entry === 'string')
114
+ .map((entry) => entry.trim())
115
+ .filter((entry) => entry.length > 0);
116
+ }
117
+ if (typeof value === 'string') {
118
+ const trimmed = value.trim();
119
+ return trimmed.length > 0 ? [trimmed] : undefined;
120
+ }
121
+ return value;
122
+ }
123
+ function normalizePostMessageInput(input) {
124
+ const rawContent = typeof input.content === 'string' ? input.content : (input.message ?? input.text ?? input.content);
125
+ const normalizedContent = typeof rawContent === 'string' ? rawContent.trim() : rawContent;
126
+ return {
127
+ content: normalizedContent,
128
+ replyTo: normalizeOptionalString(input.replyTo),
129
+ clientMessageId: normalizeOptionalString(input.clientMessageId),
130
+ targetAgents: normalizeTargetAgents(input.targetAgents),
131
+ };
132
+ }
133
+ export const getPendingMentionsInputSchema = {
134
+ includeAcked: z
135
+ .boolean()
136
+ .optional()
137
+ .describe('When true, include acknowledged mentions for explicit history review.'),
138
+ };
139
+ export const ackMentionsInputSchema = {
140
+ upToMessageId: z
141
+ .string()
142
+ .min(1)
143
+ .describe('The message ID up to which mentions have been processed. Must be within the last fetched pending window.'),
144
+ };
145
+ export const getThreadContextInputSchema = {
146
+ limit: z
147
+ .number()
148
+ .int()
149
+ .min(1)
150
+ .max(200)
151
+ .optional()
152
+ .default(20)
153
+ .describe('Number of recent messages to retrieve (default: 20)'),
154
+ threadId: z
155
+ .string()
156
+ .min(1)
157
+ .optional()
158
+ .describe('Optional: read messages from a different thread. Omit to read the current thread.'),
159
+ agentId: z.string().min(1).optional().describe("Optional: filter by speaker agentId, or pass 'user' for human messages."),
160
+ keyword: z
161
+ .string()
162
+ .min(1)
163
+ .optional()
164
+ .describe('Optional: filter messages whose content contains this keyword (case-insensitive).'),
165
+ };
166
+ export const listThreadsInputSchema = {
167
+ limit: z.number().int().min(1).max(200).optional().default(20).describe('Max threads to return (default: 20).'),
168
+ activeSince: z
169
+ .number()
170
+ .int()
171
+ .min(0)
172
+ .optional()
173
+ .describe('Optional Unix timestamp in ms; only include threads active at/after this time.'),
174
+ keyword: z
175
+ .string()
176
+ .trim()
177
+ .min(1)
178
+ .max(200)
179
+ .optional()
180
+ .describe('Optional: filter threads whose title or threadId contains this keyword (case-insensitive).'),
181
+ };
182
+ export const featIndexInputSchema = {
183
+ limit: z
184
+ .number()
185
+ .int()
186
+ .min(1)
187
+ .max(100)
188
+ .optional()
189
+ .default(20)
190
+ .describe('Max feature entries to return (default: 20, max: 100).'),
191
+ featId: z.string().min(1).optional().describe('Optional exact feature ID match (case-insensitive), e.g. F043.'),
192
+ query: z
193
+ .string()
194
+ .min(1)
195
+ .optional()
196
+ .describe('Optional fuzzy substring search over featId/name/status (case-insensitive).'),
197
+ };
198
+ export const updateTaskInputSchema = {
199
+ taskId: z.string().min(1).describe('The ID of the task to update'),
200
+ status: z.enum(['todo', 'doing', 'blocked', 'done']).optional().describe('New task status'),
201
+ why: z.string().max(1000).optional().describe('Optional note explaining the status change'),
202
+ };
203
+ export const crossPostMessageInputSchema = {
204
+ threadId: z.string().min(1).describe('Target thread ID to post into'),
205
+ content: z.string().min(1).describe('The message content to post'),
206
+ replyTo: z.string().optional().describe('Optional message ID to reply to'),
207
+ clientMessageId: z
208
+ .string()
209
+ .min(1)
210
+ .max(200)
211
+ .optional()
212
+ .describe('Optional idempotency key for at-least-once delivery de-duplication'),
213
+ };
214
+ export const listTasksInputSchema = {
215
+ threadId: z.string().min(1).optional().describe('Optional thread ID filter'),
216
+ agentId: z.string().min(1).optional().describe('Optional owner agentId filter'),
217
+ status: z.enum(['todo', 'doing', 'blocked', 'done']).optional().describe('Optional task status filter'),
218
+ };
219
+ export const listSkillsInputSchema = {
220
+ query: z
221
+ .string()
222
+ .min(1)
223
+ .max(200)
224
+ .optional()
225
+ .describe('Optional substring filter over skill name, description, category, and triggers.'),
226
+ limit: z
227
+ .number()
228
+ .int()
229
+ .min(1)
230
+ .max(200)
231
+ .optional()
232
+ .describe('Maximum number of skills to return. Omit to return all matches.'),
233
+ };
234
+ export const loadSkillInputSchema = {
235
+ name: z.string().min(1).max(200).describe('Exact skill name to load, e.g. "tdd" or "workspace-navigator".'),
236
+ };
237
+ export async function handlePostMessage(input) {
238
+ const normalizedInput = normalizePostMessageInput(input);
239
+ const parsedInput = postMessageRuntimeInputSchema.safeParse(normalizedInput);
240
+ if (!parsedInput.success) {
241
+ const issues = parsedInput.error.issues
242
+ .map((issue) => `${issue.path.join('.') || 'input'}: ${issue.message}`)
243
+ .join('; ');
244
+ return errorResult(`Invalid input for office_claw_post_message: ${issues}`);
245
+ }
246
+ const validatedInput = parsedInput.data;
247
+ const result = await callbackPost('/api/callbacks/post-message', {
248
+ content: validatedInput.content,
249
+ ...(validatedInput.replyTo ? { replyTo: validatedInput.replyTo } : {}),
250
+ clientMessageId: validatedInput.clientMessageId ?? randomUUID(),
251
+ ...(validatedInput.targetAgents?.length ? { targetAgents: validatedInput.targetAgents } : {}),
252
+ }, { enableOutbox: true });
253
+ return finalizePostMessageResult(result, validatedInput.content);
254
+ }
255
+ function finalizePostMessageResult(result, content) {
256
+ // Detect stale_ignored: server returned 200 but message was NOT delivered
257
+ // because a newer invocation for the same thread+agent has superseded this one.
258
+ // The CLI must know this so it doesn't assume the message reached the user.
259
+ if (!result.isError) {
260
+ try {
261
+ const data = JSON.parse(result.content[0].text);
262
+ if (data?.status === 'stale_ignored') {
263
+ return errorResult('Message was NOT delivered: this invocation has been superseded by a newer one for the same thread. ' +
264
+ 'Your message was silently discarded by the server (stale_ignored). ' +
265
+ 'Include the message content in your stdout response instead.');
266
+ }
267
+ }
268
+ catch {
269
+ // parse failure is fine — means result is not a stale_ignored response
270
+ }
271
+ }
272
+ // If post-message failed and content contains @mentions,
273
+ // hint that text-based @mention is always available.
274
+ // Only mention credential issues when the error actually looks like auth failure.
275
+ if (result.isError && /[@@]/.test(content)) {
276
+ const original = result.content[0].text;
277
+ const lower = original.toLowerCase();
278
+ const looksLikeCredentialFailure = lower.includes('callback failed (401)') ||
279
+ lower.includes('invalid or expired callback credentials') ||
280
+ lower.includes('callback token');
281
+ const reasonHint = looksLikeCredentialFailure
282
+ ? '这次 callback 凭证校验失败(可能是 token 过期,也可能 invocation/token 不匹配)。'
283
+ : '这次 post-message 调用失败。';
284
+ const hint = `\n\n💡 Tip: ${reasonHint}如果你想 @其他智能体,` +
285
+ '不需要用这个 MCP tool——直接在你的回复文本里另起一行写 @智能体名 即可' +
286
+ '(例如另起一行写 @队友名),系统会自动检测并触发。';
287
+ return errorResult(original + hint);
288
+ }
289
+ return result;
290
+ }
291
+ export async function handleGetPendingMentions(input) {
292
+ return callbackGet('/api/callbacks/pending-mentions', {
293
+ ...(input.includeAcked ? { includeAcked: '1' } : {}),
294
+ });
295
+ }
296
+ export async function handleAckMentions(input) {
297
+ return callbackPost('/api/callbacks/ack-mentions', {
298
+ upToMessageId: input.upToMessageId,
299
+ });
300
+ }
301
+ export async function handleGetThreadContext(input) {
302
+ const normalizedThreadId = input.threadId?.trim();
303
+ return callbackGet('/api/callbacks/thread-context', {
304
+ ...(input.limit ? { limit: String(input.limit) } : {}),
305
+ ...(normalizedThreadId && normalizedThreadId !== '.' && normalizedThreadId !== './'
306
+ ? { threadId: normalizedThreadId }
307
+ : {}),
308
+ ...(input.agentId ? { agentId: input.agentId } : {}),
309
+ ...(input.keyword ? { keyword: input.keyword } : {}),
310
+ });
311
+ }
312
+ export async function handleListThreads(input) {
313
+ return callbackGet('/api/callbacks/list-threads', {
314
+ ...(input.limit ? { limit: String(input.limit) } : {}),
315
+ ...(input.activeSince !== undefined ? { activeSince: String(input.activeSince) } : {}),
316
+ ...(input.keyword ? { keyword: input.keyword } : {}),
317
+ });
318
+ }
319
+ export async function handleFeatIndex(input) {
320
+ return callbackGet('/api/callbacks/feat-index', {
321
+ ...(input.limit ? { limit: String(input.limit) } : {}),
322
+ ...(input.featId ? { featId: input.featId } : {}),
323
+ ...(input.query ? { query: input.query } : {}),
324
+ });
325
+ }
326
+ export async function handleUpdateTask(input) {
327
+ return callbackPost('/api/callbacks/update-task', {
328
+ taskId: input.taskId,
329
+ ...(input.status ? { status: input.status } : {}),
330
+ ...(input.why ? { why: input.why } : {}),
331
+ });
332
+ }
333
+ export async function handleCrossPostMessage(input) {
334
+ const result = await callbackPost('/api/callbacks/post-message', {
335
+ threadId: input.threadId,
336
+ allowCrossThread: true,
337
+ content: input.content,
338
+ ...(input.replyTo ? { replyTo: input.replyTo } : {}),
339
+ clientMessageId: input.clientMessageId ?? randomUUID(),
340
+ }, { enableOutbox: true });
341
+ return finalizePostMessageResult(result, input.content);
342
+ }
343
+ export async function handleListTasks(input) {
344
+ return callbackGet('/api/callbacks/list-tasks', {
345
+ ...(input.threadId ? { threadId: input.threadId } : {}),
346
+ ...(input.agentId ? { agentId: input.agentId } : {}),
347
+ ...(input.status ? { status: input.status } : {}),
348
+ });
349
+ }
350
+ export async function handleListSkills(input) {
351
+ return callbackGet('/api/callbacks/skills/list', {
352
+ ...(input.query ? { query: input.query } : {}),
353
+ ...(input.limit ? { limit: String(input.limit) } : {}),
354
+ });
355
+ }
356
+ export async function handleLoadSkill(input) {
357
+ return callbackGet('/api/callbacks/skills/load', {
358
+ name: input.name,
359
+ });
360
+ }
361
+ /** F22+F96: Create a rich block (card, diff, checklist, media_gallery, audio, interactive) in the current message */
362
+ export const createRichBlockInputSchema = {
363
+ block: z
364
+ .string()
365
+ .min(1)
366
+ .describe('JSON string of the rich block object. Must include id, kind, v:1, and kind-specific fields.'),
367
+ };
368
+ /**
369
+ * #84: Route A → Route B fallback for rich block creation.
370
+ * Tries direct callback first; on failure, falls back to post_message with cc_rich text
371
+ * (which is extracted server-side after #83 fix).
372
+ */
373
+ export async function handleCreateRichBlock(input) {
374
+ let parsed;
375
+ try {
376
+ parsed = JSON.parse(input.block);
377
+ }
378
+ catch {
379
+ return errorResult('Invalid JSON in block parameter');
380
+ }
381
+ // #85 M2c: normalize before validation (type→kind, auto v:1)
382
+ parsed = normalizeRichBlock(parsed);
383
+ if (!parsed || typeof parsed !== 'object' || !('id' in parsed) || !('kind' in parsed)) {
384
+ return errorResult('Block must include id and kind fields');
385
+ }
386
+ // Route A: direct rich block callback (buffers for invocation response)
387
+ const result = await callbackPost('/api/callbacks/create-rich-block', {
388
+ block: parsed,
389
+ }, { enableOutbox: true });
390
+ if (!result.isError)
391
+ return result;
392
+ // P1 cloud-review: only fallback to Route B for auth/config failures.
393
+ // Validation errors (400/422) must surface directly, not be silently swallowed.
394
+ const errorText = result.content[0]?.type === 'text' ? result.content[0].text : '';
395
+ const isAuthOrConfigFailure = /\(40[13]\)/.test(errorText) || /not configured/i.test(errorText);
396
+ if (!isAuthOrConfigFailure)
397
+ return result;
398
+ // Route A auth/config failed — try Route B: cc_rich text via post_message (#83 extracts it server-side)
399
+ const ccRichText = `\`\`\`cc_rich\n${JSON.stringify({ v: 1, blocks: [parsed] })}\n\`\`\``;
400
+ const fallback = await handlePostMessage({
401
+ content: ccRichText,
402
+ clientMessageId: randomUUID(),
403
+ });
404
+ if (!fallback.isError) {
405
+ return successResult(JSON.stringify({ status: 'ok', route: 'B_fallback' }));
406
+ }
407
+ // Both routes failed — return error with embeddable cc_rich hint
408
+ return errorResult(`Rich block creation failed (callback token expired or missing). As a workaround, include this in your message text:\n\n${ccRichText}`);
409
+ }
410
+ export const requestPermissionInputSchema = {
411
+ action: z.string().min(1).describe('The action requiring permission (e.g. "git_commit", "file_delete")'),
412
+ reason: z.string().min(1).describe('Why you need this permission'),
413
+ context: z.string().max(5000).optional().describe('Optional additional context for the request'),
414
+ };
415
+ export const checkPermissionStatusInputSchema = {
416
+ requestId: z.string().min(1).describe('The requestId returned from a previous request_permission call'),
417
+ };
418
+ export async function handleRequestPermission(input) {
419
+ return callbackPost('/api/callbacks/request-permission', {
420
+ action: input.action,
421
+ reason: input.reason,
422
+ ...(input.context ? { context: input.context } : {}),
423
+ });
424
+ }
425
+ export async function handleCheckPermissionStatus(input) {
426
+ return callbackGet('/api/callbacks/permission-status', {
427
+ requestId: input.requestId,
428
+ });
429
+ }
430
+ // TD091: PR tracking registration — server resolves threadId from invocation record
431
+ export const registerPrTrackingInputSchema = {
432
+ repoFullName: z.string().min(1).describe('Repository full name in owner/repo format (e.g. "zts212653/office-claw")'),
433
+ prNumber: z.number().int().positive().describe('PR number'),
434
+ agentId: z
435
+ .string()
436
+ .optional()
437
+ .describe('Deprecated — server auto-resolves from invocation identity. Ignored if provided.'),
438
+ };
439
+ export async function handleRegisterPrTracking(input) {
440
+ return callbackPost('/api/callbacks/register-pr-tracking', {
441
+ repoFullName: input.repoFullName,
442
+ prNumber: input.prNumber,
443
+ ...(input.agentId ? { agentId: input.agentId } : {}),
444
+ });
445
+ }
446
+ export const updateWorkflowInputSchema = {
447
+ backlogItemId: z.string().min(1).describe('The backlog item ID to update workflow SOP for'),
448
+ featureId: z.string().min(1).describe('Feature ID (e.g. "F073")'),
449
+ stage: z
450
+ .enum(['kickoff', 'impl', 'quality_gate', 'review', 'merge', 'completion'])
451
+ .optional()
452
+ .describe('Current SOP stage'),
453
+ batonHolder: z
454
+ .string()
455
+ .min(1)
456
+ .optional()
457
+ .describe('Unique handle of the agent currently holding the baton (e.g. "opus", "codex")'),
458
+ nextSkill: z
459
+ .string()
460
+ .nullable()
461
+ .optional()
462
+ .describe('Suggested skill to load next (e.g. "tdd", "quality-gate"), or null'),
463
+ resumeCapsule: z
464
+ .object({
465
+ goal: z.string().optional().describe('What we are building'),
466
+ done: z.array(z.string()).optional().describe('What has been completed'),
467
+ currentFocus: z.string().optional().describe('What we are working on right now'),
468
+ })
469
+ .optional()
470
+ .describe('Resume capsule for cold start / context recovery'),
471
+ checks: z
472
+ .object({
473
+ remoteMainSynced: z.enum(['attested', 'verified', 'unknown']).optional(),
474
+ qualityGatePassed: z.enum(['attested', 'verified', 'unknown']).optional(),
475
+ reviewApproved: z.enum(['attested', 'verified', 'unknown']).optional(),
476
+ visionGuardDone: z.enum(['attested', 'verified', 'unknown']).optional(),
477
+ })
478
+ .optional()
479
+ .describe('SOP checkpoint attestations'),
480
+ expectedVersion: z
481
+ .number()
482
+ .int()
483
+ .optional()
484
+ .describe('CAS: reject if current version does not match (for concurrent update safety)'),
485
+ };
486
+ export async function handleUpdateWorkflow(input) {
487
+ const body = {
488
+ backlogItemId: input.backlogItemId,
489
+ featureId: input.featureId,
490
+ };
491
+ if (input.stage !== undefined)
492
+ body['stage'] = input.stage;
493
+ if (input.batonHolder !== undefined)
494
+ body['batonHolder'] = input.batonHolder;
495
+ if (input.nextSkill !== undefined)
496
+ body['nextSkill'] = input.nextSkill;
497
+ if (input.resumeCapsule !== undefined)
498
+ body['resumeCapsule'] = input.resumeCapsule;
499
+ if (input.checks !== undefined)
500
+ body['checks'] = input.checks;
501
+ if (input.expectedVersion !== undefined)
502
+ body['expectedVersion'] = input.expectedVersion;
503
+ return callbackPost('/api/callbacks/update-workflow-sop', body);
504
+ }
505
+ // ============ Multi-Mention (F086) ============
506
+ export const multiMentionInputSchema = {
507
+ targets: z
508
+ .array(z.string().min(1))
509
+ .min(1)
510
+ .max(3)
511
+ .describe('Agent IDs to invoke in parallel (max 3). Use the agent IDs from your system prompt teammate list. Example: ["assistant","office"]'),
512
+ question: z.string().min(1).max(5000).describe('The question or request for the target agents'),
513
+ callbackTo: z.string().min(1).describe('Agent ID to route all responses back to (required, usually yourself)'),
514
+ context: z.string().max(5000).optional().describe('Additional context to include for the targets'),
515
+ idempotencyKey: z
516
+ .string()
517
+ .min(1)
518
+ .max(200)
519
+ .optional()
520
+ .describe('Idempotency key to prevent duplicate dispatches within the same thread'),
521
+ timeoutMinutes: z.number().int().min(3).max(20).optional().describe('Timeout in minutes (default 8, range 3-20)'),
522
+ searchEvidenceRefs: z
523
+ .array(z.string())
524
+ .optional()
525
+ .describe('References to searches you performed before calling this tool (required unless overrideReason provided). Enforces "先搜后问" principle.'),
526
+ overrideReason: z
527
+ .string()
528
+ .min(1)
529
+ .max(500)
530
+ .optional()
531
+ .describe('Why you are skipping search evidence (required if searchEvidenceRefs omitted)'),
532
+ triggerType: z
533
+ .enum(['high-impact', 'cross-domain', 'uncertain', 'info-gap', 'recon'])
534
+ .optional()
535
+ .describe('Which meta-thinking trigger motivated this call'),
536
+ };
537
+ export const dispatchAgentTaskInputSchema = {
538
+ target: z
539
+ .string()
540
+ .min(1)
541
+ .describe('Target agent name, alias, or agentId. Prefer the customer-visible agent name.'),
542
+ task: z.string().min(1).max(10000).describe('Task content to dispatch to the target agent'),
543
+ awaitResponse: z
544
+ .boolean()
545
+ .optional()
546
+ .describe('When true, wait for the target agent to finish and return its response (default: true).'),
547
+ timeoutMs: z
548
+ .number()
549
+ .int()
550
+ .min(1000)
551
+ .max(300000)
552
+ .optional()
553
+ .describe('How long to wait for the target agent response before returning a timeout error.'),
554
+ idempotencyKey: z
555
+ .string()
556
+ .min(1)
557
+ .max(200)
558
+ .optional()
559
+ .describe('Optional idempotency key to deduplicate repeated dispatch requests within the same thread.'),
560
+ };
561
+ export async function handleMultiMention(input) {
562
+ // Client-side validation: searchEvidenceRefs or overrideReason required
563
+ if (!input.searchEvidenceRefs?.length && !input.overrideReason) {
564
+ return errorResult('multi_mention requires searchEvidenceRefs (what did you search first?) ' +
565
+ 'or overrideReason (why are you skipping search?). ' +
566
+ 'This enforces the "先搜后问" principle — search before asking.');
567
+ }
568
+ return callbackPost('/api/callbacks/multi-mention', {
569
+ targets: input.targets,
570
+ question: input.question,
571
+ callbackTo: input.callbackTo,
572
+ ...(input.context ? { context: input.context } : {}),
573
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
574
+ ...(input.timeoutMinutes !== undefined ? { timeoutMinutes: input.timeoutMinutes } : {}),
575
+ ...(input.searchEvidenceRefs ? { searchEvidenceRefs: input.searchEvidenceRefs } : {}),
576
+ ...(input.overrideReason ? { overrideReason: input.overrideReason } : {}),
577
+ ...(input.triggerType ? { triggerType: input.triggerType } : {}),
578
+ });
579
+ }
580
+ export async function handleDispatchAgentTask(input) {
581
+ const config = getCallbackConfig();
582
+ if (!config)
583
+ return errorResult(NO_CONFIG_ERROR);
584
+ const result = await sendCallbackRequest({
585
+ apiUrl: config.apiUrl,
586
+ path: '/api/callbacks/dispatch-agent-task',
587
+ body: {
588
+ invocationId: config.invocationId,
589
+ callbackToken: config.callbackToken,
590
+ target: input.target,
591
+ task: input.task,
592
+ ...(input.awaitResponse !== undefined ? { awaitResponse: input.awaitResponse } : {}),
593
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
594
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
595
+ },
596
+ }, { enableOutbox: false });
597
+ if (!result.ok)
598
+ return errorResult(result.error);
599
+ const body = result.data;
600
+ if (!body || typeof body !== 'object') {
601
+ return errorResult('Dispatch agent task returned an invalid response payload.');
602
+ }
603
+ if (body.ok === false) {
604
+ return errorResult(`Dispatch agent task failed (${body.errorCode ?? 'unknown_error'}): ${body.message ?? 'unknown error'}`);
605
+ }
606
+ return successResult(JSON.stringify(body));
607
+ }
608
+ export const callbackTools = [
609
+ {
610
+ name: 'office_claw_dispatch_agent_task',
611
+ description: 'Reliably dispatch a task to another agent by customer-visible name and optionally wait for its response. ' +
612
+ 'Use this when the task must actually be queued and tracked, not just notified. ' +
613
+ 'This avoids stale callback loss from post_message-style handoff. ' +
614
+ 'When awaitResponse=true, the target agent should return its evaluation or revision guidance and finish, ' +
615
+ 'instead of synchronously dispatching back to the caller inside the same invocation. ' +
616
+ 'For multi-round review-and-revise workflows, the caller should inspect the returned result, ' +
617
+ 'revise the artifact if needed, and then start the next round explicitly. ' +
618
+ 'GOTCHA: Treat the returned structured status as the source of truth; do not assume success just because the tool was called.',
619
+ inputSchema: dispatchAgentTaskInputSchema,
620
+ handler: handleDispatchAgentTask,
621
+ },
622
+ {
623
+ name: 'office_claw_post_message',
624
+ description: 'Post a proactive async message to the OfficeClaw chat mid-task in the CURRENT thread (e.g. progress updates, sharing results). ' +
625
+ 'To simply @mention another agent at the end of your response, use @agent-name in your reply text instead — it is free and never expires. ' +
626
+ 'GOTCHA: This tool uses callback credentials that expire — if it fails with 401, fall back to inline @mention in your response text. ' +
627
+ 'GOTCHA: Do NOT use this for routine replies — only for mid-task proactive messages when you need to share something before your response completes.',
628
+ inputSchema: postMessageInputSchema,
629
+ handler: handlePostMessage,
630
+ },
631
+ {
632
+ name: 'office_claw_get_pending_mentions',
633
+ description: 'Get recent messages that @-mention you. Use at session start to check if anyone is trying to get your attention. ' +
634
+ 'TIP: Call this early in your session, then call ack_mentions after processing to avoid seeing the same mentions next session.',
635
+ inputSchema: getPendingMentionsInputSchema,
636
+ handler: handleGetPendingMentions,
637
+ },
638
+ {
639
+ name: 'office_claw_ack_mentions',
640
+ description: 'Acknowledge that you have processed mentions up to a specific message ID. ' +
641
+ 'Call this AFTER processing mentions from get_pending_mentions to avoid seeing them again in future sessions. ' +
642
+ 'GOTCHA: Pass the message ID of the LAST mention you processed, not the first.',
643
+ inputSchema: ackMentionsInputSchema,
644
+ handler: handleAckMentions,
645
+ },
646
+ {
647
+ name: 'office_claw_get_thread_context',
648
+ description: 'Get recent conversation messages for context. Use to understand what has been discussed recently in a thread. ' +
649
+ 'Pass threadId to read a DIFFERENT thread (cross-thread context); omit to read the current thread. ' +
650
+ 'Use keyword filter to find specific topics without reading all messages. ' +
651
+ 'TIP: For searching across ALL threads/sessions, use search_evidence instead — this tool only reads one thread.',
652
+ inputSchema: getThreadContextInputSchema,
653
+ handler: handleGetThreadContext,
654
+ },
655
+ // D15: office_claw_search_messages removed — superseded by search_evidence + get_thread_context
656
+ {
657
+ name: 'office_claw_list_threads',
658
+ description: 'List thread summaries for discovery. Use when you need to find a thread by keyword or see recent activity. ' +
659
+ 'Returns thread IDs, titles, and activity timestamps. ' +
660
+ 'Use activeSince (Unix ms) to filter to recently active threads. Use keyword to search by title.',
661
+ inputSchema: listThreadsInputSchema,
662
+ handler: handleListThreads,
663
+ },
664
+ {
665
+ name: 'office_claw_feat_index',
666
+ description: 'Lookup feature index entries by featId or query. Returns featId, name, status, and linked threadIds. ' +
667
+ 'Use when you need to find which thread(s) a feature is discussed in, or check feature status. ' +
668
+ 'PARAM GUIDE: featId = exact match (e.g. "F043"), query = fuzzy substring over all fields.',
669
+ inputSchema: featIndexInputSchema,
670
+ handler: handleFeatIndex,
671
+ },
672
+ {
673
+ name: 'office_claw_cross_post_message',
674
+ description: 'Post a message to a specific thread by threadId (cross-thread notification). ' +
675
+ 'Use when you need to notify a different thread about something relevant. ' +
676
+ 'GOTCHA: Requires threadId — use list_threads or feat_index to find the right thread first.',
677
+ inputSchema: crossPostMessageInputSchema,
678
+ handler: handleCrossPostMessage,
679
+ },
680
+ {
681
+ name: 'office_claw_list_tasks',
682
+ description: 'List tasks with optional threadId/agentId/status filters for global task discovery. ' +
683
+ 'Use when you need to see what tasks exist, who owns them, or what is blocked. ' +
684
+ 'TIP: Filter by status="blocked" to find tasks that need attention.',
685
+ inputSchema: listTasksInputSchema,
686
+ handler: handleListTasks,
687
+ },
688
+ {
689
+ name: 'office_claw_list_skills',
690
+ description: 'List OfficeClaw shared skills that are currently installed for runtime use. ' +
691
+ 'Use when you need to discover which skills exist, search by intent, or answer "what skills are available?". ' +
692
+ 'For planning/TDD/compare-options/worktree tasks, use this before search_evidence/grep/read and load a close match immediately. ' +
693
+ 'Shared ACP/open-agent skills are discovered here at runtime — do not assume a local skill directory exists. ' +
694
+ 'If an intent query is empty, retry once with a shorter intent phrase or a likely exact skill name.',
695
+ inputSchema: listSkillsInputSchema,
696
+ handler: handleListSkills,
697
+ },
698
+ {
699
+ name: 'office_claw_load_skill',
700
+ description: 'Load one OfficeClaw shared skill by exact name. ' +
701
+ 'Returns the full SKILL.md plus the skill directory and related file paths. ' +
702
+ 'Call this before using a skill; ACP/open agents should not assume the skill is preinstalled locally.',
703
+ inputSchema: loadSkillInputSchema,
704
+ handler: handleLoadSkill,
705
+ },
706
+ {
707
+ name: 'office_claw_update_task',
708
+ description: 'Update the status of a task you own. Use to mark tasks as doing/blocked/done. ' +
709
+ 'GOTCHA: You can only update tasks assigned to you (your agentId). ' +
710
+ 'TIP: Include a "why" note when marking as blocked — it helps others understand the situation.',
711
+ inputSchema: updateTaskInputSchema,
712
+ handler: handleUpdateTask,
713
+ },
714
+ {
715
+ name: 'office_claw_create_rich_block',
716
+ description: 'Create a rich block (card, diff, checklist, media_gallery, audio, or interactive) attached to the current message. ' +
717
+ 'Use card for status/decisions, diff for code changes, checklist for todos, media_gallery for images, audio for voice, interactive for user selection/confirmation. ' +
718
+ 'GOTCHA: The block JSON must use "kind" (NOT "type") and include "v": 1 and a unique "id". ' +
719
+ "GOTCHA: Call get_rich_block_rules first if you haven't loaded the full schema yet in this session. " +
720
+ 'If callback auth fails, falls back to cc_rich text encoding automatically.',
721
+ inputSchema: createRichBlockInputSchema,
722
+ handler: handleCreateRichBlock,
723
+ },
724
+ {
725
+ name: 'office_claw_request_permission',
726
+ description: 'Request permission from the user before performing a sensitive action (e.g. git_commit, file_delete). ' +
727
+ 'Returns granted/denied immediately if a rule exists, or pending with a requestId if the user needs to approve. ' +
728
+ 'WORKFLOW: request_permission → if pending → wait → check_permission_status with the returned requestId.',
729
+ inputSchema: requestPermissionInputSchema,
730
+ handler: handleRequestPermission,
731
+ },
732
+ {
733
+ name: 'office_claw_check_permission_status',
734
+ description: 'Check the status of a previously submitted permission request. ' +
735
+ 'Use the requestId returned from request_permission. Returns granted/denied/pending.',
736
+ inputSchema: checkPermissionStatusInputSchema,
737
+ handler: handleCheckPermissionStatus,
738
+ },
739
+ {
740
+ name: 'office_claw_register_pr_tracking',
741
+ description: 'Register a PR for email review notification routing. Call right after `gh pr create` ' +
742
+ 'so that cloud Codex review emails are automatically routed to your current thread. ' +
743
+ 'The server resolves threadId and agentId from your invocation identity — you only need repoFullName and prNumber. ' +
744
+ 'GOTCHA: Must be called in the same session that created the PR, while callback credentials are still valid.',
745
+ inputSchema: registerPrTrackingInputSchema,
746
+ handler: handleRegisterPrTracking,
747
+ },
748
+ {
749
+ name: 'office_claw_update_workflow',
750
+ description: 'Update the SOP workflow stage for a Feature (Mission Hub bulletin board). ' +
751
+ 'Use to record current stage, baton holder, resume capsule, and checks. ' +
752
+ 'This is information sharing, not flow control — cats decide their own actions. ' +
753
+ 'STAGE VALUES: kickoff → impl → quality_gate → review → merge → completion. ' +
754
+ 'TIP: Always set resumeCapsule when updating stage — it helps the next agent cold-start.',
755
+ inputSchema: updateWorkflowInputSchema,
756
+ handler: handleUpdateWorkflow,
757
+ },
758
+ {
759
+ name: 'office_claw_multi_mention',
760
+ description: 'Invoke up to 3 agents in parallel to gather perspectives on a question. ' +
761
+ 'targets must use agent IDs (e.g. "assistant", "office", "agentteams"), NOT display names. ' +
762
+ 'All responses are automatically routed back to callbackTo (usually yourself). ' +
763
+ "REQUIRES: searchEvidenceRefs (list what you searched first) OR overrideReason (why you're skipping search). " +
764
+ 'This enforces the "先搜后问" principle — always search before asking other agents. ' +
765
+ 'Use this instead of multiple @mentions when you need structured multi-agent collaboration with guaranteed response aggregation. ' +
766
+ 'GOTCHA: callbackTo is usually your own agent ID so responses come back to you.',
767
+ inputSchema: multiMentionInputSchema,
768
+ handler: handleMultiMention,
769
+ },
770
+ ];
771
+ //# sourceMappingURL=callback-tools.js.map