@pipeshub-ai/mcp 2.2.0 → 2.3.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.
Files changed (107) hide show
  1. package/README.md +2 -0
  2. package/bin/mcp-server.js +648 -175
  3. package/bin/mcp-server.js.map +17 -11
  4. package/bin/pipeshub.js +946 -0
  5. package/bin/pipeshub.js.map +14 -0
  6. package/esm/cli/client.d.ts +54 -0
  7. package/esm/cli/client.d.ts.map +1 -0
  8. package/esm/cli/client.js +230 -0
  9. package/esm/cli/client.js.map +1 -0
  10. package/esm/cli/commands.d.ts +30 -0
  11. package/esm/cli/commands.d.ts.map +1 -0
  12. package/esm/cli/commands.js +321 -0
  13. package/esm/cli/commands.js.map +1 -0
  14. package/esm/cli/config.d.ts +73 -0
  15. package/esm/cli/config.d.ts.map +1 -0
  16. package/esm/cli/config.js +218 -0
  17. package/esm/cli/config.js.map +1 -0
  18. package/esm/cli/init-qm.d.ts +11 -0
  19. package/esm/cli/init-qm.d.ts.map +1 -0
  20. package/esm/cli/init-qm.js +170 -0
  21. package/esm/cli/init-qm.js.map +1 -0
  22. package/esm/cli/pipeshub.d.ts +2 -0
  23. package/esm/cli/pipeshub.d.ts.map +1 -0
  24. package/esm/cli/pipeshub.js +296 -0
  25. package/esm/cli/pipeshub.js.map +1 -0
  26. package/esm/funcs/connectorLookupRecordByIdentifier.d.ts +68 -0
  27. package/esm/funcs/connectorLookupRecordByIdentifier.d.ts.map +1 -0
  28. package/esm/funcs/connectorLookupRecordByIdentifier.js +138 -0
  29. package/esm/funcs/connectorLookupRecordByIdentifier.js.map +1 -0
  30. package/esm/funcs/connectorNavigateKnowledgeGraph.d.ts +75 -0
  31. package/esm/funcs/connectorNavigateKnowledgeGraph.d.ts.map +1 -0
  32. package/esm/funcs/connectorNavigateKnowledgeGraph.js +152 -0
  33. package/esm/funcs/connectorNavigateKnowledgeGraph.js.map +1 -0
  34. package/esm/lib/base64.d.ts +1 -1
  35. package/esm/lib/base64.d.ts.map +1 -1
  36. package/esm/lib/base64.js +6 -3
  37. package/esm/lib/base64.js.map +1 -1
  38. package/esm/lib/bytes-to-base64.d.ts +16 -0
  39. package/esm/lib/bytes-to-base64.d.ts.map +1 -0
  40. package/esm/lib/bytes-to-base64.js +23 -0
  41. package/esm/lib/bytes-to-base64.js.map +1 -0
  42. package/esm/mcp-server/instructions.d.ts +1 -1
  43. package/esm/mcp-server/instructions.d.ts.map +1 -1
  44. package/esm/mcp-server/instructions.js +30 -0
  45. package/esm/mcp-server/instructions.js.map +1 -1
  46. package/esm/mcp-server/shared.d.ts +2 -2
  47. package/esm/mcp-server/shared.d.ts.map +1 -1
  48. package/esm/mcp-server/shared.js +7 -3
  49. package/esm/mcp-server/shared.js.map +1 -1
  50. package/esm/mcp-server/tools/_agui.d.ts +34 -0
  51. package/esm/mcp-server/tools/_agui.d.ts.map +1 -0
  52. package/esm/mcp-server/tools/_agui.js +91 -0
  53. package/esm/mcp-server/tools/_agui.js.map +1 -0
  54. package/esm/mcp-server/tools/_helpers.d.ts.map +1 -1
  55. package/esm/mcp-server/tools/_helpers.js +13 -6
  56. package/esm/mcp-server/tools/_helpers.js.map +1 -1
  57. package/esm/mcp-server/tools/pipeshubChat.d.ts +0 -3
  58. package/esm/mcp-server/tools/pipeshubChat.d.ts.map +1 -1
  59. package/esm/mcp-server/tools/pipeshubChat.js +75 -98
  60. package/esm/mcp-server/tools/pipeshubChat.js.map +1 -1
  61. package/esm/mcp-server/tools/pipeshubGetRecordContent.d.ts +118 -1
  62. package/esm/mcp-server/tools/pipeshubGetRecordContent.d.ts.map +1 -1
  63. package/esm/mcp-server/tools/pipeshubGetRecordContent.js +180 -29
  64. package/esm/mcp-server/tools/pipeshubGetRecordContent.js.map +1 -1
  65. package/esm/mcp-server/tools/pipeshubSearch.d.ts.map +1 -1
  66. package/esm/mcp-server/tools/pipeshubSearch.js +14 -1
  67. package/esm/mcp-server/tools/pipeshubSearch.js.map +1 -1
  68. package/esm/models/availablemodelsresponse.d.ts +1 -1
  69. package/esm/models/conversation.d.ts +1 -1
  70. package/esm/models/lookuprecordbyidentifierop.d.ts +7 -0
  71. package/esm/models/lookuprecordbyidentifierop.d.ts.map +1 -0
  72. package/esm/models/lookuprecordbyidentifierop.js +14 -0
  73. package/esm/models/lookuprecordbyidentifierop.js.map +1 -0
  74. package/esm/models/navigateknowledgegraphop.d.ts +14 -0
  75. package/esm/models/navigateknowledgegraphop.d.ts.map +1 -0
  76. package/esm/models/navigateknowledgegraphop.js +25 -0
  77. package/esm/models/navigateknowledgegraphop.js.map +1 -0
  78. package/esm/models/userteamsresponse.d.ts +1 -1
  79. package/esm/tool-names.js +3 -3
  80. package/esm/tool-names.js.map +1 -1
  81. package/package.json +3 -2
  82. package/qm/README.md +247 -0
  83. package/qm/SECURITY.md +134 -0
  84. package/qm/TROUBLESHOOTING.md +218 -0
  85. package/qm/qm.config.fragment.jsonc +42 -0
  86. package/qm/sandbox/Dockerfile +22 -0
  87. package/qm/sandbox/skills/pipeshub/SKILL.md +69 -0
  88. package/qm/sandbox/tools/pipeshub/tool.json +36 -0
  89. package/src/cli/client.ts +272 -0
  90. package/src/cli/commands.ts +403 -0
  91. package/src/cli/config.ts +229 -0
  92. package/src/cli/init-qm.ts +215 -0
  93. package/src/cli/pipeshub.ts +333 -0
  94. package/src/funcs/connectorLookupRecordByIdentifier.ts +200 -0
  95. package/src/funcs/connectorNavigateKnowledgeGraph.ts +215 -0
  96. package/src/lib/base64.ts +6 -3
  97. package/src/lib/bytes-to-base64.ts +24 -0
  98. package/src/mcp-server/instructions.ts +30 -0
  99. package/src/mcp-server/shared.ts +7 -3
  100. package/src/mcp-server/tools/_agui.ts +138 -0
  101. package/src/mcp-server/tools/_helpers.ts +13 -6
  102. package/src/mcp-server/tools/pipeshubChat.ts +78 -97
  103. package/src/mcp-server/tools/pipeshubGetRecordContent.ts +213 -29
  104. package/src/mcp-server/tools/pipeshubSearch.ts +14 -1
  105. package/src/models/lookuprecordbyidentifierop.ts +26 -0
  106. package/src/models/navigateknowledgegraphop.ts +56 -0
  107. package/src/tool-names.ts +3 -3
@@ -0,0 +1,200 @@
1
+ /*
2
+ * Adapted from Speakeasy-generated connectorLookupRecordByIdentifier.
3
+ */
4
+
5
+ import { PipeshubCore } from "../core.js";
6
+ import { encodeFormQuery } from "../lib/encodings.js";
7
+ import { compactMap } from "../lib/primitives.js";
8
+ import { safeParse } from "../lib/schemas.js";
9
+ import { RequestOptions } from "../lib/sdks.js";
10
+ import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js";
11
+ import { pathToFunc } from "../lib/url.js";
12
+ import { APIError } from "../models/errors/apierror.js";
13
+ import {
14
+ ConnectionError,
15
+ InvalidRequestError,
16
+ RequestAbortedError,
17
+ RequestTimeoutError,
18
+ UnexpectedClientError,
19
+ } from "../models/errors/httpclienterrors.js";
20
+ import { SDKValidationError } from "../models/errors/sdkvalidationerror.js";
21
+ import {
22
+ LookupRecordByIdentifierRequest,
23
+ LookupRecordByIdentifierRequest$zodSchema,
24
+ } from "../models/lookuprecordbyidentifierop.js";
25
+ import { APICall, APIPromise } from "../types/async.js";
26
+ import { Result } from "../types/fp.js";
27
+
28
+ /**
29
+ * Resolve a URL, issue key or external ID to a Record ID
30
+ *
31
+ * @remarks
32
+ * Turn an external reference into the matching PipesHub record. Accepts a
33
+ * pasted link, a Jira-style issue key, or a bare external system ID from
34
+ * any connected source. Repeat `identifiers` to batch-resolve up to ten
35
+ * in one call.
36
+ *
37
+ * The response carries a rendered `text` view — each match's metadata
38
+ * block followed by a `Next:` line naming a follow-up call. The structured
39
+ * fields carry the same information for programmatic use.
40
+ *
41
+ * **Accepted identifiers**
42
+ * - Jira issue URL — `https://acme.atlassian.net/browse/PA-1787`
43
+ * - Jira issue key — `PA-1787`
44
+ * - Confluence page URL —
45
+ * `https://acme.atlassian.net/wiki/spaces/SD/pages/450625553/Agent+Loop`
46
+ * - Google Drive / Docs URL — `https://docs.google.com/document/d/1AbC.../edit`
47
+ * - Slack message link — `https://acme.slack.com/archives/C0123/p1720000000000100`
48
+ * - Bare external system ID — `450625553`
49
+ *
50
+ * **When to use this vs. the other record endpoints:**
51
+ * - **This endpoint** converts an *external* reference into an internal
52
+ * Record ID. Reach for it whenever you meet a link or ticket key and
53
+ * need the record behind it.
54
+ * - `GET /connectors/record/{recordId}/content` reads a record you have
55
+ * already identified. It needs an internal Record ID, which is exactly
56
+ * what this endpoint returns.
57
+ * - `GET /connectors/navigate` browses the hierarchy when you have a
58
+ * position in the tree rather than a specific identifier.
59
+ *
60
+ * **Typical flow:** call this with the reference, take a match's `id`
61
+ * from the response, then call
62
+ * `GET /connectors/record/{recordId}/content` to read the record.
63
+ *
64
+ * **Multiple matches:** one identifier can legitimately match more than
65
+ * one record — the same external ID may exist in several connected
66
+ * instances. In that case the response sets `ambiguous: true` and
67
+ * `matches` holds every candidate. Present the choice rather than taking
68
+ * the first; `connectorName` narrows a retry.
69
+ *
70
+ * **Misses are not errors.**
71
+ *
72
+ * Only records the caller can see are returned, and a miss is a `200`
73
+ * with an empty `matches` array and the input echoed in
74
+ * `not_found_identifiers` — not a `404`. The identifier resolved to
75
+ * nothing *or* to something the caller may not access; the two are
76
+ * deliberately indistinguishable, because an identifier is
77
+ * caller-supplied and guessable, and confirming existence would leak
78
+ * records across organizations. `searched_connectors` names what was
79
+ * covered, so a retry with `connectorName` is often the right next move.
80
+ *
81
+ * **Scope:** resolution searches every connector the caller can access,
82
+ * regardless of any source filter used elsewhere.
83
+ *
84
+ * If set, this operation will use either {@link Security.bearerAuth} or {@link Security.oauth2} from the global security.
85
+ */
86
+ export function connectorLookupRecordByIdentifier(
87
+ client$: PipeshubCore,
88
+ request: LookupRecordByIdentifierRequest,
89
+ options?: RequestOptions,
90
+ ): APIPromise<
91
+ Result<
92
+ Response,
93
+ | APIError
94
+ | SDKValidationError
95
+ | UnexpectedClientError
96
+ | InvalidRequestError
97
+ | RequestAbortedError
98
+ | RequestTimeoutError
99
+ | ConnectionError
100
+ >
101
+ > {
102
+ return new APIPromise($do(
103
+ client$,
104
+ request,
105
+ options,
106
+ ));
107
+ }
108
+
109
+ async function $do(
110
+ client$: PipeshubCore,
111
+ request: LookupRecordByIdentifierRequest,
112
+ options?: RequestOptions,
113
+ ): Promise<
114
+ [
115
+ Result<
116
+ Response,
117
+ | APIError
118
+ | SDKValidationError
119
+ | UnexpectedClientError
120
+ | InvalidRequestError
121
+ | RequestAbortedError
122
+ | RequestTimeoutError
123
+ | ConnectionError
124
+ >,
125
+ APICall,
126
+ ]
127
+ > {
128
+ const parsed$ = safeParse(
129
+ request,
130
+ (value$) => LookupRecordByIdentifierRequest$zodSchema.parse(value$),
131
+ "Input validation failed",
132
+ );
133
+ if (!parsed$.ok) {
134
+ return [parsed$, { status: "invalid" }];
135
+ }
136
+ const payload$ = parsed$.value;
137
+ const body$ = null;
138
+ const path$ = pathToFunc("/connectors/record/lookup")();
139
+ const query$ = encodeFormQuery({
140
+ "connectorName": payload$.connectorName,
141
+ "identifiers": payload$.identifiers,
142
+ });
143
+
144
+ const headers$ = new Headers(compactMap({
145
+ Accept: "application/json",
146
+ }));
147
+ const securityInput = await extractSecurity(client$._options.security);
148
+ const requestSecurity = resolveGlobalSecurity(securityInput);
149
+
150
+ const context = {
151
+ options: client$._options,
152
+ baseURL: options?.serverURL ?? client$._baseURL ?? "",
153
+ operationID: "lookupRecordByIdentifier",
154
+ oAuth2Scopes: ["kb:read", "connector:read"],
155
+ resolvedSecurity: requestSecurity,
156
+ securitySource: client$._options.security,
157
+ retryConfig: options?.retries
158
+ || client$._options.retryConfig
159
+ || { strategy: "none" },
160
+ retryCodes: options?.retryCodes || [
161
+ "429",
162
+ "500",
163
+ "502",
164
+ "503",
165
+ "504",
166
+ ],
167
+ };
168
+
169
+ const requestRes = client$._createRequest(context, {
170
+ security: requestSecurity,
171
+ method: "GET",
172
+ baseURL: options?.serverURL,
173
+ path: path$,
174
+ headers: headers$,
175
+ query: query$,
176
+ body: body$,
177
+ userAgent: client$._options.userAgent,
178
+ timeoutMs: options?.timeoutMs || client$._options.timeoutMs
179
+ || -1,
180
+ }, options);
181
+ if (!requestRes.ok) {
182
+ return [requestRes, { status: "invalid" }];
183
+ }
184
+ const req$ = requestRes.value;
185
+
186
+ const doResult = await client$._do(req$, {
187
+ context,
188
+ errorCodes: [],
189
+ retryConfig: context.retryConfig,
190
+ retryCodes: context.retryCodes,
191
+ });
192
+ if (!doResult.ok) {
193
+ return [doResult, { status: "request-error", request: req$ }];
194
+ }
195
+ return [doResult, {
196
+ status: "complete",
197
+ "request": req$,
198
+ response: doResult.value,
199
+ }];
200
+ }
@@ -0,0 +1,215 @@
1
+ /*
2
+ * Adapted from Speakeasy-generated connectorNavigateKnowledgeGraph.
3
+ */
4
+
5
+ import { PipeshubCore } from "../core.js";
6
+ import { encodeFormQuery } from "../lib/encodings.js";
7
+ import { compactMap } from "../lib/primitives.js";
8
+ import { safeParse } from "../lib/schemas.js";
9
+ import { RequestOptions } from "../lib/sdks.js";
10
+ import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js";
11
+ import { pathToFunc } from "../lib/url.js";
12
+ import { APIError } from "../models/errors/apierror.js";
13
+ import {
14
+ ConnectionError,
15
+ InvalidRequestError,
16
+ RequestAbortedError,
17
+ RequestTimeoutError,
18
+ UnexpectedClientError,
19
+ } from "../models/errors/httpclienterrors.js";
20
+ import { SDKValidationError } from "../models/errors/sdkvalidationerror.js";
21
+ import {
22
+ NavigateKnowledgeGraphRequest,
23
+ NavigateKnowledgeGraphRequest$zodSchema,
24
+ } from "../models/navigateknowledgegraphop.js";
25
+ import { APICall, APIPromise } from "../types/async.js";
26
+ import { Result } from "../types/fp.js";
27
+
28
+ /**
29
+ * Browse the knowledge graph from a node
30
+ *
31
+ * @remarks
32
+ * Open a node in the knowledge graph and see what is inside it — a file
33
+ * explorer across every connected source.
34
+ *
35
+ * Call it with no `nodeId` for a flat listing of every record group and
36
+ * record the caller can reach, newest first. This is a starting point to
37
+ * pick a node from, not a roster of connected apps — app nodes are never
38
+ * returned in a listing, though an app's `id` is accepted as a `nodeId`
39
+ * and lists that app's record groups. Pass a node's `id` to descend:
40
+ * record groups contain records and folders, and a record contains its
41
+ * own children — comments, attachments, sub-tasks — plus a `related`
42
+ * section of cross-referenced records, such as the Confluence page linked
43
+ * from a Jira ticket. `nodeId` is tolerant: a URL or an issue key such as
44
+ * `PA-1787` is resolved to its record before navigating, so a link can be
45
+ * pasted straight in without a separate lookup call.
46
+ *
47
+ * The response carries a rendered `text` view — breadcrumbs, the current
48
+ * node, the children listing, `Related:`, and a closing `Next:` line
49
+ * naming a follow-up call. The structured fields carry the same
50
+ * information for programmatic use.
51
+ *
52
+ * **When to use this vs. the other record endpoints:**
53
+ * - **This endpoint** is for structural exploration — "what is in this
54
+ * project", "what is attached to this ticket", "what else links to this
55
+ * page". It returns names, types and IDs; it never returns document
56
+ * text.
57
+ * - `GET /connectors/record/{recordId}/content` returns one record's
58
+ * actual parsed text. Use it once navigation has identified the record
59
+ * you want to read.
60
+ * - `GET /connectors/record/lookup` is the way in when you hold a URL or
61
+ * an issue key rather than a position in the tree.
62
+ *
63
+ * **Typical flow:** call with no `nodeId` to see what is reachable → pass
64
+ * a record group's `id` to list its records → take a row whose
65
+ * `is_record` is true and call
66
+ * `GET /connectors/record/{recordId}/content` to read it.
67
+ *
68
+ * **Paging and depth:** results are paginated; `pagination.has_next`
69
+ * tells you whether to request the next `page`. `depth` above 1 returns
70
+ * all descendants down to that level as one flat list, each row carrying
71
+ * its own `level`, instead of only direct children.
72
+ *
73
+ * **Scope:** everything the caller can read, across both connectors and
74
+ * Knowledge Base collections. No connector-level filter is applied — the
75
+ * listing is bounded by per-node permissions alone.
76
+ *
77
+ * **Permission scoping:**
78
+ *
79
+ * `rows` and `related` carry only nodes the caller can see, and the
80
+ * opened node itself is access-checked before any of its details are
81
+ * returned. A node that does not exist and a node the caller cannot
82
+ * access are deliberately indistinguishable — both return an empty view
83
+ * rather than an error.
84
+ *
85
+ * `breadcrumbs` is the exception: the ancestor trail is resolved by id
86
+ * alone, without a permission check. For a record shared directly with
87
+ * the caller, it can therefore name ancestors the caller cannot open.
88
+ * Treat breadcrumb entries as labels, not as nodes guaranteed to be
89
+ * navigable.
90
+ *
91
+ * If set, this operation will use either {@link Security.bearerAuth} or {@link Security.oauth2} from the global security.
92
+ */
93
+ export function connectorNavigateKnowledgeGraph(
94
+ client$: PipeshubCore,
95
+ request?: NavigateKnowledgeGraphRequest | undefined,
96
+ options?: RequestOptions,
97
+ ): APIPromise<
98
+ Result<
99
+ Response,
100
+ | APIError
101
+ | SDKValidationError
102
+ | UnexpectedClientError
103
+ | InvalidRequestError
104
+ | RequestAbortedError
105
+ | RequestTimeoutError
106
+ | ConnectionError
107
+ >
108
+ > {
109
+ return new APIPromise($do(
110
+ client$,
111
+ request,
112
+ options,
113
+ ));
114
+ }
115
+
116
+ async function $do(
117
+ client$: PipeshubCore,
118
+ request?: NavigateKnowledgeGraphRequest | undefined,
119
+ options?: RequestOptions,
120
+ ): Promise<
121
+ [
122
+ Result<
123
+ Response,
124
+ | APIError
125
+ | SDKValidationError
126
+ | UnexpectedClientError
127
+ | InvalidRequestError
128
+ | RequestAbortedError
129
+ | RequestTimeoutError
130
+ | ConnectionError
131
+ >,
132
+ APICall,
133
+ ]
134
+ > {
135
+ const parsed$ = safeParse(
136
+ request,
137
+ (value$) =>
138
+ NavigateKnowledgeGraphRequest$zodSchema.optional().parse(value$),
139
+ "Input validation failed",
140
+ );
141
+ if (!parsed$.ok) {
142
+ return [parsed$, { status: "invalid" }];
143
+ }
144
+ const payload$ = parsed$.value;
145
+ const body$ = null;
146
+ const path$ = pathToFunc("/connectors/navigate")();
147
+ const query$ = encodeFormQuery({
148
+ "createdAfter": payload$?.createdAfter,
149
+ "createdBefore": payload$?.createdBefore,
150
+ "depth": payload$?.depth,
151
+ "limit": payload$?.limit,
152
+ "modifiedAfter": payload$?.modifiedAfter,
153
+ "modifiedBefore": payload$?.modifiedBefore,
154
+ "nodeId": payload$?.nodeId,
155
+ "nodeTypes": payload$?.nodeTypes,
156
+ "page": payload$?.page,
157
+ });
158
+
159
+ const headers$ = new Headers(compactMap({
160
+ Accept: "application/json",
161
+ }));
162
+ const securityInput = await extractSecurity(client$._options.security);
163
+ const requestSecurity = resolveGlobalSecurity(securityInput);
164
+
165
+ const context = {
166
+ options: client$._options,
167
+ baseURL: options?.serverURL ?? client$._baseURL ?? "",
168
+ operationID: "navigateKnowledgeGraph",
169
+ oAuth2Scopes: ["kb:read", "connector:read"],
170
+ resolvedSecurity: requestSecurity,
171
+ securitySource: client$._options.security,
172
+ retryConfig: options?.retries
173
+ || client$._options.retryConfig
174
+ || { strategy: "none" },
175
+ retryCodes: options?.retryCodes || [
176
+ "429",
177
+ "500",
178
+ "502",
179
+ "503",
180
+ "504",
181
+ ],
182
+ };
183
+
184
+ const requestRes = client$._createRequest(context, {
185
+ security: requestSecurity,
186
+ method: "GET",
187
+ baseURL: options?.serverURL,
188
+ path: path$,
189
+ headers: headers$,
190
+ query: query$,
191
+ body: body$,
192
+ userAgent: client$._options.userAgent,
193
+ timeoutMs: options?.timeoutMs || client$._options.timeoutMs
194
+ || -1,
195
+ }, options);
196
+ if (!requestRes.ok) {
197
+ return [requestRes, { status: "invalid" }];
198
+ }
199
+ const req$ = requestRes.value;
200
+
201
+ const doResult = await client$._do(req$, {
202
+ context,
203
+ errorCodes: [],
204
+ retryConfig: context.retryConfig,
205
+ retryCodes: context.retryCodes,
206
+ });
207
+ if (!doResult.ok) {
208
+ return [doResult, { status: "request-error", request: req$ }];
209
+ }
210
+ return [doResult, {
211
+ status: "complete",
212
+ "request": req$,
213
+ response: doResult.value,
214
+ }];
215
+ }
package/src/lib/base64.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  /*
2
2
  * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
3
+ *
4
+ * Exception: `bytesToBase64` is re-exported from the hand-maintained
5
+ * `./bytes-to-base64.js` (see that file and `.genignore`). Do not restore
6
+ * the generated one-liner — it overflows the stack on binary payloads.
3
7
  */
4
8
 
5
9
  import * as z from "zod";
6
10
 
7
- export function bytesToBase64(u8arr: Uint8Array): string {
8
- return btoa(String.fromCodePoint(...u8arr));
9
- }
11
+ export { bytesToBase64 } from "./bytes-to-base64.js";
12
+ import { bytesToBase64 } from "./bytes-to-base64.js";
10
13
 
11
14
  export function bytesFromBase64(encoded: string): Uint8Array {
12
15
  return Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0));
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Hand-maintained base64 encoder for binary payloads.
3
+ *
4
+ * Speakeasy's generated helper is:
5
+ * btoa(String.fromCodePoint(...u8arr))
6
+ * which passes one argument per byte and throws "Maximum call stack size
7
+ * exceeded" once the array is larger than the engine's argument limit
8
+ * (between 64 KB and 128 KB on Node 24). That is smaller than a typical
9
+ * PDF or image, so binary MCP results fail outright.
10
+ *
11
+ * Keep this file out of Speakeasy regeneration (see `.genignore`). The
12
+ * generated wrappers in `src/lib/base64.ts` and `src/mcp-server/shared.ts`
13
+ * re-export / call through here.
14
+ */
15
+
16
+ const BASE64_CHUNK = 0x8000;
17
+
18
+ export function bytesToBase64(u8arr: Uint8Array): string {
19
+ let binary = "";
20
+ for (let i = 0; i < u8arr.length; i += BASE64_CHUNK) {
21
+ binary += String.fromCharCode(...u8arr.subarray(i, i + BASE64_CHUNK));
22
+ }
23
+ return btoa(binary);
24
+ }
@@ -71,6 +71,36 @@ the user can verify.
71
71
  - \`pipeshub_agents\` — list the org's configured **agents** (specialized
72
72
  assistants with their own prompt, tools, and knowledge scope).
73
73
 
74
+ ## Knowledge Sources
75
+
76
+ A source (an app connector or a KB) holds record groups (a project, space,
77
+ drive, folder), which hold records (one ticket, page, file). Records nest —
78
+ an epic over its stories, a page over its child pages.
79
+
80
+ **Every tool returns metadata or content.**
81
+
82
+ - \`pipeshub_search\` / \`pipeshub_chat\` — content, but only a ranked sample of
83
+ blocks: never all blocks of a record, never every record that matches, never
84
+ a complete list of what exists.
85
+ - \`mode:"lookup"\` and \`mode:"navigate"\` — metadata: recordIds, fields, and
86
+ structure. No document text. \`mode:"content"\` — one whole record.
87
+
88
+ **Combining them.** Search finds a starting point by wording; navigate on a
89
+ \`recordId\` reveals what search structurally cannot — siblings, children,
90
+ linked records, and the group's total count. For a question that must be
91
+ exhaustive ("all", "how many", "every"), do NOT count search hits or trust a
92
+ chat answer: navigate the record group, which reports the real total. For a
93
+ record's siblings, navigate its parent — take the id from the \`Path\` line.
94
+
95
+ **A named record** — an issue key, a Confluence / Drive / Slack link, an
96
+ external ID → \`mode:"lookup"\` FIRST. It returns that record's metadata (for a
97
+ ticket: status, assignee, priority, dates) plus its \`recordId\`, which often
98
+ answers the question outright. Never guess a \`recordId\`.
99
+
100
+ Pass a source's id from \`pipeshub_sources\` as \`apps: ["<id>"]\` to search or
101
+ chat, and as \`nodeId\` to navigate. Navigate and lookup end in a \`Next:\` line
102
+ naming the exact follow-up call — follow it.
103
+
74
104
  ## Agents
75
105
 
76
106
  Some orgs configure **agents** for specific jobs (e.g. a Slack messenger, a
@@ -1,8 +1,14 @@
1
1
  /*
2
2
  * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
3
+ *
4
+ * Exception: `bytesToBase64` is imported from the hand-maintained
5
+ * `../lib/bytes-to-base64.js` (see that file and `.genignore`). Do not
6
+ * restore the generated one-liner — it overflows the stack on binary
7
+ * payloads.
3
8
  */
4
9
 
5
10
  import * as z from "zod";
11
+ import { bytesToBase64 } from "../lib/bytes-to-base64.js";
6
12
 
7
13
  type BinaryData =
8
14
  | Uint8Array
@@ -14,9 +20,7 @@ type BinaryData =
14
20
 
15
21
  const base64Schema = z.string().base64();
16
22
 
17
- export function bytesToBase64(u8arr: Uint8Array): string {
18
- return btoa(String.fromCodePoint(...u8arr));
19
- }
23
+ export { bytesToBase64 };
20
24
 
21
25
  export async function consumeStream(
22
26
  stream: ReadableStream<Uint8Array>,
@@ -0,0 +1,138 @@
1
+ // AG-UI frame folding for the chat stream.
2
+ //
3
+ // AG-UI is the only wire protocol the conversation and agent stream routes
4
+ // speak; the legacy `connected` / `answer_chunk` / `complete` / `error`
5
+ // vocabulary was removed with the new agent loop and is never emitted.
6
+ //
7
+ // Kept separate from `pipeshubChat.ts` so the fold is a pure function over
8
+ // frames and can be exercised without a live stream.
9
+
10
+ import type { SSEFrame } from "./_helpers.js";
11
+
12
+ export const AGUIEvent = {
13
+ RUN_FINISHED: "RUN_FINISHED",
14
+ RUN_ERROR: "RUN_ERROR",
15
+ CUSTOM: "CUSTOM",
16
+ TEXT_MESSAGE_CONTENT: "TEXT_MESSAGE_CONTENT",
17
+ } as const;
18
+
19
+ export type ChatStreamState = {
20
+ /** Persisted conversation from the terminal frame; null until then. */
21
+ conversation: any | null;
22
+ recordsUsed: number | undefined;
23
+ error: string | null;
24
+ /** From `CUSTOM{name:"conversation_created"}`, available before the answer. */
25
+ conversationId: string | null;
26
+ title: string | null;
27
+ /**
28
+ * Answer text accumulated per `messageId`. Universal `agent` mode forwards
29
+ * child-run text alongside the root answer, so a single buffer would splice
30
+ * sub-agent output into the middle of the reply.
31
+ */
32
+ text: Map<string, string>;
33
+ /** messageIds seen on frames with no `parentRunId` — i.e. the root run. */
34
+ rootMessageIds: Set<string>;
35
+ };
36
+
37
+ export function newChatStreamState(): ChatStreamState {
38
+ return {
39
+ conversation: null,
40
+ recordsUsed: undefined,
41
+ error: null,
42
+ conversationId: null,
43
+ title: null,
44
+ text: new Map(),
45
+ rootMessageIds: new Set(),
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Fold one frame into `state`. Returns true when the frame is terminal and the
51
+ * caller should stop draining the stream.
52
+ */
53
+ /** The subset of AG-UI payload fields this fold reads. */
54
+ type FrameData = {
55
+ result?: {
56
+ conversation?: unknown;
57
+ recordsUsed?: number;
58
+ meta?: { recordsUsed?: number };
59
+ };
60
+ message?: string;
61
+ error?: string;
62
+ name?: string;
63
+ value?: { conversationId?: string; title?: string };
64
+ delta?: string;
65
+ messageId?: string;
66
+ parentRunId?: string | null;
67
+ };
68
+
69
+ export function applyAGUIFrame(
70
+ state: ChatStreamState,
71
+ frame: SSEFrame,
72
+ ): boolean {
73
+ const data = frame.data;
74
+ const d: FrameData = data && typeof data === "object"
75
+ ? data as FrameData
76
+ : {};
77
+
78
+ switch (frame.event) {
79
+ case AGUIEvent.RUN_FINISHED: {
80
+ // Only the gateway's own re-emitted frame carries `result` — it swallows
81
+ // the AI backend's root RUN_FINISHED and re-sends an enriched one after
82
+ // persisting. Sub-agent RUN_FINISHED frames are forwarded verbatim with
83
+ // no `result`, so keying off the event name alone would end the stream
84
+ // as soon as the first sub-agent finished.
85
+ const result = d.result;
86
+ if (!result || typeof result !== "object") return false;
87
+ state.conversation = result.conversation ?? null;
88
+ state.recordsUsed = result.recordsUsed ?? result.meta?.recordsUsed;
89
+ return true;
90
+ }
91
+
92
+ case AGUIEvent.RUN_ERROR: {
93
+ state.error = (typeof data === "string" ? data : null)
94
+ ?? d.message ?? d.error ?? frame.raw ?? "Stream error";
95
+ return true;
96
+ }
97
+
98
+ case AGUIEvent.CUSTOM: {
99
+ if (d.name === "conversation_created") {
100
+ const value = d.value ?? {};
101
+ state.conversationId = value.conversationId ?? state.conversationId;
102
+ state.title = value.title ?? state.title;
103
+ }
104
+ return false;
105
+ }
106
+
107
+ case AGUIEvent.TEXT_MESSAGE_CONTENT: {
108
+ if (typeof d.delta !== "string" || d.delta.length === 0) return false;
109
+ const id = typeof d.messageId === "string" ? d.messageId : "";
110
+ state.text.set(id, (state.text.get(id) ?? "") + d.delta);
111
+ if (d.parentRunId == null) state.rootMessageIds.add(id);
112
+ return false;
113
+ }
114
+
115
+ // RUN_STARTED, STEP_*, TOOL_CALL_*, REASONING_*, STATE_*,
116
+ // TEXT_MESSAGE_START/END, HEARTBEAT — lifecycle noise.
117
+ default:
118
+ return false;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Best-effort answer when the stream ended without a terminal frame.
124
+ *
125
+ * Prefers the longest root-run buffer; falls back to the longest buffer of any
126
+ * run so a partial answer still reaches the caller.
127
+ */
128
+ export function salvagedText(state: ChatStreamState): string | null {
129
+ const pick = (ids: Iterable<string>): string | null => {
130
+ let best: string | null = null;
131
+ for (const id of ids) {
132
+ const text = state.text.get(id);
133
+ if (text && (best === null || text.length > best.length)) best = text;
134
+ }
135
+ return best;
136
+ };
137
+ return pick(state.rootMessageIds) ?? pick(state.text.keys());
138
+ }
@@ -224,17 +224,24 @@ export function trimConversation(conv: any) {
224
224
  export function trimCitations(citations: unknown) {
225
225
  if (!Array.isArray(citations)) return [];
226
226
  return citations.map((c: any) => {
227
- const md = c?.metadata ?? {};
227
+ // Two wire shapes exist. Flat: `{ content, chunkIndex, metadata }`.
228
+ // Nested (current backend): `{ citationId, citationData: { content,
229
+ // chunkIndex, metadata } }`. Reading only the flat one silently yields
230
+ // a citation with every field `undefined` — which JSON.stringify drops,
231
+ // leaving `{"snippet": null}` and no way to identify the source at all.
232
+ const cd = c?.citationData ?? c;
233
+ const md = cd?.metadata ?? c?.metadata ?? {};
234
+ const content = typeof cd?.content === "string" ? cd.content : c?.content;
228
235
  return {
229
- recordId: md.recordId ?? c?.recordId,
230
- recordName: md.recordName ?? c?.recordName,
231
- snippet: typeof c?.content === "string"
232
- ? c.content.slice(0, 280)
236
+ recordId: md.recordId ?? cd?.recordId ?? c?.recordId,
237
+ recordName: md.recordName ?? cd?.recordName ?? c?.recordName,
238
+ snippet: typeof content === "string"
239
+ ? content.slice(0, 280)
233
240
  : md.blockText ?? null,
234
241
  mimeType: md.mimeType,
235
242
  webUrl: md.webUrl,
236
243
  pageNum: md.pageNum,
237
- chunkIndex: c?.chunkIndex ?? md.chunkIndex,
244
+ chunkIndex: cd?.chunkIndex ?? c?.chunkIndex ?? md.chunkIndex,
238
245
  };
239
246
  });
240
247
  }