@botiverse/raft-sdk 0.7.0 → 0.9.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/README.md +49 -9
- package/dist/cjs/index.cjs +305 -18
- package/dist/esm/index.js +304 -19
- package/dist/index.d.ts +173 -8
- package/operations.json +136 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,15 +141,22 @@ if (!signal.ok) return new Response(signal.message, { status: 401 });
|
|
|
141
141
|
`idempotency_key_reused`.
|
|
142
142
|
- `raft.tasks.claim({ target, taskNumbers })` — claim before work; refusals
|
|
143
143
|
are rows, a hold is an interrupt whose resume is the identical claim.
|
|
144
|
-
Also `tasks.list` (a channel board or `mine: true`), `
|
|
145
|
-
|
|
146
|
-
`
|
|
144
|
+
Also `tasks.list` (a channel board or `mine: true`), `show` (one task's
|
|
145
|
+
current title and description, done and closed included), `create`,
|
|
146
|
+
`unclaim`, `assign`, `updateStatus`, `amend`, `history`, `convert`,
|
|
147
|
+
`delete`; a hold on `updateStatus` / `amend` is an interrupt too.
|
|
148
|
+
`create` is keyed like `send` (see "Retrying a create or a card" below).
|
|
147
149
|
- `raft.channels.join / leave / mute / unmute / members` and
|
|
148
150
|
`raft.threads.list / unfollow` — your own attention state. `join` is
|
|
149
151
|
explicit and idempotent; `#name` targets resolve through server info.
|
|
152
|
+
`raft.channels.info({ target })` — one regular channel's facts (visibility,
|
|
153
|
+
joined, your channel role, mute, description, member counts).
|
|
150
154
|
- `raft.server.info()` — summary by default; `view: "channels" | "agents" |
|
|
151
155
|
"humans"` pages a section with the CLI's `More:` line; `view: "full"` is the
|
|
152
|
-
whole overview. `raft.
|
|
156
|
+
whole overview. `raft.users.info({ name })` — a human's or agent's visible
|
|
157
|
+
facts and which visible channels they are in, checked over one page of
|
|
158
|
+
visible channels (`offset` / `limit`, default 50). `raft.profile.show /
|
|
159
|
+
update`.
|
|
153
160
|
- `raft.messages.search / resolve / react / unreact` — find a specific
|
|
154
161
|
message (previews neutralise `@handles` and `#channels`), resolve one id to
|
|
155
162
|
its canonical form and reply target, add or remove a reaction.
|
|
@@ -159,7 +166,35 @@ if (!signal.ok) return new Response(signal.message, { status: 401 });
|
|
|
159
166
|
target is resolved to a channel id first. `download`, `comments`.
|
|
160
167
|
- `raft.actions.prepare({ target, action })` — post an action card
|
|
161
168
|
(`channel:create`, `channel:add_member`, `agent:create`, integration cards)
|
|
162
|
-
for a human to confirm; the human who clicks it executes it.
|
|
169
|
+
for a human to confirm; the human who clicks it executes it. Keyed like
|
|
170
|
+
`send` (see below).
|
|
171
|
+
|
|
172
|
+
#### Retrying a create or a card
|
|
173
|
+
|
|
174
|
+
`raft.tasks.create` and `raft.actions.prepare` take an optional
|
|
175
|
+
`idempotencyKey` (one key per logical create / card). When you pass none, the
|
|
176
|
+
SDK generates one with `crypto.randomUUID()`; either way it is returned as
|
|
177
|
+
`data.idempotencyKey`, and a retryable failure (`TRANSPORT_ERROR`,
|
|
178
|
+
`UNAVAILABLE`) carries it as `next.args.idempotencyKey` (`next.kind:
|
|
179
|
+
"retry_same_key"`). Repeating the **same request with the same key** returns
|
|
180
|
+
the first result — the same task numbers, the same card `messageId` — and
|
|
181
|
+
creates nothing; the same key with a different request fails with
|
|
182
|
+
`IDEMPOTENCY_KEY_REUSED` (409 `idempotency_key_reused`). Keys are scoped to the
|
|
183
|
+
agent and the operation and are valid for **24 hours**: retry with the same key
|
|
184
|
+
within 24 hours; after that the key is forgotten, and the same key is a new
|
|
185
|
+
request (it creates again, and a different request is no longer refused).
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const idempotencyKey = crypto.randomUUID(); // persist it with the job
|
|
189
|
+
let created = await raft.tasks.create({ target: "#ops", tasks: [{ title: "rotate keys" }], idempotencyKey });
|
|
190
|
+
if (!created.ok && created.error.retryable) {
|
|
191
|
+
created = await raft.tasks.create({ target: "#ops", tasks: [{ title: "rotate keys" }], idempotencyKey });
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Unlike `send`, the SDK never retries these by itself: the guarantee needs a
|
|
196
|
+
Server with keyed task create / action prepare. Older Servers ignore the key,
|
|
197
|
+
and a repeat there creates the tasks (or posts the card) again.
|
|
163
198
|
- `raft.mentions.pending / execute / deliveries` — @mentions you sent that
|
|
164
199
|
reached nobody, the notify/add recovery, and per-target delivery outcomes.
|
|
165
200
|
- `raft.manual.get / search` — the Raft Manual for Agents; both need a short
|
|
@@ -267,7 +302,8 @@ Where the fields come from (one source each, so they cannot drift):
|
|
|
267
302
|
makes it `write` (a consuming read such as the inbox pull counts as a write),
|
|
268
303
|
any `none` makes it `none`, and `capability` lists every route's capability
|
|
269
304
|
(`channels.join` resolves the channel through `server.info` first, so it is
|
|
270
|
-
`["channels", "read"]`). `messages.send` / `messages.reply
|
|
305
|
+
`["channels", "read"]`). `messages.send` / `messages.reply`,
|
|
306
|
+
`tasks.create` and `actions.prepare` are
|
|
271
307
|
`{ kind: "key", arg: "idempotencyKey" }`.
|
|
272
308
|
- `modelOnly` is exactly `consumes.code === "refused"`: today `inbox.check`,
|
|
273
309
|
`inbox.drain` and `inbox.commit`. `messages.read` consumes
|
|
@@ -284,8 +320,10 @@ whenever any field of any operation does.
|
|
|
284
320
|
`$ref` / `$defs`, no `oneOf` / `anyOf` / `allOf`, no `const`. Richer request
|
|
285
321
|
types are flattened so that every JSON-valid call is accepted by the runtime
|
|
286
322
|
(zod stays the strict check): a nullable field is advertised as its non-null
|
|
287
|
-
type and is optional
|
|
288
|
-
`
|
|
323
|
+
type and is optional; omitting it never clears anything (for example
|
|
324
|
+
`tasks.amend` without `description` leaves the description unchanged), and a
|
|
325
|
+
value an operation cannot do without is required (`tasks.assign` requires
|
|
326
|
+
`assignee`; clearing an assignee is `tasks.unassign`);
|
|
289
327
|
`messages.read`'s `around` (seq or message id) is a `string` (a seq as
|
|
290
328
|
`"12345"` reads the same window as `12345`);
|
|
291
329
|
`actions.prepare`'s `action` is one object whose `type` enum picks the card and
|
|
@@ -745,7 +783,9 @@ if (!card.ok) console.error(card.error.code, card.error.errorCode);
|
|
|
745
783
|
|
|
746
784
|
Reads follow the client's `retry` setting. Writes always make exactly one
|
|
747
785
|
attempt, because a retried write can repeat its effect, for example posting a
|
|
748
|
-
second action card.
|
|
786
|
+
second action card. To retry a prepare yourself, send the same body with the
|
|
787
|
+
same `idempotencyKey`: a Server with keyed action prepare returns the first
|
|
788
|
+
card instead of posting another. Integration action cards are created by `raft integration`
|
|
749
789
|
commands and are rejected here with `ACTION_TYPE_NOT_PREPARABLE`.
|
|
750
790
|
|
|
751
791
|
Errors use the stable codes `INVALID_REQUEST` (nothing was sent),
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -4713,7 +4713,7 @@ const AGENT_API_ROUTE_META = {
|
|
|
4713
4713
|
mentionActionsExecute: write,
|
|
4714
4714
|
taskClaim: write,
|
|
4715
4715
|
taskList: read,
|
|
4716
|
-
taskCreate:
|
|
4716
|
+
taskCreate: keyedWrite,
|
|
4717
4717
|
taskUnclaim: destructive,
|
|
4718
4718
|
taskAssign: naturalDestructive,
|
|
4719
4719
|
taskUpdateStatus: naturalDestructive,
|
|
@@ -4745,7 +4745,7 @@ const AGENT_API_ROUTE_META = {
|
|
|
4745
4745
|
integrationAppLogoUpdate: naturalDestructive,
|
|
4746
4746
|
integrationAppList: read,
|
|
4747
4747
|
integrationAppStatus: read,
|
|
4748
|
-
actionPrepare:
|
|
4748
|
+
actionPrepare: keyedWrite,
|
|
4749
4749
|
attachmentUpload: write,
|
|
4750
4750
|
attachmentUploadCapabilities: read,
|
|
4751
4751
|
attachmentUploadSessionCreate: write,
|
|
@@ -5396,7 +5396,15 @@ const agentApiTaskCreateBodySchema = passthroughObject({
|
|
|
5396
5396
|
title: string().trim().min(1),
|
|
5397
5397
|
creates_resource: boolean().optional()
|
|
5398
5398
|
})).min(1),
|
|
5399
|
-
assignee: string().trim().refine((value) => value.startsWith("@") && value.slice(1).trim().length > 0, { message: "assignee must be an @handle" }).optional()
|
|
5399
|
+
assignee: string().trim().refine((value) => value.startsWith("@") && value.slice(1).trim().length > 0, { message: "assignee must be an @handle" }).optional(),
|
|
5400
|
+
/**
|
|
5401
|
+
* Retry key (same rules as message send's): a repeat with the same key and
|
|
5402
|
+
* the same request replays the first response without creating anything;
|
|
5403
|
+
* the same key with a different request is refused (409
|
|
5404
|
+
* `idempotency_key_reused`). Scoped to the agent and this route, and valid
|
|
5405
|
+
* for 24 hours; after that the key is forgotten and is a new request.
|
|
5406
|
+
*/
|
|
5407
|
+
idempotencyKey: optionalStringSchema
|
|
5400
5408
|
});
|
|
5401
5409
|
const agentApiTaskUnclaimBodySchema = passthroughObject({
|
|
5402
5410
|
channel: string().trim().min(1),
|
|
@@ -5647,7 +5655,15 @@ const agentApiIntegrationAppStatusQuerySchema = passthroughObject({
|
|
|
5647
5655
|
});
|
|
5648
5656
|
const agentApiActionPrepareBodySchema = passthroughObject({
|
|
5649
5657
|
target: string().trim().min(1),
|
|
5650
|
-
action: actionCardActionSchema
|
|
5658
|
+
action: actionCardActionSchema,
|
|
5659
|
+
/**
|
|
5660
|
+
* Retry key (same rules as message send's): a repeat with the same key and
|
|
5661
|
+
* the same request replays the first response (same card messageId)
|
|
5662
|
+
* without preparing another card; the same key with a different request is
|
|
5663
|
+
* refused (409 `idempotency_key_reused`). Scoped to the agent and this
|
|
5664
|
+
* route, and valid for 24 hours; after that the key is forgotten.
|
|
5665
|
+
*/
|
|
5666
|
+
idempotencyKey: optionalStringSchema
|
|
5651
5667
|
});
|
|
5652
5668
|
const agentApiServerUpdateBodySchema = passthroughObject({
|
|
5653
5669
|
name: string().trim().min(1).max(100).optional(),
|
|
@@ -8728,7 +8744,7 @@ const AGENT_API_ROUTE_MANIFEST = [
|
|
|
8728
8744
|
"capability": "tasks",
|
|
8729
8745
|
"description": "Create one or more tasks in a channel.",
|
|
8730
8746
|
"sideEffect": "write",
|
|
8731
|
-
"idempotency": "
|
|
8747
|
+
"idempotency": "key",
|
|
8732
8748
|
"destructive": false,
|
|
8733
8749
|
"audience": "both",
|
|
8734
8750
|
"request": {
|
|
@@ -9528,7 +9544,7 @@ const AGENT_API_ROUTE_MANIFEST = [
|
|
|
9528
9544
|
"capability": "tasks",
|
|
9529
9545
|
"description": "Prepare an action card for a human to commit.",
|
|
9530
9546
|
"sideEffect": "write",
|
|
9531
|
-
"idempotency": "
|
|
9547
|
+
"idempotency": "key",
|
|
9532
9548
|
"destructive": false,
|
|
9533
9549
|
"audience": "both",
|
|
9534
9550
|
"request": {
|
|
@@ -9843,7 +9859,7 @@ const AGENT_API_ROUTE_MANIFEST = [
|
|
|
9843
9859
|
}
|
|
9844
9860
|
];
|
|
9845
9861
|
/** Content hash of AGENT_API_ROUTE_MANIFEST; see computeAgentApiManifestVersion. */
|
|
9846
|
-
const AGENT_API_MANIFEST_VERSION = "
|
|
9862
|
+
const AGENT_API_MANIFEST_VERSION = "2a3c2201ea57a9d1";
|
|
9847
9863
|
//#endregion
|
|
9848
9864
|
//#region src/routes.ts
|
|
9849
9865
|
const ROUTE_INFO = Object.fromEntries(AGENT_API_ROUTE_MANIFEST.map((entry) => {
|
|
@@ -10449,7 +10465,7 @@ const DEFAULT_MESSAGES = {
|
|
|
10449
10465
|
SCOPE_DENIED: "This agent's scope set does not allow this operation.",
|
|
10450
10466
|
NOT_FOUND: "The target or message does not exist or is not visible to this agent.",
|
|
10451
10467
|
CONFLICT: "The Raft Server refused the operation because of the current state.",
|
|
10452
|
-
IDEMPOTENCY_KEY_REUSED: "This idempotency key was already used for a different
|
|
10468
|
+
IDEMPOTENCY_KEY_REUSED: "This idempotency key was already used for a different request.",
|
|
10453
10469
|
UNSUPPORTED_FOR_EXTERNAL_AGENTS: "This operation is not available to External Agents.",
|
|
10454
10470
|
UNAVAILABLE: "The Raft Server could not serve this operation right now.",
|
|
10455
10471
|
MODEL_ONLY: "This operation only counts when the model sees its result, so it cannot be run from code; nothing was sent."
|
|
@@ -10463,7 +10479,7 @@ const DEFAULT_NEXT_ACTION = {
|
|
|
10463
10479
|
SCOPE_DENIED: "Ask a human with editAgents authority to extend this agent's scopes.",
|
|
10464
10480
|
NOT_FOUND: "Check the target spelling with `raft server info --channels` or resolve the message id first.",
|
|
10465
10481
|
CONFLICT: "Read the current state before repeating this operation.",
|
|
10466
|
-
IDEMPOTENCY_KEY_REUSED: "Use a new idempotency key for a different
|
|
10482
|
+
IDEMPOTENCY_KEY_REUSED: "Use a new idempotency key for a different request, or resend the identical request to reconcile.",
|
|
10467
10483
|
UNSUPPORTED_FOR_EXTERNAL_AGENTS: "Use your own runtime for this; the Server does not provide it to External Agents.",
|
|
10468
10484
|
UNAVAILABLE: "Retry in a moment.",
|
|
10469
10485
|
MODEL_ONLY: "Call it as a model tool call instead of from code."
|
|
@@ -10552,6 +10568,24 @@ function failureOutcome(error) {
|
|
|
10552
10568
|
text: formatOpErrorText(error)
|
|
10553
10569
|
};
|
|
10554
10570
|
}
|
|
10571
|
+
/**
|
|
10572
|
+
* A failure of a keyed write (`idempotencyKey`). A retryable failure (the
|
|
10573
|
+
* request may not have reached the Server, or the Server was unavailable)
|
|
10574
|
+
* says to repeat the SAME request with the SAME key, and carries the key in
|
|
10575
|
+
* `next.args.idempotencyKey`, so a caller that let the SDK generate it can
|
|
10576
|
+
* still retry without acting twice.
|
|
10577
|
+
*/
|
|
10578
|
+
function keyedWriteFailure(failure, idempotencyKey) {
|
|
10579
|
+
if (!failure.error.retryable) return failure;
|
|
10580
|
+
return {
|
|
10581
|
+
...failure,
|
|
10582
|
+
next: {
|
|
10583
|
+
kind: "retry_same_key",
|
|
10584
|
+
args: { idempotencyKey },
|
|
10585
|
+
why: "Repeat the same request with this idempotencyKey: if the first attempt was committed, the Server returns its result instead of acting twice."
|
|
10586
|
+
}
|
|
10587
|
+
};
|
|
10588
|
+
}
|
|
10555
10589
|
/** Map a shared-client failure to a failure outcome. */
|
|
10556
10590
|
function failureFromClientResult(result) {
|
|
10557
10591
|
return failureOutcome(opErrorFromClientError(result.error, result.status));
|
|
@@ -12079,6 +12113,22 @@ function formatAgentTaskHistory(data) {
|
|
|
12079
12113
|
return `seq=${event.seq} time=${event.createdAt} actor=${actor} type=${event.eventType}\n ${JSON.stringify(event.payload)}`;
|
|
12080
12114
|
}).join("\n")}`;
|
|
12081
12115
|
}
|
|
12116
|
+
/**
|
|
12117
|
+
* `raft task show`: one task's current title and description (moved verbatim
|
|
12118
|
+
* from the CLI's commands/task/show.ts). Labels match the agent delivery
|
|
12119
|
+
* surface (`Current title:` / `Current description:`). `description` has three
|
|
12120
|
+
* states that must not collapse: a string, an explicit null, and a field the
|
|
12121
|
+
* envelope omitted.
|
|
12122
|
+
*/
|
|
12123
|
+
function formatAgentTaskShow(target, task) {
|
|
12124
|
+
const descriptionLine = typeof task.description === "string" ? `Current description: ${task.description}` : task.description === null ? "Current description: (none set)" : "Current description: (not returned by this surface)";
|
|
12125
|
+
return [
|
|
12126
|
+
`#${task.taskNumber} [${task.status ?? "unknown"}] in ${target}`,
|
|
12127
|
+
`Current title: ${task.title ?? "(none set)"}`,
|
|
12128
|
+
descriptionLine,
|
|
12129
|
+
""
|
|
12130
|
+
].join("\n");
|
|
12131
|
+
}
|
|
12082
12132
|
//#endregion
|
|
12083
12133
|
//#region ../shared/src/agentOps/tasks.ts
|
|
12084
12134
|
const taskChannelSchema = string().describe("The channel the task board belongs to, for example `#proj-sdk`.");
|
|
@@ -12251,21 +12301,24 @@ const createTasksRequestSchema = requestSchema()(object({
|
|
|
12251
12301
|
title: string().describe("Task title."),
|
|
12252
12302
|
createsResource: boolean().optional().describe("The task produces a resource (for example a document) that needs a receipt.")
|
|
12253
12303
|
})).describe("One entry per task to create."),
|
|
12254
|
-
assignee: string().optional().describe("`@handle`: yourself to start in_progress, or (owner/admin) someone else to reserve a todo.")
|
|
12304
|
+
assignee: string().optional().describe("`@handle`: yourself to start in_progress, or (owner/admin) someone else to reserve a todo."),
|
|
12305
|
+
idempotencyKey: string().optional().describe("One key per logical create; generated when omitted and returned. Repeat the same request with the same key within 24 hours to retry without creating the tasks twice.")
|
|
12255
12306
|
}));
|
|
12256
12307
|
async function createTasks(client, request) {
|
|
12257
12308
|
const invalid = validateOpRequest(createTasksRequestSchema, request);
|
|
12258
12309
|
if (invalid) return invalid;
|
|
12259
12310
|
if (!request.target?.trim() || !request.tasks?.length) return failureOutcome(opError("INVALID_REQUEST", { message: "A channel target and at least one task title are required." }));
|
|
12311
|
+
const idempotencyKey = request.idempotencyKey?.trim() || globalThis.crypto.randomUUID();
|
|
12260
12312
|
const result = await client.tasks.create({
|
|
12261
12313
|
channel: request.target,
|
|
12262
12314
|
tasks: request.tasks.map((t) => ({
|
|
12263
12315
|
title: t.title,
|
|
12264
12316
|
...t.createsResource ? { creates_resource: true } : {}
|
|
12265
12317
|
})),
|
|
12266
|
-
...request.assignee ? { assignee: request.assignee } : {}
|
|
12318
|
+
...request.assignee ? { assignee: request.assignee } : {},
|
|
12319
|
+
idempotencyKey
|
|
12267
12320
|
});
|
|
12268
|
-
if (!result.ok) return failureFromClientResult(result);
|
|
12321
|
+
if (!result.ok) return keyedWriteFailure(failureFromClientResult(result), idempotencyKey);
|
|
12269
12322
|
const data = result.data;
|
|
12270
12323
|
const first = data.tasks[0];
|
|
12271
12324
|
return {
|
|
@@ -12273,7 +12326,8 @@ async function createTasks(client, request) {
|
|
|
12273
12326
|
state: "created",
|
|
12274
12327
|
data: {
|
|
12275
12328
|
...data,
|
|
12276
|
-
target: request.target
|
|
12329
|
+
target: request.target,
|
|
12330
|
+
idempotencyKey
|
|
12277
12331
|
},
|
|
12278
12332
|
next: first ? {
|
|
12279
12333
|
kind: "post_in_task_thread",
|
|
@@ -12468,6 +12522,38 @@ async function taskHistory(client, request) {
|
|
|
12468
12522
|
text: formatAgentTaskHistory(result.data)
|
|
12469
12523
|
};
|
|
12470
12524
|
}
|
|
12525
|
+
/**
|
|
12526
|
+
* `raft task show`: one task's current title and description. Reads the
|
|
12527
|
+
* channel's whole board (`status: "all"`, so done and closed tasks are found)
|
|
12528
|
+
* and picks the task; a miss says whether the Server asserted the list is
|
|
12529
|
+
* complete, exactly as the CLI does.
|
|
12530
|
+
*/
|
|
12531
|
+
async function showTask(client, request) {
|
|
12532
|
+
const invalid = requireTaskRef(request);
|
|
12533
|
+
if (invalid) return invalid;
|
|
12534
|
+
const { target, taskNumber } = request;
|
|
12535
|
+
const result = await client.tasks.list({
|
|
12536
|
+
channel: target,
|
|
12537
|
+
status: "all"
|
|
12538
|
+
});
|
|
12539
|
+
if (!result.ok) return failureFromClientResult(result);
|
|
12540
|
+
const tasks = result.data.tasks ?? [];
|
|
12541
|
+
const task = tasks.find((candidate) => candidate.taskNumber === taskNumber);
|
|
12542
|
+
if (!task) return failureOutcome(opError("NOT_FOUND", {
|
|
12543
|
+
message: result.data.pagination?.mode === "complete" && result.data.pagination.truncated === false ? `task #${taskNumber} not found in ${target} (searched ${tasks.length} task(s), status=all; server asserts this list is complete)` : tasks.length === 0 ? `task #${taskNumber} not found in ${target}: the server returned 0 tasks and did not assert the list is complete. That can mean the channel has no tasks, or that the server failed to read them (it currently reports some read errors as an empty list), and this command cannot tell which — so this is not evidence that task #${taskNumber} does not exist` : `task #${taskNumber} not found in ${target} (searched ${tasks.length} task(s), status=all; this surface does NOT assert completeness — so this is "absent from what was returned", not "does not exist")`,
|
|
12544
|
+
nextAction: `Check the number with \`raft task list --target "${target}" --status all\`.`
|
|
12545
|
+
}));
|
|
12546
|
+
return {
|
|
12547
|
+
ok: true,
|
|
12548
|
+
state: "task",
|
|
12549
|
+
data: {
|
|
12550
|
+
target,
|
|
12551
|
+
task
|
|
12552
|
+
},
|
|
12553
|
+
next: null,
|
|
12554
|
+
text: formatAgentTaskShow(target, task)
|
|
12555
|
+
};
|
|
12556
|
+
}
|
|
12471
12557
|
const convertMessageToTaskRequestSchema = requestSchema()(object({
|
|
12472
12558
|
target: taskChannelSchema,
|
|
12473
12559
|
messageId: string().describe("Full or short id of a top-level message in that channel.")
|
|
@@ -12796,6 +12882,30 @@ function formatPageFooter(page) {
|
|
|
12796
12882
|
if (page.nextCommand && end < page.total) lines.push(`More: ${page.nextCommand}`);
|
|
12797
12883
|
return `${lines.join("\n")}\n`;
|
|
12798
12884
|
}
|
|
12885
|
+
/** Single channel detail block. */
|
|
12886
|
+
function formatAgentChannelInfo(channel, memberCounts) {
|
|
12887
|
+
const lines = ["## Channel", ""];
|
|
12888
|
+
lines.push(`Channel: ${agentChannelRef(channel.name, channel.type)}`);
|
|
12889
|
+
if (channel.id) lines.push(`ID: ${channel.id}`);
|
|
12890
|
+
lines.push(`Visibility: ${channelVisibility(channel)}`);
|
|
12891
|
+
lines.push(`Joined: ${channel.joined ? "yes" : "no"}`);
|
|
12892
|
+
if (channel.channelRole) lines.push(`Channel role: ${channel.channelRole}`);
|
|
12893
|
+
if (channel.channelAdminBasis) lines.push(`Channel admin basis: ${channel.channelAdminBasis}`);
|
|
12894
|
+
const callableCapabilities = Object.entries(channel.channelCapabilities ?? {}).filter(([, allowed]) => allowed).map(([capability]) => capability);
|
|
12895
|
+
if (callableCapabilities.length > 0) lines.push(`Channel capabilities: ${callableCapabilities.join(", ")}`);
|
|
12896
|
+
const muted = channelMuted(channel);
|
|
12897
|
+
if (muted !== void 0) lines.push(`Muted: ${muted ? "yes" : "no"}`);
|
|
12898
|
+
if (typeof channel.archived === "boolean") lines.push(`Archived: ${channel.archived ? "yes" : "no"}`);
|
|
12899
|
+
lines.push(`Description: ${channel.description?.trim() || "(none)"}`);
|
|
12900
|
+
if (memberCounts) {
|
|
12901
|
+
const agents = memberCounts.agents ?? 0;
|
|
12902
|
+
const humans = memberCounts.humans ?? 0;
|
|
12903
|
+
lines.push(`Members: ${agents + humans} (${agents} agents, ${humans} humans)`);
|
|
12904
|
+
}
|
|
12905
|
+
lines.push("");
|
|
12906
|
+
lines.push(`More: raft channel members "${agentChannelRef(channel.name, channel.type)}"`);
|
|
12907
|
+
return `${lines.join("\n")}\n`;
|
|
12908
|
+
}
|
|
12799
12909
|
/** Compact server summary. */
|
|
12800
12910
|
function formatAgentServerSummary(data) {
|
|
12801
12911
|
const channels = data.channels ?? [];
|
|
@@ -12862,6 +12972,23 @@ function formatAgentServerHumans(humans, page) {
|
|
|
12862
12972
|
}
|
|
12863
12973
|
return `${lines.join("\n")}${formatPageFooter(page)}`;
|
|
12864
12974
|
}
|
|
12975
|
+
/** Narrow visible facts for one user/agent. */
|
|
12976
|
+
function formatAgentUserInfo(user, memberships, page, skippedChannels = 0) {
|
|
12977
|
+
const name = user.value.name;
|
|
12978
|
+
const role = roleLabel(user.value.role);
|
|
12979
|
+
const lines = ["## User", ""];
|
|
12980
|
+
lines.push(`User: @${name}`);
|
|
12981
|
+
lines.push(`Kind: ${user.kind}`);
|
|
12982
|
+
if (user.kind === "agent") lines.push(`Status: ${agentStatusLabel(user.value)}`);
|
|
12983
|
+
if (role) lines.push(`Role: ${role.slice(2, -1)}`);
|
|
12984
|
+
if (user.value.description) lines.push(`Description: ${user.value.description}`);
|
|
12985
|
+
lines.push("");
|
|
12986
|
+
lines.push("### Visible Channel Memberships");
|
|
12987
|
+
if (memberships.length === 0) lines.push("(none found in inspected visible channels)");
|
|
12988
|
+
else for (const channel of memberships) lines.push(`${agentChannelRef(channel.name, channel.type)} [${channelStatus(channel)}]`);
|
|
12989
|
+
if (skippedChannels > 0) lines.push(`Skipped ${skippedChannels} visible channel roster checks because the server rejected them.`);
|
|
12990
|
+
return `${lines.join("\n")}${formatPageFooter(page)}`;
|
|
12991
|
+
}
|
|
12865
12992
|
/** Channel membership with server-role labels. */
|
|
12866
12993
|
function formatAgentChannelMembers(data) {
|
|
12867
12994
|
let text = "## Channel Members\n\n";
|
|
@@ -13252,6 +13379,41 @@ async function unfollowThread(client, request) {
|
|
|
13252
13379
|
text: `Unfollowed ${request.target}. Ordinary delivery from this thread stops; a direct @mention reactivates the follow.`
|
|
13253
13380
|
};
|
|
13254
13381
|
}
|
|
13382
|
+
const channelInfoRequestSchema = requestSchema()(object({ target: string().describe("A regular channel, `#channel-name` (the `#` may be omitted). DMs and threads are not accepted.") }));
|
|
13383
|
+
/**
|
|
13384
|
+
* `raft channel info <target>`: the channel's facts from `server.info`, plus
|
|
13385
|
+
* member counts from its roster when the Server shows it.
|
|
13386
|
+
*/
|
|
13387
|
+
async function channelInfo(client, request) {
|
|
13388
|
+
const invalid = validateOpRequest(channelInfoRequestSchema, request);
|
|
13389
|
+
if (invalid) return invalid;
|
|
13390
|
+
const trimmed = request.target.trim();
|
|
13391
|
+
const input = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
13392
|
+
const name = parseRaftRegularChannelTarget$1(input);
|
|
13393
|
+
if (!name) return failureOutcome(opError("INVALID_REQUEST", { message: "Target must be a regular channel name, e.g. '#engineering' or 'engineering'. DMs and thread targets are not supported." }));
|
|
13394
|
+
const info = await client.server.info();
|
|
13395
|
+
if (!info.ok) return failureFromClientResult(info);
|
|
13396
|
+
const channel = info.data.channels.find((candidate) => candidate.name === name);
|
|
13397
|
+
if (!channel) return failureOutcome(opError("NOT_FOUND", {
|
|
13398
|
+
message: `Channel not found or not visible: ${input}`,
|
|
13399
|
+
nextAction: "Run `raft server info --channels --query <name>` to inspect visible channels, or ask a channel member to add you if this is private."
|
|
13400
|
+
}));
|
|
13401
|
+
const members = await client.channels.members({ channel: `#${name}` });
|
|
13402
|
+
const memberCounts = members.ok ? {
|
|
13403
|
+
agents: members.data.agents?.length ?? 0,
|
|
13404
|
+
humans: members.data.humans?.length ?? 0
|
|
13405
|
+
} : null;
|
|
13406
|
+
return {
|
|
13407
|
+
ok: true,
|
|
13408
|
+
state: "info",
|
|
13409
|
+
data: {
|
|
13410
|
+
channel,
|
|
13411
|
+
memberCounts
|
|
13412
|
+
},
|
|
13413
|
+
next: null,
|
|
13414
|
+
text: formatAgentChannelInfo(channel, memberCounts)
|
|
13415
|
+
};
|
|
13416
|
+
}
|
|
13255
13417
|
//#endregion
|
|
13256
13418
|
//#region ../shared/src/agentOps/server.ts
|
|
13257
13419
|
const serverInfoRequestSchema = requestSchema()(object({
|
|
@@ -13375,6 +13537,85 @@ async function updateProfile(client, request, options = {}) {
|
|
|
13375
13537
|
text: formatAgentProfile(result.data, options)
|
|
13376
13538
|
};
|
|
13377
13539
|
}
|
|
13540
|
+
const userInfoRequestSchema = requestSchema()(object({
|
|
13541
|
+
name: string().describe("The human or agent, `@handle` (or the bare handle)."),
|
|
13542
|
+
offset: number$1().int().nonnegative().optional().describe("Membership paging: visible channels to skip before inspecting (default 0)."),
|
|
13543
|
+
limit: number$1().int().positive().optional().describe("Membership paging: visible channels to inspect (default 50).")
|
|
13544
|
+
}));
|
|
13545
|
+
/**
|
|
13546
|
+
* `raft user info <name>`: the user's visible facts from `server.info`, and
|
|
13547
|
+
* their memberships among one page of the visible channels, checked one
|
|
13548
|
+
* channel roster at a time (a rejected roster is skipped and counted).
|
|
13549
|
+
*/
|
|
13550
|
+
async function userInfo(client, request) {
|
|
13551
|
+
const invalid = validateOpRequest(userInfoRequestSchema, request);
|
|
13552
|
+
if (invalid) return invalid;
|
|
13553
|
+
const trimmed = request.name.trim();
|
|
13554
|
+
const name = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
13555
|
+
if (!name) return failureOutcome(opError("INVALID_REQUEST", { message: "user name is required" }));
|
|
13556
|
+
const limit = request.limit ?? 50;
|
|
13557
|
+
const offset = request.offset ?? 0;
|
|
13558
|
+
const info = await client.server.info();
|
|
13559
|
+
if (!info.ok) return failureFromClientResult(info);
|
|
13560
|
+
const agent = info.data.agents.find((candidate) => candidate.name === name);
|
|
13561
|
+
const human = info.data.humans.find((candidate) => candidate.name === name);
|
|
13562
|
+
const user = agent ? {
|
|
13563
|
+
kind: "agent",
|
|
13564
|
+
value: agent
|
|
13565
|
+
} : human ? {
|
|
13566
|
+
kind: "human",
|
|
13567
|
+
value: human
|
|
13568
|
+
} : null;
|
|
13569
|
+
if (!user) return failureOutcome(opError("NOT_FOUND", {
|
|
13570
|
+
message: `User not found or not visible: @${name}`,
|
|
13571
|
+
nextAction: "Run `raft server info --agents --query <name>` or `raft server info --humans --query <name>` to inspect visible users."
|
|
13572
|
+
}));
|
|
13573
|
+
const visibleChannels = info.data.channels;
|
|
13574
|
+
const memberships = [];
|
|
13575
|
+
let skippedChannels = 0;
|
|
13576
|
+
for (const channel of visibleChannels.slice(offset, offset + limit)) {
|
|
13577
|
+
const members = await client.channels.members({ channel: `#${channel.name}` });
|
|
13578
|
+
if (!members.ok) {
|
|
13579
|
+
skippedChannels += 1;
|
|
13580
|
+
continue;
|
|
13581
|
+
}
|
|
13582
|
+
if ((user.kind === "agent" ? members.data.agents ?? [] : members.data.humans ?? []).some((candidate) => candidate.name === name)) memberships.push({
|
|
13583
|
+
...channel,
|
|
13584
|
+
joined: true,
|
|
13585
|
+
muted: void 0,
|
|
13586
|
+
activityMuted: void 0
|
|
13587
|
+
});
|
|
13588
|
+
}
|
|
13589
|
+
const nextOffset = offset + limit;
|
|
13590
|
+
const page = {
|
|
13591
|
+
total: visibleChannels.length,
|
|
13592
|
+
offset,
|
|
13593
|
+
limit,
|
|
13594
|
+
nextCommand: nextOffset < visibleChannels.length ? `raft user info @${name} --offset ${nextOffset} --limit ${limit}` : void 0
|
|
13595
|
+
};
|
|
13596
|
+
const next = page.nextCommand ? {
|
|
13597
|
+
kind: "next_page",
|
|
13598
|
+
command: page.nextCommand,
|
|
13599
|
+
args: {
|
|
13600
|
+
name: `@${name}`,
|
|
13601
|
+
offset: nextOffset,
|
|
13602
|
+
limit
|
|
13603
|
+
},
|
|
13604
|
+
why: "Only one page of visible channels was inspected for memberships."
|
|
13605
|
+
} : null;
|
|
13606
|
+
return {
|
|
13607
|
+
ok: true,
|
|
13608
|
+
state: "info",
|
|
13609
|
+
data: {
|
|
13610
|
+
user,
|
|
13611
|
+
memberships,
|
|
13612
|
+
skippedChannels,
|
|
13613
|
+
page
|
|
13614
|
+
},
|
|
13615
|
+
next,
|
|
13616
|
+
text: formatAgentUserInfo(user, memberships, page, skippedChannels)
|
|
13617
|
+
};
|
|
13618
|
+
}
|
|
13378
13619
|
//#endregion
|
|
13379
13620
|
//#region ../shared/src/agentText/attachments.ts
|
|
13380
13621
|
/** Upload receipt with attachment id and send-usage hint. */
|
|
@@ -14481,11 +14722,16 @@ async function prepareActionCard(client, request) {
|
|
|
14481
14722
|
if (typeof request?.target !== "string" || !request.target.trim() || !request.action) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and an action are required to prepare a card." }));
|
|
14482
14723
|
const invalid = validateOpRequest(prepareActionCardRequestSchema, request);
|
|
14483
14724
|
if (invalid) return invalid;
|
|
14484
|
-
const
|
|
14485
|
-
|
|
14725
|
+
const idempotencyKey = request.idempotencyKey?.trim() || globalThis.crypto.randomUUID();
|
|
14726
|
+
const result = await client.actions.prepare({
|
|
14727
|
+
...request,
|
|
14728
|
+
idempotencyKey
|
|
14729
|
+
});
|
|
14730
|
+
if (!result.ok) return keyedWriteFailure(failureFromClientResult(result), idempotencyKey);
|
|
14486
14731
|
const card = {
|
|
14487
14732
|
target: request.target,
|
|
14488
|
-
messageId: result.data.messageId
|
|
14733
|
+
messageId: result.data.messageId,
|
|
14734
|
+
idempotencyKey
|
|
14489
14735
|
};
|
|
14490
14736
|
return {
|
|
14491
14737
|
ok: true,
|
|
@@ -14946,9 +15192,11 @@ const OPERATION_DEFS = [
|
|
|
14946
15192
|
schema: prepareActionCardRequestSchema,
|
|
14947
15193
|
fieldDescriptions: {
|
|
14948
15194
|
target: "Conversation to post the card in: `#channel`, `dm:@peer`, or a thread.",
|
|
14949
|
-
action: "The operation the card proposes: `type` picks it, and the other fields apply per type as described."
|
|
15195
|
+
action: "The operation the card proposes: `type` picks it, and the other fields apply per type as described.",
|
|
15196
|
+
idempotencyKey: "One key per logical prepare; generated when omitted and returned. Repeat the same request with the same key within 24 hours to retry without posting a second card."
|
|
14950
15197
|
},
|
|
14951
15198
|
routes: ["actionPrepare"],
|
|
15199
|
+
idempotencyArg: "idempotencyKey",
|
|
14952
15200
|
consumes: nothing,
|
|
14953
15201
|
output: small
|
|
14954
15202
|
},
|
|
@@ -14999,6 +15247,7 @@ const OPERATION_DEFS = [
|
|
|
14999
15247
|
description: "Create one or more tasks on a channel's board; each gets its own thread.",
|
|
15000
15248
|
schema: createTasksRequestSchema,
|
|
15001
15249
|
routes: ["taskCreate"],
|
|
15250
|
+
idempotencyArg: "idempotencyKey",
|
|
15002
15251
|
consumes: nothing,
|
|
15003
15252
|
output: small
|
|
15004
15253
|
},
|
|
@@ -15055,6 +15304,17 @@ const OPERATION_DEFS = [
|
|
|
15055
15304
|
boundBy: []
|
|
15056
15305
|
}
|
|
15057
15306
|
},
|
|
15307
|
+
{
|
|
15308
|
+
name: "tasks.show",
|
|
15309
|
+
description: "One task's current status, title, and description. Finds done and closed tasks too.",
|
|
15310
|
+
schema: taskRefSchema,
|
|
15311
|
+
routes: ["taskList"],
|
|
15312
|
+
consumes: nothing,
|
|
15313
|
+
output: {
|
|
15314
|
+
mayBeLarge: true,
|
|
15315
|
+
boundBy: []
|
|
15316
|
+
}
|
|
15317
|
+
},
|
|
15058
15318
|
{
|
|
15059
15319
|
name: "tasks.convert",
|
|
15060
15320
|
description: "Turn a top-level message into an unassigned task.",
|
|
@@ -15114,6 +15374,14 @@ const OPERATION_DEFS = [
|
|
|
15114
15374
|
boundBy: []
|
|
15115
15375
|
}
|
|
15116
15376
|
},
|
|
15377
|
+
{
|
|
15378
|
+
name: "channels.info",
|
|
15379
|
+
description: "A regular channel's facts: visibility, whether you joined it, your role and mute state, description, and member counts.",
|
|
15380
|
+
schema: channelInfoRequestSchema,
|
|
15381
|
+
routes: ["serverInfo", "channelMembers"],
|
|
15382
|
+
consumes: nothing,
|
|
15383
|
+
output: small
|
|
15384
|
+
},
|
|
15117
15385
|
{
|
|
15118
15386
|
name: "threads.list",
|
|
15119
15387
|
description: "The threads you follow.",
|
|
@@ -15148,6 +15416,17 @@ const OPERATION_DEFS = [
|
|
|
15148
15416
|
]
|
|
15149
15417
|
}
|
|
15150
15418
|
},
|
|
15419
|
+
{
|
|
15420
|
+
name: "users.info",
|
|
15421
|
+
description: "A human's or agent's visible facts (kind, status, role, description) and which visible channels they are in. Memberships are checked over one page of visible channels; page with offset and limit.",
|
|
15422
|
+
schema: userInfoRequestSchema,
|
|
15423
|
+
routes: ["serverInfo", "channelMembers"],
|
|
15424
|
+
consumes: nothing,
|
|
15425
|
+
output: {
|
|
15426
|
+
mayBeLarge: true,
|
|
15427
|
+
boundBy: ["offset", "limit"]
|
|
15428
|
+
}
|
|
15429
|
+
},
|
|
15151
15430
|
{
|
|
15152
15431
|
name: "profile.show",
|
|
15153
15432
|
description: "Show your profile, or someone else's by @handle.",
|
|
@@ -15393,6 +15672,7 @@ function createRaft(options) {
|
|
|
15393
15672
|
updateStatus: (request) => updateTaskStatus(api, request),
|
|
15394
15673
|
amend: (request) => amendTask(api, request),
|
|
15395
15674
|
history: (request) => taskHistory(api, request),
|
|
15675
|
+
show: (request) => showTask(api, request),
|
|
15396
15676
|
convert: (request) => convertMessageToTask(api, request),
|
|
15397
15677
|
delete: (request) => deleteTask(api, request)
|
|
15398
15678
|
},
|
|
@@ -15401,13 +15681,15 @@ function createRaft(options) {
|
|
|
15401
15681
|
leave: (request) => leaveChannel(api, request),
|
|
15402
15682
|
mute: (request) => muteChannel(api, request),
|
|
15403
15683
|
unmute: (request) => unmuteChannel(api, request),
|
|
15404
|
-
members: (request) => channelMembers(api, request)
|
|
15684
|
+
members: (request) => channelMembers(api, request),
|
|
15685
|
+
info: (request) => channelInfo(api, request)
|
|
15405
15686
|
},
|
|
15406
15687
|
threads: {
|
|
15407
15688
|
list: () => listThreads(api),
|
|
15408
15689
|
unfollow: (request) => unfollowThread(api, request)
|
|
15409
15690
|
},
|
|
15410
15691
|
server: { info: (request) => serverInfo(api, request) },
|
|
15692
|
+
users: { info: (request) => userInfo(api, request) },
|
|
15411
15693
|
profile: {
|
|
15412
15694
|
show: (request) => showProfile(api, request),
|
|
15413
15695
|
update: (request) => updateProfile(api, request)
|
|
@@ -15475,6 +15757,7 @@ function createRaft(options) {
|
|
|
15475
15757
|
"tasks.updateStatus": (args) => raft.tasks.updateStatus(args),
|
|
15476
15758
|
"tasks.amend": (args) => raft.tasks.amend(args),
|
|
15477
15759
|
"tasks.history": (args) => raft.tasks.history(args),
|
|
15760
|
+
"tasks.show": (args) => raft.tasks.show(args),
|
|
15478
15761
|
"tasks.convert": (args) => raft.tasks.convert(args),
|
|
15479
15762
|
"tasks.delete": (args) => raft.tasks.delete(args),
|
|
15480
15763
|
"channels.join": (args) => raft.channels.join(args),
|
|
@@ -15482,9 +15765,11 @@ function createRaft(options) {
|
|
|
15482
15765
|
"channels.mute": (args) => raft.channels.mute(args),
|
|
15483
15766
|
"channels.unmute": (args) => raft.channels.unmute(args),
|
|
15484
15767
|
"channels.members": (args) => raft.channels.members(args),
|
|
15768
|
+
"channels.info": (args) => raft.channels.info(args),
|
|
15485
15769
|
"threads.list": () => raft.threads.list(),
|
|
15486
15770
|
"threads.unfollow": (args) => raft.threads.unfollow(args),
|
|
15487
15771
|
"server.info": (args) => raft.server.info(args),
|
|
15772
|
+
"users.info": (args) => raft.users.info(args),
|
|
15488
15773
|
"profile.show": (args) => raft.profile.show(args),
|
|
15489
15774
|
"profile.update": (args) => raft.profile.update(args)
|
|
15490
15775
|
};
|
|
@@ -15545,6 +15830,7 @@ exports.amendTaskRequestSchema = amendTaskRequestSchema;
|
|
|
15545
15830
|
exports.assignTaskRequestSchema = assignTaskRequestSchema;
|
|
15546
15831
|
exports.attachmentCommentsRequestSchema = attachmentCommentsRequestSchema;
|
|
15547
15832
|
exports.bootstrapRaftCredential = bootstrapRaftCredential;
|
|
15833
|
+
exports.channelInfoRequestSchema = channelInfoRequestSchema;
|
|
15548
15834
|
exports.channelMembersRequestSchema = channelMembersRequestSchema;
|
|
15549
15835
|
exports.channelTargetRequestSchema = channelTargetRequestSchema;
|
|
15550
15836
|
exports.checkInboxRequestSchema = checkInboxRequestSchema;
|
|
@@ -15592,5 +15878,6 @@ exports.taskRefSchema = taskRefSchema;
|
|
|
15592
15878
|
exports.unfollowThreadRequestSchema = unfollowThreadRequestSchema;
|
|
15593
15879
|
exports.updateProfileRequestSchema = updateProfileRequestSchema;
|
|
15594
15880
|
exports.updateTaskStatusRequestSchema = updateTaskStatusRequestSchema;
|
|
15881
|
+
exports.userInfoRequestSchema = userInfoRequestSchema;
|
|
15595
15882
|
exports.verifyInboxNotice = verifyInboxNotice;
|
|
15596
15883
|
exports.whoamiRequestSchema = whoamiRequestSchema;
|