@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 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`), `create`, `unclaim`,
145
- `assign`, `updateStatus`, `amend`, `history`, `convert`, `delete`; a hold on
146
- `updateStatus` / `amend` is an interrupt too.
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.profile.show / update`.
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` are
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 (omitting it means null: `tasks.assign` without
288
- `assignee` clears it; the runtime still accepts an explicit null);
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. Integration action cards are created by `raft integration`
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),
@@ -4713,7 +4713,7 @@ const AGENT_API_ROUTE_META = {
4713
4713
  mentionActionsExecute: write,
4714
4714
  taskClaim: write,
4715
4715
  taskList: read,
4716
- taskCreate: write,
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: write,
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": "none",
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": "none",
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 = "c7a793816a7a90ed";
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 message.",
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 message, or resend the identical payload to reconcile.",
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 result = await client.actions.prepare(request);
14485
- if (!result.ok) return failureFromClientResult(result);
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;