@thallylabs/mcp 0.7.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/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/index.js +2393 -0
- package/dist/tools.d.ts +25 -0
- package/dist/tools.js +2354 -0
- package/dist/track.d.ts +140 -0
- package/dist/track.js +219 -0
- package/package.json +69 -0
package/dist/track.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The branch prefix the docs agent stamps on its own PRs (`run.ts` creates
|
|
3
|
+
* `thally/agent-<base36>`). Track's loop guard ignores PRs from these branches so a
|
|
4
|
+
* self-tracking repo never chases its own agent PRs. Single source of truth for
|
|
5
|
+
* the webhook relay, the scaffolded Actions workflow, and the producer.
|
|
6
|
+
*/
|
|
7
|
+
declare const AGENT_BRANCH_PREFIX = "thally/agent-";
|
|
8
|
+
/** Label that turns an OPEN PR into a preview-docs request (shared by the
|
|
9
|
+
* webhook relay and the scaffolded sender workflow). */
|
|
10
|
+
declare const DOCS_PREVIEW_LABEL = "docs-preview";
|
|
11
|
+
interface OwnerRepoRef {
|
|
12
|
+
owner: string;
|
|
13
|
+
repo: string;
|
|
14
|
+
/** Present when the spec pinned a PR number (`owner/repo#123`). */
|
|
15
|
+
pr?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse a tracked-repo spec: `owner/repo`, `owner/repo#123`, or a github.com
|
|
19
|
+
* URL (`https://github.com/owner/repo(.git)`, optionally `.../pull/123`).
|
|
20
|
+
* Returns null for anything else.
|
|
21
|
+
*/
|
|
22
|
+
declare function parseOwnerRepo(spec: string): OwnerRepoRef | null;
|
|
23
|
+
interface PrFile {
|
|
24
|
+
filename: string;
|
|
25
|
+
/** GitHub status: added | removed | modified | renamed | … */
|
|
26
|
+
status: string;
|
|
27
|
+
additions: number;
|
|
28
|
+
deletions: number;
|
|
29
|
+
/** Unified diff hunk; absent for binary or very large files. */
|
|
30
|
+
patch?: string;
|
|
31
|
+
}
|
|
32
|
+
interface PullRequestInfo {
|
|
33
|
+
number: number;
|
|
34
|
+
title: string;
|
|
35
|
+
body: string;
|
|
36
|
+
htmlUrl: string;
|
|
37
|
+
/** The branch the PR merged INTO. */
|
|
38
|
+
baseRef: string;
|
|
39
|
+
/** The merge commit SHA, when merged. */
|
|
40
|
+
mergeCommitSha?: string;
|
|
41
|
+
author?: string;
|
|
42
|
+
}
|
|
43
|
+
/** GitHub App installation credentials (the "Connect GitHub" path). */
|
|
44
|
+
interface GithubAppCreds {
|
|
45
|
+
appId: string | number;
|
|
46
|
+
installationId: string | number;
|
|
47
|
+
/** PEM-encoded private key (PKCS#1 or PKCS#8). */
|
|
48
|
+
privateKey: string;
|
|
49
|
+
}
|
|
50
|
+
interface GithubFetchOptions {
|
|
51
|
+
/** Overrides the token resolver entirely. */
|
|
52
|
+
token?: string;
|
|
53
|
+
/** GitHub App creds to mint an installation token from (used before the PAT chain). */
|
|
54
|
+
appCreds?: GithubAppCreds;
|
|
55
|
+
/** Injectable for tests — defaults to global fetch. */
|
|
56
|
+
fetchImpl?: typeof fetch;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Sign an App-level RS256 JWT (iss = appId) — authenticates AS THE APP for the
|
|
60
|
+
* App API (installation lookups, token exchange). Lives at most 10 min.
|
|
61
|
+
*/
|
|
62
|
+
declare function createAppJwt(appId: string | number, privateKey: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Mint a GitHub App installation access token: sign an App JWT, then exchange it
|
|
65
|
+
* for a short-lived, repo-scoped installation token. Dependency-free
|
|
66
|
+
* (node:crypto). Cached per (app, installation, key) — so a key rotation or a
|
|
67
|
+
* reconnect never serves a token minted from stale credentials.
|
|
68
|
+
*/
|
|
69
|
+
declare function mintInstallationToken(creds: GithubAppCreds, fetchImpl?: typeof fetch): Promise<string>;
|
|
70
|
+
/**
|
|
71
|
+
* Confirm an installation id genuinely belongs to this app (authenticated as the
|
|
72
|
+
* app). Used by the "Connect GitHub" callback to accept a post-install
|
|
73
|
+
* `installation_id` WITHOUT trusting the query param alone — an attacker-forged
|
|
74
|
+
* or foreign id fails this check, closing the CSRF hole on the install step.
|
|
75
|
+
*/
|
|
76
|
+
declare function verifyInstallationBelongsToApp(appId: string | number, privateKey: string, installationId: string | number, fetchImpl?: typeof fetch): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a GitHub API token. Precedence: an explicit token → a GitHub App
|
|
79
|
+
* installation token (passed creds, then env creds) → the personal-token chain
|
|
80
|
+
* (`THALLY_GITHUB_TOKEN → THALLY_TASKS_TOKEN → GH_TOKEN → GITHUB_TOKEN`, each
|
|
81
|
+
* `THALLY_*` falling back to its legacy `DOX_*` name). One resolver
|
|
82
|
+
* for every Track call site so the chain never drifts.
|
|
83
|
+
*/
|
|
84
|
+
declare function resolveGithubToken(options?: GithubFetchOptions): Promise<string | undefined>;
|
|
85
|
+
/** Fetch a pull request's metadata. */
|
|
86
|
+
declare function fetchPullRequest(owner: string, repo: string, number: number, options?: GithubFetchOptions): Promise<PullRequestInfo>;
|
|
87
|
+
/** Fetch a pull request's changed files with per-file patches (one page, up to 100). */
|
|
88
|
+
declare function fetchPullRequestFiles(owner: string, repo: string, number: number, options?: GithubFetchOptions): Promise<Array<PrFile>>;
|
|
89
|
+
/**
|
|
90
|
+
* The most recent MERGED pull request into `base`, or null when there is none.
|
|
91
|
+
* Used by `thally track test` / `sync_from_repo` to preview "the latest merge".
|
|
92
|
+
*/
|
|
93
|
+
declare function fetchLatestMergedPr(owner: string, repo: string, base: string, options?: GithubFetchOptions): Promise<PullRequestInfo | null>;
|
|
94
|
+
/** Single glob check (compiles on each call — use filterFilesByGlobs for many files). */
|
|
95
|
+
declare function matchesGlob(pattern: string, filePath: string): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Filter files by path globs; absent/empty globs match everything. Each glob is
|
|
98
|
+
* compiled once (M compilations), not once per file (N×M) — matters on large
|
|
99
|
+
* PRs handled in the webhook request path.
|
|
100
|
+
*/
|
|
101
|
+
declare function filterFilesByGlobs<T extends {
|
|
102
|
+
filename: string;
|
|
103
|
+
}>(files: Array<T>, globs?: Array<string>): Array<T>;
|
|
104
|
+
interface TrackedRepoLike {
|
|
105
|
+
owner: string;
|
|
106
|
+
repo: string;
|
|
107
|
+
/** The base branch PRs must merge into to trigger (default "main"). */
|
|
108
|
+
branch?: string;
|
|
109
|
+
paths?: Array<string>;
|
|
110
|
+
outputTab?: string;
|
|
111
|
+
outputGroup?: string;
|
|
112
|
+
}
|
|
113
|
+
/** Same context budget the agent's other context builders use. */
|
|
114
|
+
declare const TRACK_CONTEXT_CHAR_CAP = 20000;
|
|
115
|
+
/**
|
|
116
|
+
* The one-line instruction for a merged tracked PR. Pure (no network) and free
|
|
117
|
+
* of shell-metacharacter wrapping — it is dispatched through GitHub Actions
|
|
118
|
+
* where it lands in a shell, so it must never embed quotes that could break (or
|
|
119
|
+
* be injected into) the run script; the receiver reads it from an env var. The
|
|
120
|
+
* PR title is NOT embedded (it's attacker-influenced and the docs-repo Action
|
|
121
|
+
* rebuilds full context from --from-pr anyway).
|
|
122
|
+
*
|
|
123
|
+
* The instruction frames the agent's actual job: judge what user-facing
|
|
124
|
+
* behavior the merged PR changed and update the docs that describe it.
|
|
125
|
+
*/
|
|
126
|
+
declare function buildTrackInstruction(repo: TrackedRepoLike, pr: Pick<PullRequestInfo, 'number'>, options?: {
|
|
127
|
+
preview?: boolean;
|
|
128
|
+
}): string;
|
|
129
|
+
/** The capped markdown context for a merged tracked PR (title, body, file diffs). */
|
|
130
|
+
declare function buildTrackContext(repo: Pick<TrackedRepoLike, 'owner' | 'repo'>, pr: PullRequestInfo, files: Array<PrFile>): string;
|
|
131
|
+
/**
|
|
132
|
+
* Distill a merged PR into the docs task the agent runs: a one-line instruction
|
|
133
|
+
* (output routing) and a capped markdown context (PR description + diff).
|
|
134
|
+
*/
|
|
135
|
+
declare function buildTrackTask(repo: TrackedRepoLike, pr: PullRequestInfo, files: Array<PrFile>): {
|
|
136
|
+
instruction: string;
|
|
137
|
+
context: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
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 };
|
package/dist/track.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// src/lib/track.ts
|
|
2
|
+
import { createSign, createHash } from "crypto";
|
|
3
|
+
var AGENT_BRANCH_PREFIX = "thally/agent-";
|
|
4
|
+
var DOCS_PREVIEW_LABEL = "docs-preview";
|
|
5
|
+
function parseOwnerRepo(spec) {
|
|
6
|
+
const trimmed = spec.trim();
|
|
7
|
+
const url = trimmed.match(
|
|
8
|
+
/^https?:\/\/github\.com\/([^/\s]+)\/([^/\s#?]+?)(?:\.git)?(?:\/pull\/(\d+))?(?:[/#?].*)?$/i
|
|
9
|
+
);
|
|
10
|
+
if (url) return { owner: url[1], repo: url[2], ...url[3] ? { pr: Number(url[3]) } : {} };
|
|
11
|
+
const plain = trimmed.match(/^([A-Za-z0-9-_.]+)\/([A-Za-z0-9-_.]+?)(?:#(\d+))?$/);
|
|
12
|
+
if (!plain) return null;
|
|
13
|
+
return { owner: plain[1], repo: plain[2], ...plain[3] ? { pr: Number(plain[3]) } : {} };
|
|
14
|
+
}
|
|
15
|
+
function base64url(input) {
|
|
16
|
+
return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
17
|
+
}
|
|
18
|
+
var installationTokenCache = /* @__PURE__ */ new Map();
|
|
19
|
+
function createAppJwt(appId, privateKey) {
|
|
20
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
21
|
+
const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
22
|
+
const payload = base64url(JSON.stringify({ iat: now - 60, exp: now + 9 * 60, iss: String(appId) }));
|
|
23
|
+
const signature = base64url(createSign("RSA-SHA256").update(`${header}.${payload}`).sign(privateKey));
|
|
24
|
+
return `${header}.${payload}.${signature}`;
|
|
25
|
+
}
|
|
26
|
+
async function mintInstallationToken(creds, fetchImpl = fetch) {
|
|
27
|
+
const keyFp = createHash("sha256").update(creds.privateKey).digest("hex").slice(0, 12);
|
|
28
|
+
const cacheKey = `${creds.appId}:${creds.installationId}:${keyFp}`;
|
|
29
|
+
const cached = installationTokenCache.get(cacheKey);
|
|
30
|
+
if (cached && cached.expiresAtMs - 6e4 > Date.now()) return cached.token;
|
|
31
|
+
const jwt = createAppJwt(creds.appId, creds.privateKey);
|
|
32
|
+
const res = await fetchImpl(`https://api.github.com/app/installations/${creds.installationId}/access_tokens`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${jwt}` }
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
throw new Error(`GitHub App token exchange failed (${res.status}) \u2014 check the app id, installation id, and private key.`);
|
|
38
|
+
}
|
|
39
|
+
const body = await res.json();
|
|
40
|
+
const parsed = Date.parse(body.expires_at);
|
|
41
|
+
const expiresAtMs = Number.isFinite(parsed) ? parsed : Date.now() + 30 * 60 * 1e3;
|
|
42
|
+
installationTokenCache.set(cacheKey, { token: body.token, expiresAtMs });
|
|
43
|
+
return body.token;
|
|
44
|
+
}
|
|
45
|
+
async function verifyInstallationBelongsToApp(appId, privateKey, installationId, fetchImpl = fetch) {
|
|
46
|
+
const jwt = createAppJwt(appId, privateKey);
|
|
47
|
+
const res = await fetchImpl(`https://api.github.com/app/installations/${installationId}`, {
|
|
48
|
+
headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${jwt}` }
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) return false;
|
|
51
|
+
const body = await res.json();
|
|
52
|
+
return String(body.app_id) === String(appId);
|
|
53
|
+
}
|
|
54
|
+
function envAppCreds() {
|
|
55
|
+
const appId = (process.env.THALLY_GITHUB_APP_ID ?? process.env.DOX_GITHUB_APP_ID)?.trim();
|
|
56
|
+
const installationId = (process.env.THALLY_GITHUB_APP_INSTALLATION_ID ?? process.env.DOX_GITHUB_APP_INSTALLATION_ID)?.trim();
|
|
57
|
+
const privateKey = process.env.THALLY_GITHUB_APP_PRIVATE_KEY ?? process.env.DOX_GITHUB_APP_PRIVATE_KEY;
|
|
58
|
+
if (appId && installationId && privateKey) return { appId, installationId, privateKey };
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
async function resolveGithubToken(options) {
|
|
62
|
+
if (options?.token) return options.token;
|
|
63
|
+
const pat = process.env.THALLY_GITHUB_TOKEN ?? process.env.DOX_GITHUB_TOKEN ?? process.env.THALLY_TASKS_TOKEN ?? process.env.DOX_TASKS_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? void 0;
|
|
64
|
+
const appCreds = options?.appCreds ?? envAppCreds();
|
|
65
|
+
if (appCreds) {
|
|
66
|
+
try {
|
|
67
|
+
return await mintInstallationToken(appCreds, options?.fetchImpl ?? fetch);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn(`[thally-track] GitHub App token mint failed, falling back to PAT: ${err instanceof Error ? err.message : String(err)}`);
|
|
70
|
+
return pat;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return pat;
|
|
74
|
+
}
|
|
75
|
+
async function githubJson(path, options) {
|
|
76
|
+
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
77
|
+
const token = await resolveGithubToken(options);
|
|
78
|
+
const headers = { Accept: "application/vnd.github+json" };
|
|
79
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
80
|
+
const response = await fetchImpl(`https://api.github.com${path}`, { headers });
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const hint = response.status === 404 || response.status === 403 ? " (private repo or rate limit? set THALLY_GITHUB_TOKEN or connect a GitHub App)" : "";
|
|
83
|
+
throw new Error(`GitHub API ${response.status} for ${path}${hint}`);
|
|
84
|
+
}
|
|
85
|
+
return await response.json();
|
|
86
|
+
}
|
|
87
|
+
function toPullRequestInfo(raw) {
|
|
88
|
+
return {
|
|
89
|
+
number: raw.number,
|
|
90
|
+
title: raw.title ?? "",
|
|
91
|
+
body: raw.body ?? "",
|
|
92
|
+
htmlUrl: raw.html_url ?? "",
|
|
93
|
+
baseRef: raw.base?.ref ?? "main",
|
|
94
|
+
...raw.merge_commit_sha ? { mergeCommitSha: raw.merge_commit_sha } : {},
|
|
95
|
+
...raw.user?.login ? { author: raw.user.login } : {}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function fetchPullRequest(owner, repo, number, options) {
|
|
99
|
+
const raw = await githubJson(`/repos/${owner}/${repo}/pulls/${number}`, options);
|
|
100
|
+
return toPullRequestInfo(raw);
|
|
101
|
+
}
|
|
102
|
+
async function fetchPullRequestFiles(owner, repo, number, options) {
|
|
103
|
+
const perPage = 100;
|
|
104
|
+
const maxPages = 30;
|
|
105
|
+
const raw = [];
|
|
106
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
107
|
+
const chunk = await githubJson(`/repos/${owner}/${repo}/pulls/${number}/files?per_page=${perPage}&page=${page}`, options);
|
|
108
|
+
raw.push(...chunk);
|
|
109
|
+
if (chunk.length < perPage) break;
|
|
110
|
+
}
|
|
111
|
+
return raw.map((f) => ({
|
|
112
|
+
filename: f.filename,
|
|
113
|
+
status: f.status,
|
|
114
|
+
additions: f.additions,
|
|
115
|
+
deletions: f.deletions,
|
|
116
|
+
...f.patch ? { patch: f.patch } : {}
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
async function fetchLatestMergedPr(owner, repo, base, options) {
|
|
120
|
+
const raw = await githubJson(
|
|
121
|
+
`/repos/${owner}/${repo}/pulls?state=closed&base=${encodeURIComponent(base)}&sort=updated&direction=desc&per_page=30`,
|
|
122
|
+
options
|
|
123
|
+
);
|
|
124
|
+
const merged = raw.filter((pr) => Boolean(pr.merged_at)).sort((a, b) => Date.parse(b.merged_at ?? "") - Date.parse(a.merged_at ?? ""))[0];
|
|
125
|
+
return merged ? toPullRequestInfo(merged) : null;
|
|
126
|
+
}
|
|
127
|
+
function compileGlob(pattern) {
|
|
128
|
+
const normalized = pattern.replace(/^\.\//, "");
|
|
129
|
+
let regex = "";
|
|
130
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
131
|
+
const char = normalized[i];
|
|
132
|
+
if (char === "*") {
|
|
133
|
+
if (normalized[i + 1] === "*") {
|
|
134
|
+
if (normalized[i + 2] === "/") {
|
|
135
|
+
regex += "(?:[^/]+/)*";
|
|
136
|
+
i += 2;
|
|
137
|
+
} else {
|
|
138
|
+
regex += ".*";
|
|
139
|
+
i += 1;
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
regex += "[^/]*";
|
|
143
|
+
}
|
|
144
|
+
} else if (char === "?") {
|
|
145
|
+
regex += "[^/]";
|
|
146
|
+
} else {
|
|
147
|
+
regex += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return new RegExp(`^${regex}$`);
|
|
151
|
+
}
|
|
152
|
+
function matchesGlob(pattern, filePath) {
|
|
153
|
+
return compileGlob(pattern).test(filePath.replace(/^\.\//, ""));
|
|
154
|
+
}
|
|
155
|
+
function filterFilesByGlobs(files, globs) {
|
|
156
|
+
if (!globs || globs.length === 0) return files;
|
|
157
|
+
const compiled = globs.map(compileGlob);
|
|
158
|
+
return files.filter((file) => {
|
|
159
|
+
const path = file.filename.replace(/^\.\//, "");
|
|
160
|
+
return compiled.some((re) => re.test(path));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
var TRACK_CONTEXT_CHAR_CAP = 2e4;
|
|
164
|
+
function buildTrackInstruction(repo, pr, options) {
|
|
165
|
+
const placement = repo.outputTab ? ` If new pages are warranted, add them under the ${repo.outputTab} tab${repo.outputGroup ? ` (${repo.outputGroup} group)` : ""}.` : "";
|
|
166
|
+
const lead = options?.preview ? `An OPEN pull request in ${repo.owner}/${repo.repo} (#${pr.number}) is up for review.` : `A pull request merged in ${repo.owner}/${repo.repo} (#${pr.number}).`;
|
|
167
|
+
const tail = options?.preview ? ` This is a preview: the PR may still change before it merges, so draft the docs it will need for review alongside it.` : ` Make no change if the PR has no user-facing impact.`;
|
|
168
|
+
return `${lead} Review it and decide what user-facing behavior it changes (API surface, config, CLI, defaults, behavior). Then find the documentation pages that describe that behavior and update them so the docs match \u2014 editing existing pages in place where they already cover it.${placement}` + tail;
|
|
169
|
+
}
|
|
170
|
+
function buildTrackContext(repo, pr, files) {
|
|
171
|
+
const header = [
|
|
172
|
+
`# Merged PR ${repo.owner}/${repo.repo}#${pr.number}: ${pr.title}`,
|
|
173
|
+
pr.author ? `Author: ${pr.author}` : null,
|
|
174
|
+
pr.htmlUrl ? `URL: ${pr.htmlUrl}` : null,
|
|
175
|
+
"",
|
|
176
|
+
pr.body?.trim() || "(no description)",
|
|
177
|
+
""
|
|
178
|
+
].filter((line) => line !== null).join("\n");
|
|
179
|
+
const NOTE_RESERVE = 100;
|
|
180
|
+
let context = header;
|
|
181
|
+
for (const file of files) {
|
|
182
|
+
const section = [
|
|
183
|
+
`### ${file.filename} (${file.status}, +${file.additions}/-${file.deletions})`,
|
|
184
|
+
file.patch ? `\`\`\`diff
|
|
185
|
+
${file.patch}
|
|
186
|
+
\`\`\`` : "_(no text diff \u2014 binary or too large)_",
|
|
187
|
+
""
|
|
188
|
+
].join("\n");
|
|
189
|
+
if (context.length + section.length > TRACK_CONTEXT_CHAR_CAP - NOTE_RESERVE) {
|
|
190
|
+
context += `
|
|
191
|
+
_(diff truncated \u2014 ${files.length} file(s) total)_
|
|
192
|
+
`;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
context += section;
|
|
196
|
+
}
|
|
197
|
+
return context.slice(0, TRACK_CONTEXT_CHAR_CAP);
|
|
198
|
+
}
|
|
199
|
+
function buildTrackTask(repo, pr, files) {
|
|
200
|
+
return { instruction: buildTrackInstruction(repo, pr), context: buildTrackContext(repo, pr, files) };
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
AGENT_BRANCH_PREFIX,
|
|
204
|
+
DOCS_PREVIEW_LABEL,
|
|
205
|
+
TRACK_CONTEXT_CHAR_CAP,
|
|
206
|
+
buildTrackContext,
|
|
207
|
+
buildTrackInstruction,
|
|
208
|
+
buildTrackTask,
|
|
209
|
+
createAppJwt,
|
|
210
|
+
fetchLatestMergedPr,
|
|
211
|
+
fetchPullRequest,
|
|
212
|
+
fetchPullRequestFiles,
|
|
213
|
+
filterFilesByGlobs,
|
|
214
|
+
matchesGlob,
|
|
215
|
+
mintInstallationToken,
|
|
216
|
+
parseOwnerRepo,
|
|
217
|
+
resolveGithubToken,
|
|
218
|
+
verifyInstallationBelongsToApp
|
|
219
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thallylabs/mcp",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "MCP server for scaffolding and managing Thally documentation projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"thally-mcp": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.js",
|
|
14
|
+
"./tools": {
|
|
15
|
+
"types": "./dist/tools.d.ts",
|
|
16
|
+
"import": "./dist/tools.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json",
|
|
19
|
+
"./track": {
|
|
20
|
+
"types": "./dist/track.d.ts",
|
|
21
|
+
"import": "./dist/track.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"dev": "tsup --watch",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@anthropic-ai/sdk": "^0.36.0",
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
37
|
+
"gray-matter": "^4.0.3",
|
|
38
|
+
"p-limit": "^6.1.0",
|
|
39
|
+
"tar": "^6.2.0",
|
|
40
|
+
"zod": "^3.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"@types/tar": "^6.1.13",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.0.0"
|
|
47
|
+
},
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"keywords": [
|
|
50
|
+
"thally",
|
|
51
|
+
"documentation",
|
|
52
|
+
"mcp",
|
|
53
|
+
"model-context-protocol",
|
|
54
|
+
"ai",
|
|
55
|
+
"agent",
|
|
56
|
+
"claude",
|
|
57
|
+
"cursor",
|
|
58
|
+
"llm"
|
|
59
|
+
],
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/thallylabs/thally.git",
|
|
63
|
+
"directory": "packages/mcp"
|
|
64
|
+
},
|
|
65
|
+
"homepage": "https://github.com/thallylabs/thally#readme",
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/thallylabs/thally/issues"
|
|
68
|
+
}
|
|
69
|
+
}
|