@mininglamp-oss/cc-channel-octo 1.0.3-dev.4ca07a0 → 1.0.3-dev.4edf363

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.
@@ -90,4 +90,83 @@ export class GroupMdCache {
90
90
  this.mem.delete(groupNo);
91
91
  }
92
92
  }
93
+ /**
94
+ * THREAD.md server cache (P3-1) — IN-MEMORY ONLY, with a TTL.
95
+ *
96
+ * The thread analogue of {@link GroupMdCache}. Holds server-fetched THREAD.md
97
+ * for a CommunityTopic thread. Same memory-only security model as the group
98
+ * cache (see the module SECURITY note): the resolved THREAD.md is injected as a
99
+ * trusted system-prompt block, so it must never touch disk — the only path
100
+ * content can enter the trusted channel is a live, authenticated `getThreadMd`
101
+ * over the bot token against the SSRF-validated `apiUrl`.
102
+ *
103
+ * Keying — COMPOSITE `<groupNo>::<shortId>`, NOT the bare shortId. Octo's
104
+ * current shortId is a globally unique 19-digit snowflake, so a bare shortId
105
+ * would not collide across groups today; the composite key is defensive
106
+ * double-insurance so that even if a future shortId scheme becomes a
107
+ * per-parent-group sequence, one group's THREAD.md can never be served for a
108
+ * same-shortId thread under a different parent group. `::` is intentionally
109
+ * outside the safe-id charset, so it can never appear inside a validated
110
+ * component and cannot be forged to cross a key boundary.
111
+ *
112
+ * Never throws.
113
+ */
114
+ export class ThreadMdCache {
115
+ mem = new Map();
116
+ ttlMs;
117
+ now;
118
+ /**
119
+ * @param ttlMs staleness backstop in ms (entry expires this long after it was
120
+ * stored). Defaults to {@link DEFAULT_GROUP_MD_TTL_MS} — the thread cache
121
+ * shares the group cache's staleness policy. A non-positive value disables
122
+ * expiry (entries live until invalidate()).
123
+ * @param now injectable clock (testing); defaults to Date.now.
124
+ */
125
+ constructor(ttlMs = DEFAULT_GROUP_MD_TTL_MS, now = () => Date.now()) {
126
+ this.ttlMs = ttlMs;
127
+ this.now = now;
128
+ }
129
+ /**
130
+ * Build the composite Map key, or null if either component is unsafe. Both
131
+ * halves are validated with the same isSafeGroupNo rule the group cache uses,
132
+ * and neither can contain `::` (colon is outside the charset), so the
133
+ * separator is unambiguous.
134
+ */
135
+ key(groupNo, shortId) {
136
+ if (!isSafeGroupNo(groupNo) || !isSafeGroupNo(shortId))
137
+ return null;
138
+ return `${groupNo}::${shortId}`;
139
+ }
140
+ /**
141
+ * Read a cached entry from memory. Returns undefined on a miss, an expired
142
+ * entry (which is also evicted), or an unsafe groupNo/shortId.
143
+ */
144
+ get(groupNo, shortId) {
145
+ const k = this.key(groupNo, shortId);
146
+ if (k === null)
147
+ return undefined;
148
+ const stored = this.mem.get(k);
149
+ if (!stored)
150
+ return undefined;
151
+ if (this.ttlMs > 0 && this.now() - stored.storedAt >= this.ttlMs) {
152
+ this.mem.delete(k);
153
+ return undefined;
154
+ }
155
+ return stored.entry;
156
+ }
157
+ /** Store an entry in memory, stamping it for TTL expiry. */
158
+ set(groupNo, shortId, entry) {
159
+ const k = this.key(groupNo, shortId);
160
+ if (k === null)
161
+ return;
162
+ this.mem.set(k, { entry, storedAt: this.now() });
163
+ }
164
+ /** Drop a cached entry (event-driven refresh hook, symmetric to GroupMdCache). */
165
+ invalidate(groupNo, shortId) {
166
+ const k = this.key(groupNo, shortId);
167
+ if (k === null)
168
+ return;
169
+ this.mem.delete(k);
170
+ }
171
+ }
93
172
  //# sourceMappingURL=group-md-cache.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"group-md-cache.js","sourceRoot":"","sources":["../src/group-md-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAUH,+EAA+E;AAC/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,YAAY;AAElE;;;;GAIG;AACH,SAAS,aAAa,CAAC,OAAe;IACpC,OAAO,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,IAAI,CAAC;AAClF,CAAC;AAOD,MAAM,OAAO,YAAY;IACN,GAAG,GAAG,IAAI,GAAG,EAAuB,CAAC;IACrC,KAAK,CAAS;IACd,GAAG,CAAe;IAEnC;;;;;OAKG;IACH,YAAY,QAAgB,uBAAuB,EAAE,MAAoB,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;QACvF,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,OAAe;QACjB,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC;YAAE,OAAO,SAAS,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QAC9B,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACjE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACzB,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,MAAM,CAAC,KAAK,CAAC;IACtB,CAAC;IAED,4DAA4D;IAC5D,GAAG,CAAC,OAAe,EAAE,KAAmB;QACtC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC;YAAE,OAAO;QACpC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACzD,CAAC;IAED;;;OAGG;IACH,UAAU,CAAC,OAAe;QACxB,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC;YAAE,OAAO;QACpC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;CACF"}
1
+ {"version":3,"file":"group-md-cache.js","sourceRoot":"","sources":["../src/group-md-cache.ts"],"names":[],"mappings":"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"}
@@ -27,6 +27,11 @@
27
27
  /**
28
28
  * Provisional `event.type` literals treated as a GROUP.md change. PROVISIONAL —
29
29
  * see the calibration note above. Overridable via `config.serverMdEventTypes`.
30
+ *
31
+ * Both an UPDATE and a DELETE of the server GROUP.md drive the same action —
32
+ * INVALIDATE the cached entry so the next turn re-fetches (an updated entry is
33
+ * re-read; a deleted one 404s and cleanly degrades to the local fallback), so
34
+ * `group_md_deleted` sits alongside `group_md_updated` here (P3-2 deleted tail).
30
35
  */
31
36
  export declare const DEFAULT_GROUP_MD_EVENT_TYPES: readonly string[];
32
37
  /** The `payload.event` shape (mirrors MessagePayload.event in octo/types.ts). */
@@ -35,9 +40,36 @@ export interface GroupMdEventLike {
35
40
  group_no?: string;
36
41
  }
37
42
  /**
38
- * True iff this event signals a GROUP.md change and should drive a cache
39
- * refresh. All other system events (group join/leave, etc.) return false and
40
- * are dropped unchanged by the router. `eventTypes` lets the operator override
41
- * the provisional literal(s) without a code change.
43
+ * True iff this event signals a GROUP.md change (update or delete) and should
44
+ * drive a cache refresh. All other system events (group join/leave, etc.) return
45
+ * false and are dropped unchanged by the router. `eventTypes` lets the operator
46
+ * override the provisional literal(s) without a code change.
42
47
  */
43
48
  export declare function isGroupMdUpdateEvent(event: GroupMdEventLike | undefined, eventTypes?: readonly string[]): boolean;
49
+ /**
50
+ * Provisional `event.type` literals treated as a THREAD.md change (P3-2). Same
51
+ * PROVISIONAL status as the group literals above — the exact wire literal is not
52
+ * yet confirmed from a captured event, so it is named after the design and is
53
+ * overridable via `config.threadMdEventTypes` without a code change. As on the
54
+ * group side, both update and delete map to the same invalidate action.
55
+ */
56
+ export declare const DEFAULT_THREAD_MD_EVENT_TYPES: readonly string[];
57
+ /**
58
+ * The `payload.event` shape for a THREAD.md change — like {@link GroupMdEventLike}
59
+ * but also carrying `short_id`, which locates the subarea whose composite-keyed
60
+ * (`groupNo::shortId`) cache entry to invalidate. `short_id` is already part of
61
+ * MessagePayload.event (octo/types.ts).
62
+ */
63
+ export interface ThreadMdEventLike {
64
+ type?: string;
65
+ group_no?: string;
66
+ short_id?: string;
67
+ }
68
+ /**
69
+ * True iff this event signals a THREAD.md change (update or delete). The literal
70
+ * sets for group vs thread are DISJOINT (`group_md_*` vs `thread_md_*`), so a
71
+ * thread event never trips the group invalidation and vice versa — the two
72
+ * classifiers stay mutually exclusive, matching the read/write mutual-exclusion
73
+ * contract of #88 P3.
74
+ */
75
+ export declare function isThreadMdUpdateEvent(event: ThreadMdEventLike | undefined, eventTypes?: readonly string[]): boolean;
@@ -27,17 +27,48 @@
27
27
  /**
28
28
  * Provisional `event.type` literals treated as a GROUP.md change. PROVISIONAL —
29
29
  * see the calibration note above. Overridable via `config.serverMdEventTypes`.
30
+ *
31
+ * Both an UPDATE and a DELETE of the server GROUP.md drive the same action —
32
+ * INVALIDATE the cached entry so the next turn re-fetches (an updated entry is
33
+ * re-read; a deleted one 404s and cleanly degrades to the local fallback), so
34
+ * `group_md_deleted` sits alongside `group_md_updated` here (P3-2 deleted tail).
30
35
  */
31
- export const DEFAULT_GROUP_MD_EVENT_TYPES = ['group_md_updated'];
36
+ export const DEFAULT_GROUP_MD_EVENT_TYPES = [
37
+ 'group_md_updated',
38
+ 'group_md_deleted',
39
+ ];
32
40
  /**
33
- * True iff this event signals a GROUP.md change and should drive a cache
34
- * refresh. All other system events (group join/leave, etc.) return false and
35
- * are dropped unchanged by the router. `eventTypes` lets the operator override
36
- * the provisional literal(s) without a code change.
41
+ * True iff this event signals a GROUP.md change (update or delete) and should
42
+ * drive a cache refresh. All other system events (group join/leave, etc.) return
43
+ * false and are dropped unchanged by the router. `eventTypes` lets the operator
44
+ * override the provisional literal(s) without a code change.
37
45
  */
38
46
  export function isGroupMdUpdateEvent(event, eventTypes = DEFAULT_GROUP_MD_EVENT_TYPES) {
39
47
  if (!event || typeof event.type !== 'string' || event.type === '')
40
48
  return false;
41
49
  return eventTypes.includes(event.type);
42
50
  }
51
+ /**
52
+ * Provisional `event.type` literals treated as a THREAD.md change (P3-2). Same
53
+ * PROVISIONAL status as the group literals above — the exact wire literal is not
54
+ * yet confirmed from a captured event, so it is named after the design and is
55
+ * overridable via `config.threadMdEventTypes` without a code change. As on the
56
+ * group side, both update and delete map to the same invalidate action.
57
+ */
58
+ export const DEFAULT_THREAD_MD_EVENT_TYPES = [
59
+ 'thread_md_updated',
60
+ 'thread_md_deleted',
61
+ ];
62
+ /**
63
+ * True iff this event signals a THREAD.md change (update or delete). The literal
64
+ * sets for group vs thread are DISJOINT (`group_md_*` vs `thread_md_*`), so a
65
+ * thread event never trips the group invalidation and vice versa — the two
66
+ * classifiers stay mutually exclusive, matching the read/write mutual-exclusion
67
+ * contract of #88 P3.
68
+ */
69
+ export function isThreadMdUpdateEvent(event, eventTypes = DEFAULT_THREAD_MD_EVENT_TYPES) {
70
+ if (!event || typeof event.type !== 'string' || event.type === '')
71
+ return false;
72
+ return eventTypes.includes(event.type);
73
+ }
43
74
  //# sourceMappingURL=group-md-events.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"group-md-events.js","sourceRoot":"","sources":["../src/group-md-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAsB,CAAC,kBAAkB,CAAC,CAAC;AAQpF;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAmC,EACnC,aAAgC,4BAA4B;IAE5D,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAChF,OAAO,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC"}
1
+ {"version":3,"file":"group-md-events.js","sourceRoot":"","sources":["../src/group-md-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAsB;IAC7D,kBAAkB;IAClB,kBAAkB;CACnB,CAAC;AAQF;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAmC,EACnC,aAAgC,4BAA4B;IAE5D,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAChF,OAAO,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAsB;IAC9D,mBAAmB;IACnB,mBAAmB;CACpB,CAAC;AAcF;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAoC,EACpC,aAAgC,6BAA6B;IAE7D,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAChF,OAAO,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * P2-C: GROUP.md write-back tool — an in-process MCP server letting the agent
3
+ * persist an updated GROUP.md back to the server. The tool surfaces to the model
4
+ * as `mcp__group_md__update_group_md`.
5
+ *
6
+ * The server is built PER TURN (`createGroupMdToolServer`) with the current
7
+ * message's channel coords + the bot owner uid, so:
8
+ * - the write targets the PARENT group of the channel the agent is in
9
+ * (`extractParentGroupNo` — a thread shares its parent group's GROUP.md);
10
+ * - invocation is GATED to the bot owner (registerBot.owner_uid). The group's
11
+ * octo_tag token has server-side write permission, but the agent is driven by
12
+ * untrusted IM users, so this owner gate — not LLM judgment, and independent
13
+ * of the token's group-role permission — is what stops a prompt-injected
14
+ * agent from rewriting the operator's trusted GROUP.md from any chat.
15
+ *
16
+ * Concurrency, the byte ceiling, and the cache refresh are owned by the shared
17
+ * {@link GroupMdWriteback} coordinator (group-md-writeback.ts); this layer is
18
+ * only the owner-gate policy + the MCP surface.
19
+ */
20
+ import { z } from 'zod';
21
+ import { type GroupMdWriteback, type ThreadMdWriteback } from './group-md-writeback.js';
22
+ /** MCP server name; the tool surfaces as `mcp__group_md__update_group_md`. */
23
+ export declare const GROUP_MD_TOOL_SERVER_NAME = "group_md";
24
+ /** MCP server name; the tool surfaces as `mcp__thread_md__update_thread_md`. */
25
+ export declare const THREAD_MD_TOOL_SERVER_NAME = "thread_md";
26
+ /** Raw coords of the session invoking the tool — gates the call + targets the group. */
27
+ export interface GroupMdSessionCoords {
28
+ /** Full channelId (may be a `<groupNo>____<shortId>` thread composite). */
29
+ channelId: string;
30
+ fromUid: string;
31
+ fromName?: string;
32
+ }
33
+ /** Shared deps the tool needs to perform a write-back. */
34
+ export interface GroupMdToolDeps {
35
+ writeback: GroupMdWriteback;
36
+ apiUrl: string;
37
+ botToken: string;
38
+ }
39
+ /** Shared deps the thread write-back tool needs. */
40
+ export interface ThreadMdToolDeps {
41
+ writeback: ThreadMdWriteback;
42
+ apiUrl: string;
43
+ botToken: string;
44
+ }
45
+ /**
46
+ * Build the GROUP.md tool DEFINITIONS for one agent turn. Exported separately
47
+ * from the server so tests can invoke the handler directly. `coords` targets the
48
+ * group + supplies the caller uid; `ownerUid` is the owner gate.
49
+ */
50
+ export declare function buildGroupMdTools(deps: GroupMdToolDeps, coords: GroupMdSessionCoords, ownerUid: string): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
51
+ content: z.ZodString;
52
+ }>[];
53
+ /**
54
+ * Build the GROUP.md write-back MCP server for one agent turn. `coords` targets
55
+ * the group + supplies the caller uid; `ownerUid` is the owner gate.
56
+ */
57
+ export declare function createGroupMdToolServer(deps: GroupMdToolDeps, coords: GroupMdSessionCoords, ownerUid: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
58
+ /**
59
+ * Build the THREAD.md tool DEFINITIONS for one agent turn (P3-2). The thread
60
+ * analogue of {@link buildGroupMdTools}: the tool surfaces as
61
+ * `mcp__thread_md__update_thread_md` and writes THIS thread's OWN THREAD.md
62
+ * (PUT /v1/bot/groups/{groupNo}/threads/{shortId}/md), NEVER the parent group's
63
+ * GROUP.md — the thread/group split is mutually exclusive (#88 P3).
64
+ * index.ts only ever wires the group tool OR this thread tool for a turn (chosen
65
+ * by channelId shape), so the two never co-exist in one session.
66
+ *
67
+ * `coords.channelId` MUST be a thread composite (`<groupNo>____<shortId>`); the
68
+ * caller (index.ts) guarantees this by routing on `isThreadChannelId`. The
69
+ * owner-gate is identical to the group tool (bot-owner-only, independent of the
70
+ * token's server-side thread permission).
71
+ */
72
+ export declare function buildThreadMdTools(deps: ThreadMdToolDeps, coords: GroupMdSessionCoords, ownerUid: string): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
73
+ content: z.ZodString;
74
+ }>[];
75
+ /**
76
+ * Build the THREAD.md write-back MCP server for one agent turn. `coords` targets
77
+ * the thread (its composite channelId) + supplies the caller uid; `ownerUid` is
78
+ * the owner gate.
79
+ */
80
+ export declare function createThreadMdToolServer(deps: ThreadMdToolDeps, coords: GroupMdSessionCoords, ownerUid: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * P2-C: GROUP.md write-back tool — an in-process MCP server letting the agent
3
+ * persist an updated GROUP.md back to the server. The tool surfaces to the model
4
+ * as `mcp__group_md__update_group_md`.
5
+ *
6
+ * The server is built PER TURN (`createGroupMdToolServer`) with the current
7
+ * message's channel coords + the bot owner uid, so:
8
+ * - the write targets the PARENT group of the channel the agent is in
9
+ * (`extractParentGroupNo` — a thread shares its parent group's GROUP.md);
10
+ * - invocation is GATED to the bot owner (registerBot.owner_uid). The group's
11
+ * octo_tag token has server-side write permission, but the agent is driven by
12
+ * untrusted IM users, so this owner gate — not LLM judgment, and independent
13
+ * of the token's group-role permission — is what stops a prompt-injected
14
+ * agent from rewriting the operator's trusted GROUP.md from any chat.
15
+ *
16
+ * Concurrency, the byte ceiling, and the cache refresh are owned by the shared
17
+ * {@link GroupMdWriteback} coordinator (group-md-writeback.ts); this layer is
18
+ * only the owner-gate policy + the MCP surface.
19
+ */
20
+ import { z } from 'zod';
21
+ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
22
+ import { extractParentGroupNo, extractThreadShortId, isThreadChannelId, } from './octo/channel-id.js';
23
+ import { GroupMdContentTooLargeError, MAX_GROUP_MD_CONTENT_BYTES, ThreadMdContentTooLargeError, MAX_THREAD_MD_CONTENT_BYTES, } from './group-md-writeback.js';
24
+ /** MCP server name; the tool surfaces as `mcp__group_md__update_group_md`. */
25
+ export const GROUP_MD_TOOL_SERVER_NAME = 'group_md';
26
+ /** MCP server name; the tool surfaces as `mcp__thread_md__update_thread_md`. */
27
+ export const THREAD_MD_TOOL_SERVER_NAME = 'thread_md';
28
+ function jsonResult(value) {
29
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
30
+ }
31
+ function errResult(msg) {
32
+ return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
33
+ }
34
+ /**
35
+ * Build the GROUP.md tool DEFINITIONS for one agent turn. Exported separately
36
+ * from the server so tests can invoke the handler directly. `coords` targets the
37
+ * group + supplies the caller uid; `ownerUid` is the owner gate.
38
+ */
39
+ export function buildGroupMdTools(deps, coords, ownerUid) {
40
+ const isOwner = coords.fromUid === ownerUid && ownerUid !== '';
41
+ return [
42
+ tool('update_group_md', 'Persist new GROUP.md content for THIS group on the server (it becomes the ' +
43
+ "group's trusted operator instructions on the next turn). `content` is the " +
44
+ 'FULL replacement document, not a diff. Hard limit: 10240 bytes UTF-8. Only ' +
45
+ 'the bot owner may call this; a non-owner request is rejected. Last write ' +
46
+ 'wins server-side — compose the complete updated document, do not assume a ' +
47
+ 'concurrent edit merged.', {
48
+ content: z
49
+ .string()
50
+ .describe('Full replacement GROUP.md document (≤10240 bytes UTF-8).'),
51
+ }, async (args) => {
52
+ try {
53
+ if (!isOwner) {
54
+ return errResult('Only the bot owner can update GROUP.md.');
55
+ }
56
+ const groupNo = extractParentGroupNo(coords.channelId);
57
+ if (!groupNo) {
58
+ return errResult('Could not resolve a group number from this channel.');
59
+ }
60
+ // Surface a friendly over-limit message before the coordinator throws
61
+ // (it re-checks as the authoritative boundary; this is just for UX).
62
+ const bytes = Buffer.byteLength(args.content, 'utf-8');
63
+ if (bytes > MAX_GROUP_MD_CONTENT_BYTES) {
64
+ return errResult(`content is ${bytes} bytes, over the ${MAX_GROUP_MD_CONTENT_BYTES}-byte ` +
65
+ `UTF-8 limit — trim it before writing (the server would reject it).`);
66
+ }
67
+ const res = await deps.writeback.writeBack({
68
+ apiUrl: deps.apiUrl,
69
+ botToken: deps.botToken,
70
+ groupNo,
71
+ content: args.content,
72
+ });
73
+ return jsonResult({
74
+ updated: { groupNo: res.groupNo, version: res.version, bytes: res.bytes },
75
+ });
76
+ }
77
+ catch (err) {
78
+ if (err instanceof GroupMdContentTooLargeError) {
79
+ return errResult(`content is ${err.bytes} bytes, over the ${MAX_GROUP_MD_CONTENT_BYTES}-byte UTF-8 limit.`);
80
+ }
81
+ return errResult(err instanceof Error ? err.message : String(err));
82
+ }
83
+ }),
84
+ ];
85
+ }
86
+ /**
87
+ * Build the GROUP.md write-back MCP server for one agent turn. `coords` targets
88
+ * the group + supplies the caller uid; `ownerUid` is the owner gate.
89
+ */
90
+ export function createGroupMdToolServer(deps, coords, ownerUid) {
91
+ return createSdkMcpServer({
92
+ name: GROUP_MD_TOOL_SERVER_NAME,
93
+ version: '1.0.0',
94
+ tools: buildGroupMdTools(deps, coords, ownerUid),
95
+ });
96
+ }
97
+ /**
98
+ * Build the THREAD.md tool DEFINITIONS for one agent turn (P3-2). The thread
99
+ * analogue of {@link buildGroupMdTools}: the tool surfaces as
100
+ * `mcp__thread_md__update_thread_md` and writes THIS thread's OWN THREAD.md
101
+ * (PUT /v1/bot/groups/{groupNo}/threads/{shortId}/md), NEVER the parent group's
102
+ * GROUP.md — the thread/group split is mutually exclusive (#88 P3).
103
+ * index.ts only ever wires the group tool OR this thread tool for a turn (chosen
104
+ * by channelId shape), so the two never co-exist in one session.
105
+ *
106
+ * `coords.channelId` MUST be a thread composite (`<groupNo>____<shortId>`); the
107
+ * caller (index.ts) guarantees this by routing on `isThreadChannelId`. The
108
+ * owner-gate is identical to the group tool (bot-owner-only, independent of the
109
+ * token's server-side thread permission).
110
+ */
111
+ export function buildThreadMdTools(deps, coords, ownerUid) {
112
+ const isOwner = coords.fromUid === ownerUid && ownerUid !== '';
113
+ return [
114
+ tool('update_thread_md', 'Persist new THREAD.md content for THIS thread (subarea) on the server (it ' +
115
+ "becomes the thread's trusted operator instructions on the next turn). " +
116
+ '`content` is the FULL replacement document, not a diff. Hard limit: 10240 ' +
117
+ 'bytes UTF-8. Only the bot owner may call this; a non-owner request is ' +
118
+ 'rejected. This writes the thread\'s OWN THREAD.md, never the parent ' +
119
+ "group's GROUP.md. Last write wins server-side — compose the complete " +
120
+ 'updated document, do not assume a concurrent edit merged.', {
121
+ content: z
122
+ .string()
123
+ .describe('Full replacement THREAD.md document (≤10240 bytes UTF-8).'),
124
+ }, async (args) => {
125
+ try {
126
+ if (!isOwner) {
127
+ return errResult('Only the bot owner can update THREAD.md.');
128
+ }
129
+ if (!isThreadChannelId(coords.channelId)) {
130
+ return errResult('This channel is not a thread — THREAD.md is only writable from a thread.');
131
+ }
132
+ const groupNo = extractParentGroupNo(coords.channelId);
133
+ const shortId = extractThreadShortId(coords.channelId);
134
+ if (!groupNo || !shortId) {
135
+ return errResult('Could not resolve a thread (groupNo/shortId) from this channel.');
136
+ }
137
+ // Friendly over-limit message before the coordinator throws (it re-checks
138
+ // as the authoritative boundary; this is just for UX).
139
+ const bytes = Buffer.byteLength(args.content, 'utf-8');
140
+ if (bytes > MAX_THREAD_MD_CONTENT_BYTES) {
141
+ return errResult(`content is ${bytes} bytes, over the ${MAX_THREAD_MD_CONTENT_BYTES}-byte ` +
142
+ `UTF-8 limit — trim it before writing (the server would reject it).`);
143
+ }
144
+ const res = await deps.writeback.writeBack({
145
+ apiUrl: deps.apiUrl,
146
+ botToken: deps.botToken,
147
+ groupNo,
148
+ shortId,
149
+ content: args.content,
150
+ });
151
+ return jsonResult({
152
+ updated: {
153
+ groupNo: res.groupNo,
154
+ shortId: res.shortId,
155
+ version: res.version,
156
+ bytes: res.bytes,
157
+ },
158
+ });
159
+ }
160
+ catch (err) {
161
+ if (err instanceof ThreadMdContentTooLargeError) {
162
+ return errResult(`content is ${err.bytes} bytes, over the ${MAX_THREAD_MD_CONTENT_BYTES}-byte UTF-8 limit.`);
163
+ }
164
+ return errResult(err instanceof Error ? err.message : String(err));
165
+ }
166
+ }),
167
+ ];
168
+ }
169
+ /**
170
+ * Build the THREAD.md write-back MCP server for one agent turn. `coords` targets
171
+ * the thread (its composite channelId) + supplies the caller uid; `ownerUid` is
172
+ * the owner gate.
173
+ */
174
+ export function createThreadMdToolServer(deps, coords, ownerUid) {
175
+ return createSdkMcpServer({
176
+ name: THREAD_MD_TOOL_SERVER_NAME,
177
+ version: '1.0.0',
178
+ tools: buildThreadMdTools(deps, coords, ownerUid),
179
+ });
180
+ }
181
+ //# sourceMappingURL=group-md-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-md-tool.js","sourceRoot":"","sources":["../src/group-md-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,EAC1B,4BAA4B,EAC5B,2BAA2B,GAG5B,MAAM,yBAAyB,CAAC;AAEjC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,yBAAyB,GAAG,UAAU,CAAC;AAEpD,gFAAgF;AAChF,MAAM,CAAC,MAAM,0BAA0B,GAAG,WAAW,CAAC;AAwBtD,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC/E,CAAC;AACD,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC/E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAqB,EACrB,MAA4B,EAC5B,QAAgB;IAEhB,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,QAAQ,KAAK,EAAE,CAAC;IAE/D,OAAO;QACL,IAAI,CACF,iBAAiB,EACjB,4EAA4E;YAC1E,4EAA4E;YAC5E,6EAA6E;YAC7E,2EAA2E;YAC3E,4EAA4E;YAC5E,yBAAyB,EAC3B;YACE,OAAO,EAAE,CAAC;iBACP,MAAM,EAAE;iBACR,QAAQ,CAAC,0DAA0D,CAAC;SACxE,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;YACb,IAAI,CAAC;gBACH,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,OAAO,SAAS,CAAC,yCAAyC,CAAC,CAAC;gBAC9D,CAAC;gBACD,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBACvD,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,OAAO,SAAS,CAAC,qDAAqD,CAAC,CAAC;gBAC1E,CAAC;gBACD,sEAAsE;gBACtE,qEAAqE;gBACrE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBACvD,IAAI,KAAK,GAAG,0BAA0B,EAAE,CAAC;oBACvC,OAAO,SAAS,CACd,cAAc,KAAK,oBAAoB,0BAA0B,QAAQ;wBACvE,oEAAoE,CACvE,CAAC;gBACJ,CAAC;gBACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;oBACzC,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,OAAO;oBACP,OAAO,EAAE,IAAI,CAAC,OAAO;iBACtB,CAAC,CAAC;gBACH,OAAO,UAAU,CAAC;oBAChB,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE;iBAC1E,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,2BAA2B,EAAE,CAAC;oBAC/C,OAAO,SAAS,CACd,cAAc,GAAG,CAAC,KAAK,oBAAoB,0BAA0B,oBAAoB,CAC1F,CAAC;gBACJ,CAAC;gBACD,OAAO,SAAS,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACrE,CAAC;QACH,CAAC,CACF;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACrC,IAAqB,EACrB,MAA4B,EAC5B,QAAgB;IAEhB,OAAO,kBAAkB,CAAC;QACxB,IAAI,EAAE,yBAAyB;QAC/B,OAAO,EAAE,OAAO;QAChB,KAAK,EAAE,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC;KACjD,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAsB,EACtB,MAA4B,EAC5B,QAAgB;IAEhB,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,QAAQ,KAAK,EAAE,CAAC;IAE/D,OAAO;QACL,IAAI,CACF,kBAAkB,EAClB,4EAA4E;YAC1E,wEAAwE;YACxE,4EAA4E;YAC5E,wEAAwE;YACxE,sEAAsE;YACtE,uEAAuE;YACvE,2DAA2D,EAC7D;YACE,OAAO,EAAE,CAAC;iBACP,MAAM,EAAE;iBACR,QAAQ,CAAC,2DAA2D,CAAC;SACzE,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;YACb,IAAI,CAAC;gBACH,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,OAAO,SAAS,CAAC,0CAA0C,CAAC,CAAC;gBAC/D,CAAC;gBACD,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;oBACzC,OAAO,SAAS,CAAC,0EAA0E,CAAC,CAAC;gBAC/F,CAAC;gBACD,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBACvD,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBACvD,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;oBACzB,OAAO,SAAS,CAAC,iEAAiE,CAAC,CAAC;gBACtF,CAAC;gBACD,0EAA0E;gBAC1E,uDAAuD;gBACvD,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBACvD,IAAI,KAAK,GAAG,2BAA2B,EAAE,CAAC;oBACxC,OAAO,SAAS,CACd,cAAc,KAAK,oBAAoB,2BAA2B,QAAQ;wBACxE,oEAAoE,CACvE,CAAC;gBACJ,CAAC;gBACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;oBACzC,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,OAAO;oBACP,OAAO;oBACP,OAAO,EAAE,IAAI,CAAC,OAAO;iBACtB,CAAC,CAAC;gBACH,OAAO,UAAU,CAAC;oBAChB,OAAO,EAAE;wBACP,OAAO,EAAE,GAAG,CAAC,OAAO;wBACpB,OAAO,EAAE,GAAG,CAAC,OAAO;wBACpB,OAAO,EAAE,GAAG,CAAC,OAAO;wBACpB,KAAK,EAAE,GAAG,CAAC,KAAK;qBACjB;iBACF,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,4BAA4B,EAAE,CAAC;oBAChD,OAAO,SAAS,CACd,cAAc,GAAG,CAAC,KAAK,oBAAoB,2BAA2B,oBAAoB,CAC3F,CAAC;gBACJ,CAAC;gBACD,OAAO,SAAS,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACrE,CAAC;QACH,CAAC,CACF;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CACtC,IAAsB,EACtB,MAA4B,EAC5B,QAAgB;IAEhB,OAAO,kBAAkB,CAAC;QACxB,IAAI,EAAE,0BAA0B;QAChC,OAAO,EAAE,OAAO;QAChB,KAAK,EAAE,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC;KAClD,CAAC,CAAC;AACL,CAAC"}