@thallylabs/mcp 0.10.21 → 0.10.23
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/dist/index.js +438 -142
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +438 -142
- package/dist/track.d.ts +14 -8
- package/dist/track.js +81 -21
- package/package.json +2 -2
package/dist/track.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
/** The legacy docs-agent branch namespace. Retained for API compatibility. */
|
|
2
|
+
declare const AGENT_BRANCH_PREFIX = "thally/agent-";
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Every branch namespace authored by a Track-controlled writer.
|
|
5
|
+
*
|
|
6
|
+
* Intake must ignore all of these namespaces. Otherwise a self-tracking docs
|
|
7
|
+
* repository can treat Track's own documentation pull request as a new product
|
|
8
|
+
* change and recursively create another run. Keep this list shared by Cloud,
|
|
9
|
+
* generated sender workflows, and any future Track producer.
|
|
6
10
|
*/
|
|
7
|
-
declare const
|
|
11
|
+
declare const TRACK_OWNED_BRANCH_PREFIXES: readonly ["thally/agent-", "thally/track-vnext/"];
|
|
12
|
+
/** Return whether a branch or full `refs/heads/` ref belongs to Track. */
|
|
13
|
+
declare function isTrackOwnedBranch(ref: string | null | undefined): boolean;
|
|
8
14
|
/** Label that turns an OPEN PR into a preview-docs request (shared by the
|
|
9
15
|
* webhook relay and the scaffolded sender workflow). */
|
|
10
16
|
declare const DOCS_PREVIEW_LABEL = "docs-preview";
|
|
@@ -123,11 +129,11 @@ declare const TRACK_CONTEXT_CHAR_CAP = 20000;
|
|
|
123
129
|
* The instruction frames the agent's actual job: judge what user-facing
|
|
124
130
|
* behavior the merged PR changed and update the docs that describe it.
|
|
125
131
|
*/
|
|
126
|
-
declare function buildTrackInstruction(repo: TrackedRepoLike, pr: Pick<PullRequestInfo,
|
|
132
|
+
declare function buildTrackInstruction(repo: TrackedRepoLike, pr: Pick<PullRequestInfo, "number">, options?: {
|
|
127
133
|
preview?: boolean;
|
|
128
134
|
}): string;
|
|
129
135
|
/** The capped markdown context for a merged tracked PR (title, body, file diffs). */
|
|
130
|
-
declare function buildTrackContext(repo: Pick<TrackedRepoLike,
|
|
136
|
+
declare function buildTrackContext(repo: Pick<TrackedRepoLike, "owner" | "repo">, pr: PullRequestInfo, files: Array<PrFile>): string;
|
|
131
137
|
/**
|
|
132
138
|
* Distill a merged PR into the docs task the agent runs: a one-line instruction
|
|
133
139
|
* (output routing) and a capped markdown context (PR description + diff).
|
|
@@ -137,4 +143,4 @@ declare function buildTrackTask(repo: TrackedRepoLike, pr: PullRequestInfo, file
|
|
|
137
143
|
context: string;
|
|
138
144
|
};
|
|
139
145
|
|
|
140
|
-
export { AGENT_BRANCH_PREFIX, DOCS_PREVIEW_LABEL, type GithubAppCreds, type GithubFetchOptions, type OwnerRepoRef, type PrFile, type PullRequestInfo, TRACK_CONTEXT_CHAR_CAP, type TrackedRepoLike, buildTrackContext, buildTrackInstruction, buildTrackTask, createAppJwt, fetchLatestMergedPr, fetchPullRequest, fetchPullRequestFiles, filterFilesByGlobs, matchesGlob, mintInstallationToken, parseOwnerRepo, resolveGithubToken, verifyInstallationBelongsToApp };
|
|
146
|
+
export { AGENT_BRANCH_PREFIX, DOCS_PREVIEW_LABEL, type GithubAppCreds, type GithubFetchOptions, type OwnerRepoRef, type PrFile, type PullRequestInfo, TRACK_CONTEXT_CHAR_CAP, TRACK_OWNED_BRANCH_PREFIXES, type TrackedRepoLike, buildTrackContext, buildTrackInstruction, buildTrackTask, createAppJwt, fetchLatestMergedPr, fetchPullRequest, fetchPullRequestFiles, filterFilesByGlobs, isTrackOwnedBranch, matchesGlob, mintInstallationToken, parseOwnerRepo, resolveGithubToken, verifyInstallationBelongsToApp };
|
package/dist/track.js
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
// src/lib/track.ts
|
|
2
2
|
import { createSign, createHash } from "crypto";
|
|
3
3
|
var AGENT_BRANCH_PREFIX = "thally/agent-";
|
|
4
|
+
var TRACK_OWNED_BRANCH_PREFIXES = [
|
|
5
|
+
AGENT_BRANCH_PREFIX,
|
|
6
|
+
"thally/track-vnext/"
|
|
7
|
+
];
|
|
8
|
+
function isTrackOwnedBranch(ref) {
|
|
9
|
+
if (typeof ref !== "string") return false;
|
|
10
|
+
const branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
|
|
11
|
+
return TRACK_OWNED_BRANCH_PREFIXES.some(
|
|
12
|
+
(prefix) => branch.startsWith(prefix)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
4
15
|
var DOCS_PREVIEW_LABEL = "docs-preview";
|
|
5
16
|
function parseOwnerRepo(spec) {
|
|
6
17
|
const trimmed = spec.trim();
|
|
7
18
|
const url = trimmed.match(
|
|
8
19
|
/^https?:\/\/github\.com\/([^/\s]+)\/([^/\s#?]+?)(?:\.git)?(?:\/pull\/(\d+))?(?:[/#?].*)?$/i
|
|
9
20
|
);
|
|
10
|
-
if (url)
|
|
11
|
-
|
|
21
|
+
if (url)
|
|
22
|
+
return {
|
|
23
|
+
owner: url[1],
|
|
24
|
+
repo: url[2],
|
|
25
|
+
...url[3] ? { pr: Number(url[3]) } : {}
|
|
26
|
+
};
|
|
27
|
+
const plain = trimmed.match(
|
|
28
|
+
/^([A-Za-z0-9-_.]+)\/([A-Za-z0-9-_.]+?)(?:#(\d+))?$/
|
|
29
|
+
);
|
|
12
30
|
if (!plain) return null;
|
|
13
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
owner: plain[1],
|
|
33
|
+
repo: plain[2],
|
|
34
|
+
...plain[3] ? { pr: Number(plain[3]) } : {}
|
|
35
|
+
};
|
|
14
36
|
}
|
|
15
37
|
function base64url(input) {
|
|
16
38
|
return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
@@ -19,8 +41,12 @@ var installationTokenCache = /* @__PURE__ */ new Map();
|
|
|
19
41
|
function createAppJwt(appId, privateKey) {
|
|
20
42
|
const now = Math.floor(Date.now() / 1e3);
|
|
21
43
|
const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
22
|
-
const payload = base64url(
|
|
23
|
-
|
|
44
|
+
const payload = base64url(
|
|
45
|
+
JSON.stringify({ iat: now - 60, exp: now + 9 * 60, iss: String(appId) })
|
|
46
|
+
);
|
|
47
|
+
const signature = base64url(
|
|
48
|
+
createSign("RSA-SHA256").update(`${header}.${payload}`).sign(privateKey)
|
|
49
|
+
);
|
|
24
50
|
return `${header}.${payload}.${signature}`;
|
|
25
51
|
}
|
|
26
52
|
async function mintInstallationToken(creds, fetchImpl = fetch) {
|
|
@@ -29,12 +55,20 @@ async function mintInstallationToken(creds, fetchImpl = fetch) {
|
|
|
29
55
|
const cached = installationTokenCache.get(cacheKey);
|
|
30
56
|
if (cached && cached.expiresAtMs - 6e4 > Date.now()) return cached.token;
|
|
31
57
|
const jwt = createAppJwt(creds.appId, creds.privateKey);
|
|
32
|
-
const res = await fetchImpl(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
const res = await fetchImpl(
|
|
59
|
+
`https://api.github.com/app/installations/${creds.installationId}/access_tokens`,
|
|
60
|
+
{
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
Accept: "application/vnd.github+json",
|
|
64
|
+
Authorization: `Bearer ${jwt}`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
);
|
|
36
68
|
if (!res.ok) {
|
|
37
|
-
throw new Error(
|
|
69
|
+
throw new Error(
|
|
70
|
+
`GitHub App token exchange failed (${res.status}) \u2014 check the app id, installation id, and private key.`
|
|
71
|
+
);
|
|
38
72
|
}
|
|
39
73
|
const body = await res.json();
|
|
40
74
|
const parsed = Date.parse(body.expires_at);
|
|
@@ -44,9 +78,15 @@ async function mintInstallationToken(creds, fetchImpl = fetch) {
|
|
|
44
78
|
}
|
|
45
79
|
async function verifyInstallationBelongsToApp(appId, privateKey, installationId, fetchImpl = fetch) {
|
|
46
80
|
const jwt = createAppJwt(appId, privateKey);
|
|
47
|
-
const res = await fetchImpl(
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
const res = await fetchImpl(
|
|
82
|
+
`https://api.github.com/app/installations/${installationId}`,
|
|
83
|
+
{
|
|
84
|
+
headers: {
|
|
85
|
+
Accept: "application/vnd.github+json",
|
|
86
|
+
Authorization: `Bearer ${jwt}`
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
);
|
|
50
90
|
if (!res.ok) return false;
|
|
51
91
|
const body = await res.json();
|
|
52
92
|
return String(body.app_id) === String(appId);
|
|
@@ -55,7 +95,8 @@ function envAppCreds() {
|
|
|
55
95
|
const appId = (process.env.THALLY_GITHUB_APP_ID ?? process.env.DOX_GITHUB_APP_ID)?.trim();
|
|
56
96
|
const installationId = (process.env.THALLY_GITHUB_APP_INSTALLATION_ID ?? process.env.DOX_GITHUB_APP_INSTALLATION_ID)?.trim();
|
|
57
97
|
const privateKey = process.env.THALLY_GITHUB_APP_PRIVATE_KEY ?? process.env.DOX_GITHUB_APP_PRIVATE_KEY;
|
|
58
|
-
if (appId && installationId && privateKey)
|
|
98
|
+
if (appId && installationId && privateKey)
|
|
99
|
+
return { appId, installationId, privateKey };
|
|
59
100
|
return void 0;
|
|
60
101
|
}
|
|
61
102
|
async function resolveGithubToken(options) {
|
|
@@ -66,7 +107,9 @@ async function resolveGithubToken(options) {
|
|
|
66
107
|
try {
|
|
67
108
|
return await mintInstallationToken(appCreds, options?.fetchImpl ?? fetch);
|
|
68
109
|
} catch (err) {
|
|
69
|
-
console.warn(
|
|
110
|
+
console.warn(
|
|
111
|
+
`[thally-track] GitHub App token mint failed, falling back to PAT: ${err instanceof Error ? err.message : String(err)}`
|
|
112
|
+
);
|
|
70
113
|
return pat;
|
|
71
114
|
}
|
|
72
115
|
}
|
|
@@ -75,9 +118,13 @@ async function resolveGithubToken(options) {
|
|
|
75
118
|
async function githubJson(path, options) {
|
|
76
119
|
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
77
120
|
const token = await resolveGithubToken(options);
|
|
78
|
-
const headers = {
|
|
121
|
+
const headers = {
|
|
122
|
+
Accept: "application/vnd.github+json"
|
|
123
|
+
};
|
|
79
124
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
80
|
-
const response = await fetchImpl(`https://api.github.com${path}`, {
|
|
125
|
+
const response = await fetchImpl(`https://api.github.com${path}`, {
|
|
126
|
+
headers
|
|
127
|
+
});
|
|
81
128
|
if (!response.ok) {
|
|
82
129
|
const hint = response.status === 404 || response.status === 403 ? " (private repo or rate limit? set THALLY_GITHUB_TOKEN or connect a GitHub App)" : "";
|
|
83
130
|
throw new Error(`GitHub API ${response.status} for ${path}${hint}`);
|
|
@@ -96,7 +143,10 @@ function toPullRequestInfo(raw) {
|
|
|
96
143
|
};
|
|
97
144
|
}
|
|
98
145
|
async function fetchPullRequest(owner, repo, number, options) {
|
|
99
|
-
const raw = await githubJson(
|
|
146
|
+
const raw = await githubJson(
|
|
147
|
+
`/repos/${owner}/${repo}/pulls/${number}`,
|
|
148
|
+
options
|
|
149
|
+
);
|
|
100
150
|
return toPullRequestInfo(raw);
|
|
101
151
|
}
|
|
102
152
|
async function fetchPullRequestFiles(owner, repo, number, options) {
|
|
@@ -104,7 +154,10 @@ async function fetchPullRequestFiles(owner, repo, number, options) {
|
|
|
104
154
|
const maxPages = 30;
|
|
105
155
|
const raw = [];
|
|
106
156
|
for (let page = 1; page <= maxPages; page++) {
|
|
107
|
-
const chunk = await githubJson(
|
|
157
|
+
const chunk = await githubJson(
|
|
158
|
+
`/repos/${owner}/${repo}/pulls/${number}/files?per_page=${perPage}&page=${page}`,
|
|
159
|
+
options
|
|
160
|
+
);
|
|
108
161
|
raw.push(...chunk);
|
|
109
162
|
if (chunk.length < perPage) break;
|
|
110
163
|
}
|
|
@@ -121,7 +174,9 @@ async function fetchLatestMergedPr(owner, repo, base, options) {
|
|
|
121
174
|
`/repos/${owner}/${repo}/pulls?state=closed&base=${encodeURIComponent(base)}&sort=updated&direction=desc&per_page=30`,
|
|
122
175
|
options
|
|
123
176
|
);
|
|
124
|
-
const merged = raw.filter((pr) => Boolean(pr.merged_at)).sort(
|
|
177
|
+
const merged = raw.filter((pr) => Boolean(pr.merged_at)).sort(
|
|
178
|
+
(a, b) => Date.parse(b.merged_at ?? "") - Date.parse(a.merged_at ?? "")
|
|
179
|
+
)[0];
|
|
125
180
|
return merged ? toPullRequestInfo(merged) : null;
|
|
126
181
|
}
|
|
127
182
|
function compileGlob(pattern) {
|
|
@@ -197,12 +252,16 @@ _(diff truncated \u2014 ${files.length} file(s) total)_
|
|
|
197
252
|
return context.slice(0, TRACK_CONTEXT_CHAR_CAP);
|
|
198
253
|
}
|
|
199
254
|
function buildTrackTask(repo, pr, files) {
|
|
200
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
instruction: buildTrackInstruction(repo, pr),
|
|
257
|
+
context: buildTrackContext(repo, pr, files)
|
|
258
|
+
};
|
|
201
259
|
}
|
|
202
260
|
export {
|
|
203
261
|
AGENT_BRANCH_PREFIX,
|
|
204
262
|
DOCS_PREVIEW_LABEL,
|
|
205
263
|
TRACK_CONTEXT_CHAR_CAP,
|
|
264
|
+
TRACK_OWNED_BRANCH_PREFIXES,
|
|
206
265
|
buildTrackContext,
|
|
207
266
|
buildTrackInstruction,
|
|
208
267
|
buildTrackTask,
|
|
@@ -211,6 +270,7 @@ export {
|
|
|
211
270
|
fetchPullRequest,
|
|
212
271
|
fetchPullRequestFiles,
|
|
213
272
|
filterFilesByGlobs,
|
|
273
|
+
isTrackOwnedBranch,
|
|
214
274
|
matchesGlob,
|
|
215
275
|
mintInstallationToken,
|
|
216
276
|
parseOwnerRepo,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thallylabs/mcp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.23",
|
|
4
4
|
"description": "MCP server for managing Thally knowledge surfaces and tracing product changes into documentation work.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@anthropic-ai/sdk": "^0.36.0",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
41
|
-
"create-thally-docs": "0.10.
|
|
41
|
+
"create-thally-docs": "0.10.21",
|
|
42
42
|
"gray-matter": "^4.0.3",
|
|
43
43
|
"p-limit": "^6.1.0",
|
|
44
44
|
"yaml": "^2.8.2",
|