@kammcs/buildit-mcp 0.2.0 → 0.8.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/CHANGELOG.md +149 -0
- package/README.md +273 -32
- package/SECURITY.md +1 -1
- package/dist/api/client.js +5 -1
- package/dist/api/generated/operations.js +840 -11
- package/dist/api/generated/schemas.js +936 -3
- package/dist/api/generated/strict.js +1088 -3
- package/dist/capabilities.js +236 -0
- package/dist/channel/api.js +159 -0
- package/dist/channel/approvals.js +259 -0
- package/dist/channel/handoffs.js +190 -0
- package/dist/channel/item-events.js +164 -0
- package/dist/channel/lock.js +181 -0
- package/dist/channel/loop.js +1034 -0
- package/dist/channel/mask.js +365 -0
- package/dist/channel/threads.js +60 -0
- package/dist/channel/tool-ring.js +124 -0
- package/dist/channel/typing.js +150 -0
- package/dist/channel/waiters.js +229 -0
- package/dist/channel/workers.js +66 -0
- package/dist/cli/claude-config.js +100 -0
- package/dist/cli/commands.js +438 -0
- package/dist/config.js +356 -6
- package/dist/errors.js +49 -1
- package/dist/index.js +122 -6
- package/dist/log.js +3 -2
- package/dist/secrets/keychain.js +273 -0
- package/dist/server.js +61 -5
- package/dist/tools/approve.js +791 -0
- package/dist/tools/ask.js +898 -0
- package/dist/tools/chat.js +98 -1
- package/dist/tools/handoff.js +409 -0
- package/dist/tools/items-read.js +6 -3
- package/dist/tools/items-write.js +77 -16
- package/dist/tools/plans.js +221 -14
- package/dist/tools/reply.js +138 -0
- package/dist/tools/shared.js +24 -1
- package/dist/tools/threads.js +149 -0
- package/dist/tools/whoami.js +53 -3
- package/dist/toolsets/catalog.js +3 -1
- package/dist/toolsets/registry.js +2 -0
- package/dist/transports/stdio.js +288 -12
- package/package.json +5 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reply: answers the session's owner in buildIt.Social (channel mode only).
|
|
3
|
+
*
|
|
4
|
+
* The answer is posted as the agent, labelled with the session, in the
|
|
5
|
+
* agent's direct conversation with its owner, or in a work item's thread
|
|
6
|
+
* (in the item's project channel) when the session works on that item: in
|
|
7
|
+
* the thread named (or the item's, by key), else in the thread of the
|
|
8
|
+
* latest message, mention or assignment that arrived, else as a new
|
|
9
|
+
* message of the direct conversation. It is safe to retry: the
|
|
10
|
+
* idempotency key is the new message's id.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { postSessionMessage } from '../channel/api.js';
|
|
15
|
+
import { NOT_THE_CHANNEL } from '../channel/lock.js';
|
|
16
|
+
import { isUuid, resolveThread } from '../channel/threads.js';
|
|
17
|
+
import { ToolInputError } from '../errors.js';
|
|
18
|
+
import { defineTool } from '../toolsets/registry.js';
|
|
19
|
+
import { IdempotencyKeyInput, itemRef, ItemRefInput } from './shared.js';
|
|
20
|
+
export const REPLY_MAX_CHARS = 4000;
|
|
21
|
+
export const ReplyInputSchema = z.object({
|
|
22
|
+
text: z
|
|
23
|
+
.string()
|
|
24
|
+
.min(1)
|
|
25
|
+
.max(REPLY_MAX_CHARS)
|
|
26
|
+
.refine((s) => s.trim().length > 0, 'A reply cannot be blank.')
|
|
27
|
+
.describe(`The answer, in Markdown, at most ${REPLY_MAX_CHARS} characters. No images. Item keys such as DEMO-12 show as live cards.`),
|
|
28
|
+
thread_id: z
|
|
29
|
+
.uuid()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('The thread_id of the <channel> message you answer. Left out: the thread of the latest message, while only one thread is active; with several, give it (or item).'),
|
|
32
|
+
item: ItemRefInput.optional().describe("A work item's key (DEMO-12), to answer in its thread: one you work on (the <channel> tag's item). Instead of thread_id."),
|
|
33
|
+
reply_to_id: z
|
|
34
|
+
.uuid()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('A message_id to quote as the one you answer, inside the thread.'),
|
|
37
|
+
idempotency_key: IdempotencyKeyInput.optional(),
|
|
38
|
+
});
|
|
39
|
+
const ReplyOutputSchema = z.object({
|
|
40
|
+
created: z.boolean().describe('false: an earlier call with this idempotency_key posted it.'),
|
|
41
|
+
message: z.object({
|
|
42
|
+
id: z.string(),
|
|
43
|
+
conversation_id: z.string(),
|
|
44
|
+
thread_id: z.string().nullable(),
|
|
45
|
+
reply_to_id: z.string().nullable(),
|
|
46
|
+
created_at: z.string(),
|
|
47
|
+
}),
|
|
48
|
+
idempotency_key: z.string(),
|
|
49
|
+
});
|
|
50
|
+
/** The reply tool for one channel session. */
|
|
51
|
+
export function createReplyTool(state) {
|
|
52
|
+
return defineTool({
|
|
53
|
+
name: 'reply',
|
|
54
|
+
// Not in the catalog: listed only in channel mode, whatever the toolsets.
|
|
55
|
+
toolset: 'chat',
|
|
56
|
+
title: 'Reply in buildIt.Social',
|
|
57
|
+
description: `Answers the person who directs this session, in buildIt.Social: posts your text as the agent (labelled with this session) in its conversation with them, or in a work item's thread when the message came from one (meta item), where they read it on their computer or phone. Use it for every answer to a message that arrived as a <channel> tag: pass that tag's thread_id so the answer lands in the same thread (left out, it goes to the thread of the latest message, mention or assignment; with none yet, it starts a new message in your conversation with them). For an item assigned to you, post progress, questions and the result in its thread: pass its thread_id, or item with its key. You can post only in the threads of items you work on; everyone in the project's channel reads them. Markdown, at most ${REPLY_MAX_CHARS} characters; split longer answers. idempotency_key makes a retry return the first message instead of posting twice; one is generated and returned if you leave it out.`,
|
|
58
|
+
scopes: [],
|
|
59
|
+
annotations: {
|
|
60
|
+
title: 'Reply in buildIt.Social',
|
|
61
|
+
readOnlyHint: false,
|
|
62
|
+
destructiveHint: false,
|
|
63
|
+
idempotentHint: false,
|
|
64
|
+
openWorldHint: true,
|
|
65
|
+
},
|
|
66
|
+
inputSchema: ReplyInputSchema,
|
|
67
|
+
outputSchema: ReplyOutputSchema,
|
|
68
|
+
async run(args, ctx) {
|
|
69
|
+
if (!state.isChannel)
|
|
70
|
+
throw new ToolInputError(NOT_THE_CHANNEL);
|
|
71
|
+
const key = args.idempotency_key ?? randomUUID();
|
|
72
|
+
const threadId = await resolveThread(state, ctx, args, 'reply');
|
|
73
|
+
const r = await postSessionMessage(ctx.api, {
|
|
74
|
+
id: key,
|
|
75
|
+
text: args.text,
|
|
76
|
+
...(threadId === undefined ? {} : { thread_id: threadId }),
|
|
77
|
+
...(args.reply_to_id === undefined ? {} : { reply_to_id: args.reply_to_id }),
|
|
78
|
+
}, ctx.apiOptions);
|
|
79
|
+
state.typing?.stop(threadId);
|
|
80
|
+
const m = r.message;
|
|
81
|
+
const item = args.item === undefined ? undefined : itemRef(args.item).toUpperCase();
|
|
82
|
+
const where = m.thread_id
|
|
83
|
+
? item !== undefined && !isUuid(item)
|
|
84
|
+
? `in the thread of ${item} (${m.thread_id})`
|
|
85
|
+
: `in thread ${m.thread_id}`
|
|
86
|
+
: 'as a new message';
|
|
87
|
+
return {
|
|
88
|
+
structured: {
|
|
89
|
+
created: r.created,
|
|
90
|
+
message: {
|
|
91
|
+
id: m.id,
|
|
92
|
+
conversation_id: m.conversation_id,
|
|
93
|
+
thread_id: m.thread_id ?? null,
|
|
94
|
+
reply_to_id: m.reply_to_id ?? null,
|
|
95
|
+
created_at: m.created_at,
|
|
96
|
+
},
|
|
97
|
+
idempotency_key: key,
|
|
98
|
+
},
|
|
99
|
+
text: r.created
|
|
100
|
+
? `Sent ${where} (message ${m.id}).`
|
|
101
|
+
: `Nothing new: this reply was already sent ${where} with this idempotency_key (message ${m.id}).`,
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* turn_ended: called by the plugin's Stop hook when Claude's turn ends, so
|
|
108
|
+
* buildIt.Social stops showing that the agent is writing, and the main
|
|
109
|
+
* agent's tool approvals still open (the person answered "No" in the
|
|
110
|
+
* terminal, so the turn went on) close as no longer needed. Not for Claude.
|
|
111
|
+
*/
|
|
112
|
+
export function createTurnEndedTool(state) {
|
|
113
|
+
return defineTool({
|
|
114
|
+
name: 'turn_ended',
|
|
115
|
+
toolset: 'chat',
|
|
116
|
+
title: 'Turn ended (plugin hook)',
|
|
117
|
+
description: "Called by the buildit plugin's Stop hook when your turn ends; it tells buildIt.Social that the agent is no longer writing, and closes your tool approvals that no longer need an answer. Not for you to call: it does nothing you need.",
|
|
118
|
+
scopes: [],
|
|
119
|
+
runsAfterSessionEnd: true,
|
|
120
|
+
annotations: {
|
|
121
|
+
title: 'Turn ended (plugin hook)',
|
|
122
|
+
readOnlyHint: true,
|
|
123
|
+
destructiveHint: false,
|
|
124
|
+
idempotentHint: true,
|
|
125
|
+
openWorldHint: false,
|
|
126
|
+
},
|
|
127
|
+
inputSchema: z.object({}),
|
|
128
|
+
outputSchema: z.object({ stopped: z.number().describe('Threads that were working.') }),
|
|
129
|
+
async run() {
|
|
130
|
+
const working = state.typing?.working.length ?? 0;
|
|
131
|
+
// The main agent's open tool approvals are no longer needed.
|
|
132
|
+
state.approvals?.agentDone('');
|
|
133
|
+
await state.typing?.stopAll();
|
|
134
|
+
// The Stop hook reads the text as its output: an empty object lets the turn end.
|
|
135
|
+
return { structured: { stopped: working }, text: '{}' };
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
package/dist/tools/shared.js
CHANGED
|
@@ -66,6 +66,17 @@ export const ItemRefInput = z
|
|
|
66
66
|
.string()
|
|
67
67
|
.regex(ITEM_REF_RE, 'An item key such as DEMO-12, or an item uuid.')
|
|
68
68
|
.describe('An item key such as DEMO-12 (any case, a leading # is fine) or the item uuid.');
|
|
69
|
+
/** The agent API version that gives items Needs. */
|
|
70
|
+
export const NEEDS_API = '1.9.0';
|
|
71
|
+
/** An item's Needs: capability tags (the server lower-cases, sorts and folds duplicates). */
|
|
72
|
+
export const NeedsInput = z
|
|
73
|
+
.array(z
|
|
74
|
+
.string()
|
|
75
|
+
.trim()
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.regex(/^[a-z0-9][a-z0-9+._-]{0,31}$/, 'A capability tag such as macos (a-z 0-9 + . _ -).'))
|
|
78
|
+
.max(10)
|
|
79
|
+
.describe('Capability tags (windows, macos, linux, ios, android, xcode, flutter, docker, rust, web, gpu, or your own).');
|
|
69
80
|
export const ProjectKeyInput = z
|
|
70
81
|
.string()
|
|
71
82
|
.regex(/^[A-Za-z][A-Za-z0-9]{1,5}$/, 'A project key such as DEMO.')
|
|
@@ -134,8 +145,10 @@ export const ItemOutSchema = z.object({
|
|
|
134
145
|
status_category: z.string().describe('not_started, started, done or canceled.'),
|
|
135
146
|
priority: z.string(),
|
|
136
147
|
assignee: PersonSchema.nullable(),
|
|
148
|
+
assignee_agent: z.string().nullable().describe("The assignee's agent working on it."),
|
|
137
149
|
parent: z.string().nullable().describe("The parent's key."),
|
|
138
150
|
labels: z.array(z.string()),
|
|
151
|
+
needs: z.array(z.string()),
|
|
139
152
|
estimate: z.number().nullable(),
|
|
140
153
|
sprint: z.string().nullable(),
|
|
141
154
|
fix_release: z.string().nullable(),
|
|
@@ -157,8 +170,10 @@ export function itemOut(i) {
|
|
|
157
170
|
status_category: i.status.category,
|
|
158
171
|
priority: i.priority,
|
|
159
172
|
assignee: i.assignee ? person(i.assignee) : null,
|
|
173
|
+
assignee_agent: i.assignee_agent ? label(i.assignee_agent.name, PERSON_NAME_CHARS) : null,
|
|
160
174
|
parent: i.parent?.key ?? null,
|
|
161
175
|
labels: i.labels.map((l) => label(l.name, 40)),
|
|
176
|
+
needs: (i.needs ?? []).map((n) => label(n, 32)),
|
|
162
177
|
estimate: i.estimate,
|
|
163
178
|
sprint: nameLabel(i.sprint?.name),
|
|
164
179
|
fix_release: nameLabel(i.fix_release?.name),
|
|
@@ -427,6 +442,13 @@ export function historyOut(h) {
|
|
|
427
442
|
// ---------------------------------------------------------------------------
|
|
428
443
|
// Text
|
|
429
444
|
// ---------------------------------------------------------------------------
|
|
445
|
+
/** Who an item is assigned to, in lines: "@Sam", "assignee Claude · for Sam", or "unassigned". */
|
|
446
|
+
export function assigneeText(i) {
|
|
447
|
+
if (i.assignee_agent && i.assignee) {
|
|
448
|
+
return `assignee ${i.assignee_agent} · for ${i.assignee.name}`;
|
|
449
|
+
}
|
|
450
|
+
return i.assignee ? `@${i.assignee.name}` : 'unassigned';
|
|
451
|
+
}
|
|
430
452
|
/** One line about an item, for lists (it carries the title, so lists are wrapped). */
|
|
431
453
|
export function itemLine(i) {
|
|
432
454
|
const parts = [
|
|
@@ -434,12 +456,13 @@ export function itemLine(i) {
|
|
|
434
456
|
i.type,
|
|
435
457
|
`${i.status} (${i.status_category})`,
|
|
436
458
|
...(i.priority !== 'none' ? [i.priority] : []),
|
|
437
|
-
i
|
|
459
|
+
assigneeText(i),
|
|
438
460
|
...(i.estimate !== null ? [`est ${i.estimate}`] : []),
|
|
439
461
|
...(i.parent ? [`parent ${i.parent}`] : []),
|
|
440
462
|
...(i.sprint ? [i.sprint] : []),
|
|
441
463
|
...(i.due_date ? [`due ${i.due_date}`] : []),
|
|
442
464
|
...(i.labels.length > 0 ? [`labels ${i.labels.join(', ')}`] : []),
|
|
465
|
+
...(i.needs.length > 0 ? [`needs ${i.needs.join(', ')}`] : []),
|
|
443
466
|
];
|
|
444
467
|
return `${parts.join(' · ')} — ${i.title}`;
|
|
445
468
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list_threads: the threads this session works in (channel mode only):
|
|
3
|
+
* its direct-message threads and the work items' threads bound to it,
|
|
4
|
+
* newest activity first, at most 50 (GET /v1/session/threads).
|
|
5
|
+
*
|
|
6
|
+
* It also refreshes which item threads are active (the set the explicit
|
|
7
|
+
* thread rule counts), since the server's list is the one that knows.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { listSessionThreads } from '../channel/api.js';
|
|
11
|
+
import { NOT_THE_CHANNEL } from '../channel/lock.js';
|
|
12
|
+
import { ToolInputError } from '../errors.js';
|
|
13
|
+
import { defineTool } from '../toolsets/registry.js';
|
|
14
|
+
import { sanitizeLabel, wrapUntrusted } from '../untrusted.js';
|
|
15
|
+
/** Built-in statuses whose name doesn't follow from the key. */
|
|
16
|
+
const BUILT_IN_NAMES = { todo: 'To do', canceled: 'Cancelled' };
|
|
17
|
+
/** "in_review" as people read it: "In review". */
|
|
18
|
+
const humanize = (key) => {
|
|
19
|
+
const words = key.replace(/[_-]+/g, ' ').trim();
|
|
20
|
+
return words === '' ? '' : words.charAt(0).toUpperCase() + words.slice(1);
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* A status's name. Built-in statuses come with a key and no name (the app
|
|
24
|
+
* names them in the person's language), so the name follows from the key,
|
|
25
|
+
* else from the category.
|
|
26
|
+
*/
|
|
27
|
+
export function statusName(status) {
|
|
28
|
+
if (status === null)
|
|
29
|
+
return null;
|
|
30
|
+
if (status.name !== null && status.name.trim() !== '')
|
|
31
|
+
return sanitizeLabel(status.name, 60);
|
|
32
|
+
const key = status.key?.trim().toLowerCase() ?? '';
|
|
33
|
+
const named = key === '' ? undefined : (BUILT_IN_NAMES[key] ?? humanize(key));
|
|
34
|
+
const name = named !== undefined && named !== '' ? named : humanize(status.category);
|
|
35
|
+
return name === '' ? null : sanitizeLabel(name, 60);
|
|
36
|
+
}
|
|
37
|
+
const ThreadOutSchema = z.object({
|
|
38
|
+
thread_id: z.string(),
|
|
39
|
+
conversation_id: z.string(),
|
|
40
|
+
kind: z.string().describe('dm: your direct message with the person; item: a work item.'),
|
|
41
|
+
item: z
|
|
42
|
+
.object({
|
|
43
|
+
key: z.string(),
|
|
44
|
+
title: z.string().describe('People-written; flattened to one defused line.'),
|
|
45
|
+
status: z.string().nullable(),
|
|
46
|
+
})
|
|
47
|
+
.nullable(),
|
|
48
|
+
open_cards: z.number(),
|
|
49
|
+
last_owner_message_at: z.string().nullable(),
|
|
50
|
+
handoff: z
|
|
51
|
+
.object({
|
|
52
|
+
id: z.string().describe('The handoff id (withdraw_handoff and decline_handoff take it).'),
|
|
53
|
+
role: z
|
|
54
|
+
.string()
|
|
55
|
+
.describe('target: this item is a subtask handed to this session; asked: this session proposed handing on a subtask of this item.'),
|
|
56
|
+
status: z.string(),
|
|
57
|
+
})
|
|
58
|
+
.nullable()
|
|
59
|
+
.describe("The item thread's handoff, if it has one (null for a server older than agent API 1.9.0)."),
|
|
60
|
+
});
|
|
61
|
+
/** A thread's handoff in words, for its line. */
|
|
62
|
+
function handoffFact(h) {
|
|
63
|
+
return h.role === 'target'
|
|
64
|
+
? `a subtask handed to this session (handoff ${h.id}, ${h.status}); if you can't do it, decline_handoff`
|
|
65
|
+
: h.role === 'asked'
|
|
66
|
+
? `you proposed handing on a subtask of it (handoff ${h.id}, ${h.status})`
|
|
67
|
+
: `handoff ${h.id} (${h.role}, ${h.status})`;
|
|
68
|
+
}
|
|
69
|
+
/** The list_threads tool for one channel session. */
|
|
70
|
+
export function createListThreadsTool(state) {
|
|
71
|
+
return defineTool({
|
|
72
|
+
name: 'list_threads',
|
|
73
|
+
// Not in the catalog: listed only in channel mode, whatever the toolsets.
|
|
74
|
+
toolset: 'chat',
|
|
75
|
+
title: 'List the threads this session works in',
|
|
76
|
+
description: "Lists the threads this session works in: your direct message with the person, and the work items' threads (key, title, status, open cards, when the person last wrote). Use it to name the thread for reply, ask or an approval card (thread_id, or the item's key) while you work in several, and after a restart.",
|
|
77
|
+
scopes: [],
|
|
78
|
+
annotations: {
|
|
79
|
+
title: 'List the threads this session works in',
|
|
80
|
+
readOnlyHint: true,
|
|
81
|
+
destructiveHint: false,
|
|
82
|
+
idempotentHint: true,
|
|
83
|
+
openWorldHint: true,
|
|
84
|
+
},
|
|
85
|
+
inputSchema: z.object({}),
|
|
86
|
+
outputSchema: z.object({ threads: z.array(ThreadOutSchema) }),
|
|
87
|
+
async run(_args, ctx) {
|
|
88
|
+
if (!state.isChannel)
|
|
89
|
+
throw new ToolInputError(NOT_THE_CHANNEL);
|
|
90
|
+
const r = await listSessionThreads(ctx.api, ctx.apiOptions);
|
|
91
|
+
const threads = r.threads.map((t) => ({
|
|
92
|
+
thread_id: t.thread_id,
|
|
93
|
+
conversation_id: t.conversation_id,
|
|
94
|
+
kind: sanitizeLabel(t.kind, 20),
|
|
95
|
+
item: t.item === null
|
|
96
|
+
? null
|
|
97
|
+
: {
|
|
98
|
+
key: sanitizeLabel(t.item.key, 40),
|
|
99
|
+
title: sanitizeLabel(t.item.title, 300),
|
|
100
|
+
status: statusName(t.item.status),
|
|
101
|
+
},
|
|
102
|
+
open_cards: t.open_cards,
|
|
103
|
+
last_owner_message_at: t.last_owner_message_at,
|
|
104
|
+
handoff: t.handoff === null || t.handoff === undefined
|
|
105
|
+
? null
|
|
106
|
+
: {
|
|
107
|
+
id: t.handoff.id,
|
|
108
|
+
role: sanitizeLabel(t.handoff.role, 20),
|
|
109
|
+
status: sanitizeLabel(t.handoff.status, 20),
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
112
|
+
// The server's list is the one that knows which item threads are bound.
|
|
113
|
+
state.activeItems.clear();
|
|
114
|
+
for (const t of r.threads) {
|
|
115
|
+
if (t.kind === 'item' && t.item !== null)
|
|
116
|
+
state.activateItem(t.item.key, t.item.id);
|
|
117
|
+
}
|
|
118
|
+
const newestDm = r.threads.find((t) => t.kind === 'dm');
|
|
119
|
+
if (state.lastDmThread === undefined && newestDm !== undefined) {
|
|
120
|
+
state.lastDmThread = newestDm.thread_id;
|
|
121
|
+
}
|
|
122
|
+
if (threads.length === 0) {
|
|
123
|
+
return {
|
|
124
|
+
structured: { threads },
|
|
125
|
+
text: 'This session works in no thread yet.',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const lines = threads.map((t) => {
|
|
129
|
+
const facts = [
|
|
130
|
+
`thread_id ${t.thread_id}`,
|
|
131
|
+
`${t.open_cards} open ${t.open_cards === 1 ? 'card' : 'cards'}`,
|
|
132
|
+
`the person last wrote ${t.last_owner_message_at ?? 'never'}`,
|
|
133
|
+
...(t.handoff === null ? [] : [handoffFact(t.handoff)]),
|
|
134
|
+
].join(' · ');
|
|
135
|
+
return t.item === null
|
|
136
|
+
? `your direct message · ${facts}`
|
|
137
|
+
: `${t.item.key} · ${t.item.status ?? 'no status'} · ${facts} — ${t.item.title}`;
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
structured: { threads },
|
|
141
|
+
text: [
|
|
142
|
+
`This session works in ${threads.length} ${threads.length === 1 ? 'thread' : 'threads'}, newest activity first:`,
|
|
143
|
+
// The titles and statuses are people-written: the lines are one block.
|
|
144
|
+
wrapUntrusted(lines.join('\n'), { source: 'threads', maxChars: 40_000 }),
|
|
145
|
+
].join('\n'),
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
package/dist/tools/whoami.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* whoami: who the token acts as, where, and with what reach.
|
|
3
3
|
*/
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { getSession } from '../channel/api.js';
|
|
6
|
+
import { NOT_THE_CHANNEL } from '../channel/lock.js';
|
|
5
7
|
import { OPERATIONS } from '../api/generated/operations.js';
|
|
6
8
|
import { defineTool } from '../toolsets/registry.js';
|
|
7
9
|
import { expandScopes } from '../toolsets/toolsets.js';
|
|
@@ -24,7 +26,14 @@ const OutputSchema = z.object({
|
|
|
24
26
|
.describe('A test org (true); null when the API is older than 1.2.0.'),
|
|
25
27
|
}),
|
|
26
28
|
token: z.object({
|
|
27
|
-
name: z
|
|
29
|
+
name: z
|
|
30
|
+
.string()
|
|
31
|
+
.nullable()
|
|
32
|
+
.describe("The token's name; for an agent session, the session's label."),
|
|
33
|
+
kind: z
|
|
34
|
+
.string()
|
|
35
|
+
.nullable()
|
|
36
|
+
.describe('pat: a personal access token; session: an agent session (channel mode); null when the API is older.'),
|
|
28
37
|
expires_at: z.string().nullable(),
|
|
29
38
|
}),
|
|
30
39
|
scopes: z.array(z.string()).describe('As granted; a write scope also allows reading.'),
|
|
@@ -58,6 +67,17 @@ const OutputSchema = z.object({
|
|
|
58
67
|
.number()
|
|
59
68
|
.describe("The org's requests a minute, across all its tokens."),
|
|
60
69
|
}),
|
|
70
|
+
capabilities: z
|
|
71
|
+
.object({
|
|
72
|
+
confirmed: z
|
|
73
|
+
.array(z.string())
|
|
74
|
+
.describe('The tags the person confirmed: what work with Needs can be handed to this session for.'),
|
|
75
|
+
suggested: z
|
|
76
|
+
.array(z.string())
|
|
77
|
+
.describe('What this computer detected and suggests, not confirmed yet (they never count).'),
|
|
78
|
+
})
|
|
79
|
+
.nullable()
|
|
80
|
+
.describe("An agent session's capability tags; null for a personal token or an API older than 1.9.0."),
|
|
61
81
|
server: z.object({
|
|
62
82
|
name: z.string(),
|
|
63
83
|
version: z.string(),
|
|
@@ -122,6 +142,25 @@ export const whoamiTool = defineTool({
|
|
|
122
142
|
return undefined;
|
|
123
143
|
}),
|
|
124
144
|
]);
|
|
145
|
+
// An agent session's capability tags, read fresh (the person may have confirmed some since).
|
|
146
|
+
const session = ctx.channel !== undefined && me.token.kind === 'session'
|
|
147
|
+
? await getSession(ctx.api, options).then((info) => {
|
|
148
|
+
const channel = ctx.channel;
|
|
149
|
+
if (channel?.session?.session.id === info.session.id)
|
|
150
|
+
channel.session = info;
|
|
151
|
+
return info;
|
|
152
|
+
}, () => ctx.channel?.session)
|
|
153
|
+
: undefined;
|
|
154
|
+
const confirmed = session?.session.capabilities;
|
|
155
|
+
const detected = session?.session.capabilities_detected;
|
|
156
|
+
const capabilities = confirmed === undefined && detected === undefined
|
|
157
|
+
? null
|
|
158
|
+
: {
|
|
159
|
+
confirmed: (confirmed ?? []).map((t) => sanitizeLabel(t, 32)),
|
|
160
|
+
suggested: (detected ?? [])
|
|
161
|
+
.map((t) => sanitizeLabel(t, 32))
|
|
162
|
+
.filter((t) => !(confirmed ?? []).includes(t)),
|
|
163
|
+
};
|
|
125
164
|
const minVersion = meta?.min_mcp_version;
|
|
126
165
|
const updateRequired = minVersion !== undefined && compareVersions(SERVER_VERSION, minVersion) < 0;
|
|
127
166
|
const structured = {
|
|
@@ -137,6 +176,7 @@ export const whoamiTool = defineTool({
|
|
|
137
176
|
},
|
|
138
177
|
token: {
|
|
139
178
|
name: nullableLabel(me.token.name, NAME_CHARS),
|
|
179
|
+
kind: me.token.kind === undefined ? null : sanitizeLabel(me.token.kind, 20),
|
|
140
180
|
expires_at: me.token.expires_at,
|
|
141
181
|
},
|
|
142
182
|
scopes: [...me.token.scopes].sort(),
|
|
@@ -151,6 +191,7 @@ export const whoamiTool = defineTool({
|
|
|
151
191
|
id: p.id,
|
|
152
192
|
})),
|
|
153
193
|
features: { projects: me.features.projects },
|
|
194
|
+
capabilities,
|
|
154
195
|
rate_limits: {
|
|
155
196
|
requests_per_minute: me.rate_limits.requests_per_minute,
|
|
156
197
|
user_requests_per_minute: me.rate_limits.user_requests_per_minute,
|
|
@@ -176,7 +217,9 @@ export const whoamiTool = defineTool({
|
|
|
176
217
|
const names = [
|
|
177
218
|
`User: ${s.user.name ?? s.user.id}${s.user.email ? ` (${s.user.email})` : ''}`,
|
|
178
219
|
`Org: ${s.org.name ?? s.org.id}`,
|
|
179
|
-
...(s.token.name
|
|
220
|
+
...(s.token.name
|
|
221
|
+
? [`${s.token.kind === 'session' ? 'Session' : 'Token'}: ${s.token.name}`]
|
|
222
|
+
: []),
|
|
180
223
|
];
|
|
181
224
|
if (s.projects.length > 0) {
|
|
182
225
|
const shown = s.projects
|
|
@@ -188,7 +231,7 @@ export const whoamiTool = defineTool({
|
|
|
188
231
|
names.push(`Channels the token is limited to: ${s.limits.channels.join(', ')}`);
|
|
189
232
|
}
|
|
190
233
|
const lines = [
|
|
191
|
-
`Signed in to buildIt.Social with a personal access token${s.token.expires_at ? ` (expires ${s.token.expires_at})` : ''}. Who and where, as people named them:`,
|
|
234
|
+
`Signed in to buildIt.Social with ${s.token.kind === 'session' ? "an agent session's token (it acts as the session's owner)" : 'a personal access token'}${s.token.expires_at ? ` (expires ${s.token.expires_at})` : ''}. Who and where, as people named them:`,
|
|
192
235
|
wrapLines(names, 'names'),
|
|
193
236
|
`Scopes: ${s.scopes.length > 0 ? s.scopes.join(', ') : 'none'}.`,
|
|
194
237
|
`Limits: ${s.limits.projects === null
|
|
@@ -199,6 +242,10 @@ export const whoamiTool = defineTool({
|
|
|
199
242
|
? 'no channels'
|
|
200
243
|
: `only the ${s.limits.channels.length} channel(s) listed above, among those the user can see`}.`,
|
|
201
244
|
];
|
|
245
|
+
if (s.capabilities !== null) {
|
|
246
|
+
const { confirmed: mine, suggested } = s.capabilities;
|
|
247
|
+
lines.push(`Capabilities of this session: ${mine.length > 0 ? mine.join(', ') : 'none confirmed'}${suggested.length > 0 ? `; suggested, not confirmed: ${suggested.join(', ')}` : ''}. Only confirmed ones count: work that needs others goes to another of the person's sessions (propose_handoff), or the person confirms them in buildIt.Social (Settings → Agents).`);
|
|
248
|
+
}
|
|
202
249
|
if (s.org.test === true) {
|
|
203
250
|
lines.push('This org is a test org: its data may be wiped and seeded by test tools.');
|
|
204
251
|
}
|
|
@@ -222,6 +269,9 @@ export const whoamiTool = defineTool({
|
|
|
222
269
|
if (s.server.not_served !== null && s.server.not_served.length > 0) {
|
|
223
270
|
lines.push(`This buildIt.Social server (API ${s.server.api_version ?? 'unknown'}) doesn't serve these operations yet, so calls that need them are refused until it is updated: ${s.server.not_served.join(', ')}.`);
|
|
224
271
|
}
|
|
272
|
+
if (ctx.channel !== undefined && !ctx.channel.isChannel) {
|
|
273
|
+
lines.push(NOT_THE_CHANNEL);
|
|
274
|
+
}
|
|
225
275
|
if (updateRequired) {
|
|
226
276
|
lines.push(`This buildit-mcp (${SERVER_VERSION}) is older than the API supports (${minVersion} or newer). Ask the person to update it.`);
|
|
227
277
|
}
|
package/dist/toolsets/catalog.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { PROMPTS } from '../prompts.js';
|
|
7
7
|
import { RESOURCES } from '../resources.js';
|
|
8
|
-
import { listChannelsTool, readChannelTool, readThreadTool } from '../tools/chat.js';
|
|
8
|
+
import { listChannelsTool, listEmojiTool, listPinsTool, readChannelTool, readThreadTool, } from '../tools/chat.js';
|
|
9
9
|
import { addCommentTool, listCommentsTool } from '../tools/comments.js';
|
|
10
10
|
import { getItemTool, searchItemsTool } from '../tools/items-read.js';
|
|
11
11
|
import { assignItemTool, createItemTool, linkItemsTool, rankItemTool, transitionItemTool, unlinkItemsTool, updateItemTool, } from '../tools/items-write.js';
|
|
@@ -49,6 +49,8 @@ export const CATALOG = [
|
|
|
49
49
|
listChannelsTool,
|
|
50
50
|
readChannelTool,
|
|
51
51
|
readThreadTool,
|
|
52
|
+
listPinsTool,
|
|
53
|
+
listEmojiTool,
|
|
52
54
|
// admin
|
|
53
55
|
createChannelTool,
|
|
54
56
|
createProjectTool,
|
|
@@ -13,6 +13,8 @@ export function createToolContext(init) {
|
|
|
13
13
|
...(init.signal ? { signal: init.signal } : {}),
|
|
14
14
|
apiOptions,
|
|
15
15
|
call: (id, args = {}) => callOperation(init.api, id, args, apiOptions),
|
|
16
|
+
...(init.channel ? { channel: init.channel } : {}),
|
|
17
|
+
...(init.progress ? { progress: init.progress } : {}),
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
/** Identity helper that keeps the argument and output types tied to the schemas. */
|