@imenam/mcp-documentor 1.0.86 → 1.0.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/shared/read-only.js +1 -1
- package/dist/shared/schemas/doc.schema.d.ts +3 -1
- package/dist/shared/schemas/doc.schema.js +13 -1
- package/dist/src/core/code-source.d.ts +32 -0
- package/dist/src/core/code-source.js +23 -4
- package/dist/src/core/embedding-engine.d.ts +1 -1
- package/dist/src/core/embedding-engine.js +3 -3
- package/dist/src/core/embedding-store.d.ts +1 -1
- package/dist/src/core/embedding-store.js +4 -4
- package/dist/src/core/git-sync.d.ts +5 -0
- package/dist/src/core/git-sync.js +29 -2
- package/dist/src/core/github-code-api.d.ts +3 -8
- package/dist/src/core/github-code-api.js +67 -36
- package/dist/src/core/github-log.d.ts +38 -0
- package/dist/src/core/github-log.js +64 -0
- package/dist/src/core/log-reader.d.ts +47 -0
- package/dist/src/core/log-reader.js +106 -0
- package/dist/src/core/storage-engine.d.ts +27 -2
- package/dist/src/core/storage-engine.js +99 -23
- package/dist/src/gui-worker.js +58 -1
- package/dist/src/index.js +78 -12
- package/dist/src/tools/code-sources.d.ts +9 -0
- package/dist/src/tools/code-sources.js +18 -16
- package/dist/src/tools/create-project.d.ts +19 -0
- package/dist/src/tools/create-project.js +42 -0
- package/dist/src/tools/delete-project.d.ts +16 -0
- package/dist/src/tools/delete-project.js +25 -0
- package/dist/src/tools/git-diff-doc.js +123 -98
- package/dist/src/tools/move-doc.js +20 -6
- package/dist/src/tools/patch-doc.js +14 -3
- package/gui/dist/assets/{arc-DBgCQU2O.js → arc-s4GL8QZ_.js} +1 -1
- package/gui/dist/assets/{architectureDiagram-3BPJPVTR-BIPBAzZr.js → architectureDiagram-3BPJPVTR-CfDERg0r.js} +1 -1
- package/gui/dist/assets/{blockDiagram-GPEHLZMM-BE9WwuyF.js → blockDiagram-GPEHLZMM-CjDoEG3-.js} +1 -1
- package/gui/dist/assets/{c4Diagram-AAUBKEIU-V1_4ktIt.js → c4Diagram-AAUBKEIU-9o7f2UhK.js} +1 -1
- package/gui/dist/assets/channel-BsKHRH5P.js +1 -0
- package/gui/dist/assets/{chunk-2J33WTMH-DskG-Kmf.js → chunk-2J33WTMH-B37ouRYO.js} +1 -1
- package/gui/dist/assets/{chunk-4BX2VUAB-DaPhidEI.js → chunk-4BX2VUAB-Dcq44Skv.js} +1 -1
- package/gui/dist/assets/{chunk-55IACEB6-C9r6WDFM.js → chunk-55IACEB6-CLyM-RQf.js} +1 -1
- package/gui/dist/assets/{chunk-727SXJPM-BA-aC8US.js → chunk-727SXJPM-BjzEo2iz.js} +1 -1
- package/gui/dist/assets/{chunk-AQP2D5EJ-ByBp3whV.js → chunk-AQP2D5EJ-Co03OHPi.js} +1 -1
- package/gui/dist/assets/{chunk-FMBD7UC4-CzqUByx7.js → chunk-FMBD7UC4-ClbF2rAp.js} +1 -1
- package/gui/dist/assets/{chunk-ND2GUHAM-C0_B2ryQ.js → chunk-ND2GUHAM-CN5cTCWu.js} +1 -1
- package/gui/dist/assets/{chunk-QZHKN3VN-B3eEwjDh.js → chunk-QZHKN3VN-Bg4-kEPK.js} +1 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-DphzXyc8.js +1 -0
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-DphzXyc8.js +1 -0
- package/gui/dist/assets/{cose-bilkent-S5V4N54A-D_i0I2pU.js → cose-bilkent-S5V4N54A-6Dkp59bE.js} +1 -1
- package/gui/dist/assets/{dagre-BM42HDAG-DtEVPK_z.js → dagre-BM42HDAG-2dD-ID2T.js} +1 -1
- package/gui/dist/assets/{diagram-2AECGRRQ-BftnXa5w.js → diagram-2AECGRRQ-DBE2f2Lb.js} +1 -1
- package/gui/dist/assets/{diagram-5GNKFQAL-Dh9-kl_u.js → diagram-5GNKFQAL-CkKPHiH9.js} +1 -1
- package/gui/dist/assets/{diagram-KO2AKTUF-CvB0BWCa.js → diagram-KO2AKTUF-uWtGAF2N.js} +1 -1
- package/gui/dist/assets/{diagram-LMA3HP47-CDy-J1uy.js → diagram-LMA3HP47-DyQaWg_0.js} +1 -1
- package/gui/dist/assets/{diagram-OG6HWLK6-iY9FwkIF.js → diagram-OG6HWLK6-H_DfuB5t.js} +1 -1
- package/gui/dist/assets/{erDiagram-TEJ5UH35-BT2_IX8D.js → erDiagram-TEJ5UH35-Cj9K5EXA.js} +1 -1
- package/gui/dist/assets/{flowDiagram-I6XJVG4X-DaixWyMx.js → flowDiagram-I6XJVG4X-BYF24Jvt.js} +1 -1
- package/gui/dist/assets/{ganttDiagram-6RSMTGT7-DSo3cQPH.js → ganttDiagram-6RSMTGT7-D93LjmhI.js} +1 -1
- package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-BfFT7Lds.js → gitGraphDiagram-PVQCEYII-EVTcusDb.js} +1 -1
- package/gui/dist/assets/index-CtPuh-0k.css +1 -0
- package/gui/dist/assets/{index-DPGmAc6z.js → index-DvVxuv5N.js} +154 -152
- package/gui/dist/assets/{infoDiagram-5YYISTIA-BxbxNxe2.js → infoDiagram-5YYISTIA-DeGbaexj.js} +1 -1
- package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-DpopzqZr.js → ishikawaDiagram-YF4QCWOH-DNdk7sPJ.js} +1 -1
- package/gui/dist/assets/{journeyDiagram-JHISSGLW-DrOlFzIk.js → journeyDiagram-JHISSGLW-yLLfyn3W.js} +1 -1
- package/gui/dist/assets/{kanban-definition-UN3LZRKU-vH81lYXy.js → kanban-definition-UN3LZRKU-D8yvVgr8.js} +1 -1
- package/gui/dist/assets/{linear-BipFpFah.js → linear-j3lplktA.js} +1 -1
- package/gui/dist/assets/{mindmap-definition-RKZ34NQL-DsTxCLgj.js → mindmap-definition-RKZ34NQL-t9adVeMH.js} +1 -1
- package/gui/dist/assets/{pieDiagram-4H26LBE5-Bbq2ZWQU.js → pieDiagram-4H26LBE5-6Yrvo8h8.js} +1 -1
- package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-BdTSaEo4.js → quadrantDiagram-W4KKPZXB-C_3Su6rX.js} +1 -1
- package/gui/dist/assets/{requirementDiagram-4Y6WPE33-BndI8YZy.js → requirementDiagram-4Y6WPE33-B1qYSC11.js} +1 -1
- package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-ofBkVITf.js → sankeyDiagram-5OEKKPKP-D3Ervn-u.js} +1 -1
- package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-Cjj2UrN9.js → sequenceDiagram-3UESZ5HK-CKgTeWBS.js} +1 -1
- package/gui/dist/assets/{stateDiagram-AJRCARHV-CzPrXUuz.js → stateDiagram-AJRCARHV-D_PjEdFH.js} +1 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-KPbsissC.js +1 -0
- package/gui/dist/assets/{timeline-definition-PNZ67QCA-Cz4X-IOO.js → timeline-definition-PNZ67QCA-HeWRiNHu.js} +1 -1
- package/gui/dist/assets/{vennDiagram-CIIHVFJN-BwIgXJcZ.js → vennDiagram-CIIHVFJN-CREk7vzm.js} +1 -1
- package/gui/dist/assets/{wardley-L42UT6IY-keQS0aNZ.js → wardley-L42UT6IY-BWtRQ7ZH.js} +1 -1
- package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-DhykSgbD.js → wardleyDiagram-YWT4CUSO-BvlqWWwp.js} +1 -1
- package/gui/dist/assets/{xychartDiagram-2RQKCTM6-DFTqc4cj.js → xychartDiagram-2RQKCTM6-BCjv6pwc.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/gui/dist/assets/channel-B_cci0fU.js +0 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-BqlcJ5BP.js +0 -1
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-BqlcJ5BP.js +0 -1
- package/gui/dist/assets/index-CxCC1gxu.css +0 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-DAq4ObN9.js +0 -1
package/README.md
CHANGED
|
@@ -119,13 +119,15 @@ that work on a whole project (`list_docs`, `get_doc_tree`, `check_freshness` wit
|
|
|
119
119
|
| `list_docs` | List docs with staleness indicators (optionally filter by project) |
|
|
120
120
|
| `get_doc_tree` | Return folder/doc structure for a project (or all projects) |
|
|
121
121
|
| `update_doc` | Fully replace content and/or metadata (`project` required; prefer `patch_doc` for small edits) |
|
|
122
|
-
| `patch_doc` | Search-and-replace a fragment of a doc’s content (`project` required) |
|
|
123
|
-
| `move_doc` | Change a doc’s path (
|
|
122
|
+
| `patch_doc` | Search-and-replace a fragment of a doc’s content; the text must occur once unless `replace_all` is set (`project` required) |
|
|
123
|
+
| `move_doc` | Change a doc’s path (`new_path`) and/or project (`new_project`); links are rewritten across all projects, bare within their own project (`project` required) |
|
|
124
124
|
| `delete_doc` | Permanently delete a documentation entry (`project` required) |
|
|
125
|
+
| `create_project` | Create an empty project, optionally with its code source (`code_source`, as `set_code_source`) |
|
|
126
|
+
| `delete_project` | Delete a project; refused while it holds any doc, image or other file besides its settings |
|
|
125
127
|
| `search_docs` | Semantic search over docs (natural language; requires OpenAI) |
|
|
126
128
|
| `check_freshness` | Staleness for one doc (`project` required) or a full project scan |
|
|
127
129
|
| `refresh_file_snapshot` | Mark tracked files as up-to-date without editing the doc (`project` required) |
|
|
128
|
-
| `git_diff_doc` |
|
|
130
|
+
| `git_diff_doc` | Diff of changed tracked files between their snapshot content and the current one (`project` required) |
|
|
129
131
|
| `check_links` | Validate `related_docs`, `doc://` links, and `#heading` anchors (`project` required) |
|
|
130
132
|
| `list_projects` | List all documentation projects |
|
|
131
133
|
| `upload_image` | Upload an image from a local file path to a project's assets (`project` required); returns the markdown link to insert |
|
|
@@ -291,7 +293,7 @@ The GUI then shows a report of what each project became and which entries were n
|
|
|
291
293
|
| Sees uncommitted edits | yes | no — only what is pushed |
|
|
292
294
|
| Affected by `git checkout` | yes | no |
|
|
293
295
|
| Needs the codebase on the server | yes | no |
|
|
294
|
-
| `git_diff_doc` baseline |
|
|
296
|
+
| `git_diff_doc` baseline | the commit the snapshot was taken at, else the last one before its date | the blob the snapshot was taken from, else its commit, else the last one before its date |
|
|
295
297
|
|
|
296
298
|
Every report names the source it used (`Code source: GitHub you/my-repo@main`), so a verdict is never ambiguous. If the source cannot be reached — network, auth, rate limit — the status is `unverifiable` with the reason attached, **never** `stale`.
|
|
297
299
|
|
|
@@ -302,7 +304,7 @@ Stored tracked-file paths are already relative to the code root, and a code root
|
|
|
302
304
|
- Hashes are computed on line-ending-normalised content, so a snapshot taken from a CRLF checkout still matches the LF blob in the repository. Hashes stored by earlier versions are still recognised.
|
|
303
305
|
- Absolute paths sent by an agent are reduced by the code root when one is configured, and otherwise matched against the repository tree by their longest unambiguous suffix (a bare filename is only accepted when the repository holds exactly one file by that name).
|
|
304
306
|
|
|
305
|
-
Docs snapshotted before the switch carry no source commit: `check_freshness` works immediately,
|
|
307
|
+
Docs snapshotted before the switch carry no source commit: `check_freshness` works immediately, and `git_diff_doc` falls back on the last commit before the snapshot date. Whatever its origin, a baseline is checked against the hash stored at the snapshot; when it does not match — typically after a rebase that rewrote commit dates — the diff is reported impossible, and one `refresh_file_snapshot` anchors the file on its blob, which survives rebases.
|
|
306
308
|
|
|
307
309
|
### API usage
|
|
308
310
|
|
package/dist/shared/read-only.js
CHANGED
|
@@ -5,7 +5,7 @@ export function isReadOnly() {
|
|
|
5
5
|
}
|
|
6
6
|
/** Outils MCP qui créent / modifient / suppriment de la donnée. */
|
|
7
7
|
export const WRITE_TOOLS = new Set([
|
|
8
|
-
"create_doc", "update_doc", "patch_doc", "move_doc", "delete_doc",
|
|
8
|
+
"create_doc", "update_doc", "patch_doc", "move_doc", "delete_doc", "create_project", "delete_project",
|
|
9
9
|
"restore_doc_version", "refresh_file_snapshot", "reindex_embeddings",
|
|
10
10
|
"prepare_export_bundle", "delete_export_bundle", "upload_image",
|
|
11
11
|
// Sync: pull_docs and sync_status stay allowed (read-only consumer machines
|
|
@@ -156,8 +156,9 @@ export declare const DeleteDocArgsSchema: z.ZodObject<{
|
|
|
156
156
|
}, z.core.$strip>;
|
|
157
157
|
export declare const MoveDocArgsSchema: z.ZodObject<{
|
|
158
158
|
path: z.ZodString;
|
|
159
|
-
new_path: z.ZodString
|
|
159
|
+
new_path: z.ZodOptional<z.ZodString>;
|
|
160
160
|
project: z.ZodString;
|
|
161
|
+
new_project: z.ZodOptional<z.ZodString>;
|
|
161
162
|
}, z.core.$strip>;
|
|
162
163
|
export declare const CheckFreshnessArgsSchema: z.ZodObject<{
|
|
163
164
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -177,6 +178,7 @@ export declare const GetDocMetadataArgsSchema: z.ZodObject<{
|
|
|
177
178
|
project: z.ZodString;
|
|
178
179
|
}, z.core.$strip>;
|
|
179
180
|
export declare const PatchDocArgsSchema: z.ZodObject<{
|
|
181
|
+
replace_all: z.ZodOptional<z.ZodBoolean>;
|
|
180
182
|
replacement_file_path: z.ZodOptional<z.ZodString>;
|
|
181
183
|
replacement_file_content: z.ZodOptional<z.ZodString>;
|
|
182
184
|
replacement_draft: z.ZodOptional<z.ZodString>;
|
|
@@ -123,8 +123,19 @@ export const DeleteDocArgsSchema = z.object({
|
|
|
123
123
|
});
|
|
124
124
|
export const MoveDocArgsSchema = z.object({
|
|
125
125
|
path: z.string().describe("Current path of the documentation entry, e.g. 'backend/auth/authentication'"),
|
|
126
|
-
new_path: z
|
|
126
|
+
new_path: z
|
|
127
|
+
.string()
|
|
128
|
+
.min(1)
|
|
129
|
+
.optional()
|
|
130
|
+
.describe("New path for the documentation entry, e.g. 'backend/v2/authentication'. Changes folder and/or name. Omitted: the entry keeps its current path, which requires new_project."),
|
|
127
131
|
project: requiredProject("the move"),
|
|
132
|
+
new_project: z
|
|
133
|
+
.string()
|
|
134
|
+
.min(1)
|
|
135
|
+
.optional()
|
|
136
|
+
.describe("Project the entry is moved to. Omitted: the entry stays in `project`."),
|
|
137
|
+
}).refine((args) => args.new_path !== undefined || args.new_project !== undefined, {
|
|
138
|
+
message: "Provide new_path, new_project, or both.",
|
|
128
139
|
});
|
|
129
140
|
export const CheckFreshnessArgsSchema = z
|
|
130
141
|
.object({
|
|
@@ -155,6 +166,7 @@ export const PatchDocArgsSchema = z.object({
|
|
|
155
166
|
...textSourceFields("search", "the search text"),
|
|
156
167
|
replacement: z.string().optional().describe("Text to replace it with"),
|
|
157
168
|
...textSourceFields("replacement", "the replacement"),
|
|
169
|
+
replace_all: z.boolean().optional().describe("Replace every occurrence. Without it, a search text that occurs more than once is refused."),
|
|
158
170
|
}).refine(d => d.search !== undefined || d.search_file_path !== undefined || d.search_file_content !== undefined || d.search_draft !== undefined, {
|
|
159
171
|
message: "One of search, search_file_path or search_draft is required",
|
|
160
172
|
}).refine(d => d.replacement !== undefined || d.replacement_file_path !== undefined || d.replacement_file_content !== undefined || d.replacement_draft !== undefined, {
|
|
@@ -48,6 +48,36 @@ export interface CodeSource {
|
|
|
48
48
|
/** Converts a path supplied by an agent into the portable form persisted in a doc. */
|
|
49
49
|
toStoredPath(inputPath: string): Promise<string>;
|
|
50
50
|
}
|
|
51
|
+
export declare class GitHubCodeSource implements CodeSource {
|
|
52
|
+
readonly api: GitHubCodeApi;
|
|
53
|
+
private readonly project;
|
|
54
|
+
readonly kind: "github";
|
|
55
|
+
/** Agent-supplied path → repo-relative path, per source. Repo layouts barely move. */
|
|
56
|
+
private readonly repoPathCache;
|
|
57
|
+
constructor(api: GitHubCodeApi, project: string);
|
|
58
|
+
get label(): string;
|
|
59
|
+
read(storedPath: string): Promise<SourceFile | undefined>;
|
|
60
|
+
/** The blob sha from the commit's tree — already fetched for the whole repository in one call. */
|
|
61
|
+
contentId(storedPath: string): Promise<string | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* A blob's content by its sha. A blob outlives the commits that referenced it, rebases included.
|
|
64
|
+
* `repoPath` names the file in github.log.
|
|
65
|
+
*/
|
|
66
|
+
readBlob(blobSha: string, repoPath?: string): Promise<FileContent>;
|
|
67
|
+
/** A stored path's content at a given commit, or undefined when the file does not exist there. */
|
|
68
|
+
readAt(storedPath: string, commitSha: string): Promise<FileContent | undefined>;
|
|
69
|
+
toStoredPath(inputPath: string): Promise<string>;
|
|
70
|
+
/**
|
|
71
|
+
* Maps a path onto the repository layout.
|
|
72
|
+
*
|
|
73
|
+
* A relative path is already repo-relative. An absolute one — what an agent working on a local
|
|
74
|
+
* clone naturally sends — is reduced by the configured code root when there is one, and
|
|
75
|
+
* otherwise matched against the repository tree by its longest unambiguous path suffix, so the
|
|
76
|
+
* feature works without forcing every machine to declare a root.
|
|
77
|
+
*/
|
|
78
|
+
private toRepoPath;
|
|
79
|
+
private computeRepoPath;
|
|
80
|
+
}
|
|
51
81
|
/** Where the code sources read their configuration. */
|
|
52
82
|
export interface CodeSourceProvider {
|
|
53
83
|
/** The project's code source setting. Throws when the settings cannot be read. */
|
|
@@ -86,5 +116,7 @@ export declare function locateTrackedSections(project: string, storedPath: strin
|
|
|
86
116
|
* it simply matches nothing, which is the right outcome for a filter.
|
|
87
117
|
*/
|
|
88
118
|
export declare function toStoredPathForMatching(project: string, inputPath: string): Promise<string>;
|
|
119
|
+
/** A project's source when it reads a GitHub branch, undefined when it reads the local working copy. */
|
|
120
|
+
export declare function getGitHubSource(project: string): GitHubCodeSource | undefined;
|
|
89
121
|
/** The GitHub client behind a project's source, or undefined when it reads the local working copy. */
|
|
90
122
|
export declare function getGitHubApi(project: string): GitHubCodeApi | undefined;
|
|
@@ -49,7 +49,7 @@ class LocalCodeSource {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
const toForwardSlashes = (p) => p.split("\\").join("/");
|
|
52
|
-
class GitHubCodeSource {
|
|
52
|
+
export class GitHubCodeSource {
|
|
53
53
|
api;
|
|
54
54
|
project;
|
|
55
55
|
kind = "github";
|
|
@@ -71,7 +71,7 @@ class GitHubCodeSource {
|
|
|
71
71
|
if (!blobSha)
|
|
72
72
|
return undefined;
|
|
73
73
|
return {
|
|
74
|
-
content: bytesToFileContent(await this.api.getBlob(blobSha)),
|
|
74
|
+
content: bytesToFileContent(await this.api.getBlob(blobSha, repoPath)),
|
|
75
75
|
lastModifiedAt: commitDate,
|
|
76
76
|
revision: commitSha,
|
|
77
77
|
blobId: blobSha,
|
|
@@ -83,6 +83,21 @@ class GitHubCodeSource {
|
|
|
83
83
|
const repoPath = await this.toRepoPath(storedPath, commitSha);
|
|
84
84
|
return repoPath ? this.api.getBlobSha(commitSha, repoPath) : undefined;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* A blob's content by its sha. A blob outlives the commits that referenced it, rebases included.
|
|
88
|
+
* `repoPath` names the file in github.log.
|
|
89
|
+
*/
|
|
90
|
+
async readBlob(blobSha, repoPath) {
|
|
91
|
+
return bytesToFileContent(await this.api.getBlob(blobSha, repoPath));
|
|
92
|
+
}
|
|
93
|
+
/** A stored path's content at a given commit, or undefined when the file does not exist there. */
|
|
94
|
+
async readAt(storedPath, commitSha) {
|
|
95
|
+
const repoPath = await this.toRepoPath(storedPath, commitSha);
|
|
96
|
+
if (!repoPath)
|
|
97
|
+
return undefined;
|
|
98
|
+
const blobSha = await this.api.getBlobSha(commitSha, repoPath);
|
|
99
|
+
return blobSha ? this.readBlob(blobSha, repoPath) : undefined;
|
|
100
|
+
}
|
|
86
101
|
async toStoredPath(inputPath) {
|
|
87
102
|
const { commitSha } = await this.api.resolveRef();
|
|
88
103
|
const repoPath = await this.toRepoPath(inputPath, commitSha);
|
|
@@ -264,8 +279,12 @@ export async function toStoredPathForMatching(project, inputPath) {
|
|
|
264
279
|
return toForwardSlashes(inputPath);
|
|
265
280
|
}
|
|
266
281
|
}
|
|
282
|
+
/** A project's source when it reads a GitHub branch, undefined when it reads the local working copy. */
|
|
283
|
+
export function getGitHubSource(project) {
|
|
284
|
+
const source = getCodeSource(project);
|
|
285
|
+
return source instanceof GitHubCodeSource ? source : undefined;
|
|
286
|
+
}
|
|
267
287
|
/** The GitHub client behind a project's source, or undefined when it reads the local working copy. */
|
|
268
288
|
export function getGitHubApi(project) {
|
|
269
|
-
|
|
270
|
-
return source instanceof GitHubCodeSource ? source.api : undefined;
|
|
289
|
+
return getGitHubSource(project)?.api;
|
|
271
290
|
}
|
|
@@ -11,7 +11,7 @@ export declare class EmbeddingEngine {
|
|
|
11
11
|
cosineSimilarity(a: number[], b: number[]): number;
|
|
12
12
|
saveEmbedding(docId: string, embedding: number[], project: string): Promise<void>;
|
|
13
13
|
removeEmbedding(docId: string, project: string): Promise<void>;
|
|
14
|
-
moveEmbedding(oldDocId: string, newDocId: string,
|
|
14
|
+
moveEmbedding(oldDocId: string, project: string, newDocId: string, newProject: string): Promise<void>;
|
|
15
15
|
search(query: string, project: string, topK: number, getDocContent: (id: string) => Promise<{
|
|
16
16
|
title: string;
|
|
17
17
|
content: string;
|
|
@@ -66,9 +66,9 @@ export class EmbeddingEngine {
|
|
|
66
66
|
this.store.remove(project, docId);
|
|
67
67
|
logger.debug(`Embedding removed for doc "${docId}" in project "${project}"`);
|
|
68
68
|
}
|
|
69
|
-
async moveEmbedding(oldDocId, newDocId,
|
|
70
|
-
this.store.move(project, oldDocId, newDocId);
|
|
71
|
-
logger.debug(`Embedding moved: "${oldDocId}" → "${
|
|
69
|
+
async moveEmbedding(oldDocId, project, newDocId, newProject) {
|
|
70
|
+
this.store.move(project, oldDocId, newProject, newDocId);
|
|
71
|
+
logger.debug(`Embedding moved: "${project}:${oldDocId}" → "${newProject}:${newDocId}"`);
|
|
72
72
|
}
|
|
73
73
|
async search(query, project, topK, getDocContent) {
|
|
74
74
|
return this.searchAcrossProjects(query, [project], topK, async (_proj, docId) => getDocContent(docId));
|
|
@@ -5,7 +5,7 @@ export declare class EmbeddingStore {
|
|
|
5
5
|
private getDb;
|
|
6
6
|
save(project: string, docId: string, embedding: number[]): void;
|
|
7
7
|
remove(project: string, docId: string): void;
|
|
8
|
-
move(project: string, oldDocId: string, newDocId: string): void;
|
|
8
|
+
move(project: string, oldDocId: string, newProject: string, newDocId: string): void;
|
|
9
9
|
loadAll(project: string): Map<string, number[]>;
|
|
10
10
|
loadAllProjects(projects: string[]): Array<{
|
|
11
11
|
project: string;
|
|
@@ -65,11 +65,11 @@ export class EmbeddingStore {
|
|
|
65
65
|
.run(project, docId);
|
|
66
66
|
logger.debug(`Embedding removed: [${project}] ${docId}`);
|
|
67
67
|
}
|
|
68
|
-
move(project, oldDocId, newDocId) {
|
|
68
|
+
move(project, oldDocId, newProject, newDocId) {
|
|
69
69
|
this.getDb()
|
|
70
|
-
.prepare("UPDATE embeddings SET doc_id = ? WHERE project = ? AND doc_id = ?")
|
|
71
|
-
.run(newDocId, project, oldDocId);
|
|
72
|
-
logger.debug(`Embedding moved: [${project}] ${oldDocId} → ${newDocId}`);
|
|
70
|
+
.prepare("UPDATE embeddings SET project = ?, doc_id = ? WHERE project = ? AND doc_id = ?")
|
|
71
|
+
.run(newProject, newDocId, project, oldDocId);
|
|
72
|
+
logger.debug(`Embedding moved: [${project}] ${oldDocId} → [${newProject}] ${newDocId}`);
|
|
73
73
|
}
|
|
74
74
|
loadAll(project) {
|
|
75
75
|
const rows = this.getDb()
|
|
@@ -61,6 +61,11 @@ export declare class SyncEngine {
|
|
|
61
61
|
* ever stored in .git/config (which git never commits), never versioned.
|
|
62
62
|
*/
|
|
63
63
|
private authedUrl;
|
|
64
|
+
/**
|
|
65
|
+
* Runs one git command that reaches the remote and records it in github.log. Error messages
|
|
66
|
+
* are recorded with the token masked: git quotes the authenticated URL in some of them.
|
|
67
|
+
*/
|
|
68
|
+
private remoteGit;
|
|
64
69
|
private requireConfig;
|
|
65
70
|
/**
|
|
66
71
|
* Registers the structured merge drivers in .git/config, which git never
|
|
@@ -4,6 +4,7 @@ import path from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { simpleGit } from "simple-git";
|
|
6
6
|
import { createLogger } from "@imenam/mcp-gui-interface";
|
|
7
|
+
import { describeRemote, recordGitHubCall, redactSecrets } from "./github-log.js";
|
|
7
8
|
const logger = createLogger("GitSync");
|
|
8
9
|
const GITIGNORE_CONTENT = [
|
|
9
10
|
"# Derived / machine-local state — never synced",
|
|
@@ -72,6 +73,31 @@ export class SyncEngine {
|
|
|
72
73
|
return cfg.remoteUrl;
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Runs one git command that reaches the remote and records it in github.log. Error messages
|
|
78
|
+
* are recorded with the token masked: git quotes the authenticated URL in some of them.
|
|
79
|
+
*/
|
|
80
|
+
async remoteGit(cfg, action, command, run) {
|
|
81
|
+
const startedAt = Date.now();
|
|
82
|
+
const call = { kind: "git", action, command, repo: describeRemote(cfg.remoteUrl) };
|
|
83
|
+
try {
|
|
84
|
+
const result = await run();
|
|
85
|
+
recordGitHubCall({ ...call, ok: true, status: "ok", durationMs: Date.now() - startedAt });
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
const rejected = /fetch first|non-fast-forward|\[rejected\]/i.test(message);
|
|
91
|
+
recordGitHubCall({
|
|
92
|
+
...call,
|
|
93
|
+
ok: false,
|
|
94
|
+
status: rejected ? "rejected" : "failed",
|
|
95
|
+
durationMs: Date.now() - startedAt,
|
|
96
|
+
error: redactSecrets(message, [cfg.token]).trim(),
|
|
97
|
+
});
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
75
101
|
requireConfig() {
|
|
76
102
|
const cfg = this.storage.getSyncConfig();
|
|
77
103
|
if (!cfg?.remoteUrl) {
|
|
@@ -185,7 +211,7 @@ export class SyncEngine {
|
|
|
185
211
|
const pending = await this.git.status();
|
|
186
212
|
return { status: "conflict", conflicted: pending.conflicted };
|
|
187
213
|
}
|
|
188
|
-
await this.git.fetch("origin", cfg.branch);
|
|
214
|
+
await this.remoteGit(cfg, "Fetch the docs repository", `git fetch origin ${cfg.branch}`, () => this.git.fetch("origin", cfg.branch));
|
|
189
215
|
const remoteRef = `refs/remotes/origin/${cfg.branch}`;
|
|
190
216
|
// No remote branch yet (fresh repo) — nothing to pull.
|
|
191
217
|
const hasRemote = await this.git
|
|
@@ -274,8 +300,9 @@ export class SyncEngine {
|
|
|
274
300
|
"Resolve it with resolve_sync_conflict before pushing, otherwise the conflict markers would be committed as documentation content.");
|
|
275
301
|
}
|
|
276
302
|
const committed = await this.commitLocalChanges(message);
|
|
303
|
+
const unpushed = (await this.git.status()).ahead;
|
|
277
304
|
try {
|
|
278
|
-
await this.git.push(["-u", "origin", `HEAD:${cfg.branch}`]);
|
|
305
|
+
await this.remoteGit(cfg, unpushed > 0 ? `Push ${unpushed} local doc commit${unpushed > 1 ? "s" : ""}` : "Push the docs branch", `git push -u origin HEAD:${cfg.branch}`, () => this.git.push(["-u", "origin", `HEAD:${cfg.branch}`]));
|
|
279
306
|
}
|
|
280
307
|
catch (e) {
|
|
281
308
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -101,15 +101,10 @@ export declare class GitHubCodeApi {
|
|
|
101
101
|
isTreeTruncated(commitSha: string): boolean;
|
|
102
102
|
/** Blob sha for one path at one commit, or undefined if the file does not exist there. */
|
|
103
103
|
getBlobSha(commitSha: string, repoPath: string): Promise<string | undefined>;
|
|
104
|
-
/** A blob's bytes. Blobs are immutable, so this is cached for the life of the process. */
|
|
105
|
-
getBlob(blobSha: string): Promise<Buffer>;
|
|
106
104
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* One call covers a whole doc, so `git_diff_doc` groups its tracked files by the commit they
|
|
110
|
-
* were snapshotted at rather than asking per file. GitHub caps the response at 300 files;
|
|
111
|
-
* paths beyond that are simply absent from the map.
|
|
105
|
+
* A blob's bytes. Blobs are immutable, so this is cached for the life of the process.
|
|
106
|
+
* `filePath` names the file in github.log.
|
|
112
107
|
*/
|
|
113
|
-
|
|
108
|
+
getBlob(blobSha: string, filePath?: string): Promise<Buffer>;
|
|
114
109
|
}
|
|
115
110
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createLogger } from "@imenam/mcp-gui-interface";
|
|
2
2
|
import { oncePerOperation } from "./code-operation.js";
|
|
3
|
+
import { recordGitHubCall, redactSecrets } from "./github-log.js";
|
|
3
4
|
const logger = createLogger("GitHubCodeApi");
|
|
4
5
|
const API_BASE = "https://api.github.com";
|
|
5
6
|
export function formatRepoSpec(spec) {
|
|
@@ -39,12 +40,20 @@ export class GitHubApiError extends Error {
|
|
|
39
40
|
this.name = "GitHubApiError";
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
function readRateLimit(response) {
|
|
44
|
+
const remaining = Number(response.headers.get("x-ratelimit-remaining"));
|
|
45
|
+
const limit = Number(response.headers.get("x-ratelimit-limit"));
|
|
46
|
+
const reset = Number(response.headers.get("x-ratelimit-reset"));
|
|
47
|
+
if (!Number.isFinite(remaining) || !Number.isFinite(limit) || !limit)
|
|
48
|
+
return undefined;
|
|
49
|
+
return { remaining, limit, reset: Number.isFinite(reset) ? new Date(reset * 1000).toISOString() : "" };
|
|
50
|
+
}
|
|
42
51
|
/**
|
|
43
|
-
* One GET against the GitHub REST API
|
|
52
|
+
* One GET against the GitHub REST API, recorded in github.log whatever its outcome.
|
|
44
53
|
* 404 and 422 both mean "nothing at that address": 422 is what a valid repository returns for a
|
|
45
54
|
* ref that does not exist.
|
|
46
55
|
*/
|
|
47
|
-
async function githubRequest(apiPath, token,
|
|
56
|
+
async function githubRequest(apiPath, token, purpose) {
|
|
48
57
|
const headers = {
|
|
49
58
|
Accept: "application/vnd.github+json",
|
|
50
59
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
@@ -52,31 +61,58 @@ async function githubRequest(apiPath, token, label) {
|
|
|
52
61
|
};
|
|
53
62
|
if (token)
|
|
54
63
|
headers.Authorization = `Bearer ${token}`;
|
|
64
|
+
const { label } = purpose;
|
|
65
|
+
const startedAt = Date.now();
|
|
66
|
+
const record = (status, ok, error, rateLimit) => recordGitHubCall({
|
|
67
|
+
kind: "api",
|
|
68
|
+
action: purpose.action,
|
|
69
|
+
command: `GET ${apiPath}`,
|
|
70
|
+
repo: purpose.repo,
|
|
71
|
+
ok,
|
|
72
|
+
status,
|
|
73
|
+
durationMs: Date.now() - startedAt,
|
|
74
|
+
error: error && redactSecrets(error, [token]),
|
|
75
|
+
rateLimit,
|
|
76
|
+
});
|
|
55
77
|
let response;
|
|
56
78
|
try {
|
|
57
79
|
response = await fetch(`${API_BASE}${apiPath}`, { headers });
|
|
58
80
|
}
|
|
59
81
|
catch (err) {
|
|
60
82
|
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
record("network error", false, message);
|
|
61
84
|
throw new GitHubApiError(`network error contacting GitHub (${message})`);
|
|
62
85
|
}
|
|
86
|
+
const rateLimit = readRateLimit(response);
|
|
87
|
+
const status = String(response.status);
|
|
63
88
|
if (response.status === 404 || response.status === 422) {
|
|
64
|
-
|
|
89
|
+
const message = `not found: ${label} (repository, branch or path missing — or the token cannot see this repository)`;
|
|
90
|
+
record(status, false, message, rateLimit);
|
|
91
|
+
throw new GitHubApiError(message, response.status);
|
|
65
92
|
}
|
|
66
93
|
if (response.status === 401 || response.status === 403) {
|
|
67
94
|
const rateLimited = response.headers.get("x-ratelimit-remaining") === "0";
|
|
68
|
-
|
|
95
|
+
const message = rateLimited
|
|
69
96
|
? `GitHub API rate limit exhausted for ${label}`
|
|
70
|
-
: `access denied to ${label} — check the GitHub token in the project settings
|
|
97
|
+
: `access denied to ${label} — check the GitHub token in the project settings`;
|
|
98
|
+
record(status, false, message, rateLimit);
|
|
99
|
+
throw new GitHubApiError(message, response.status);
|
|
71
100
|
}
|
|
72
101
|
if (!response.ok) {
|
|
73
|
-
|
|
102
|
+
const message = `GitHub API returned ${response.status} for ${apiPath}`;
|
|
103
|
+
record(status, false, message, rateLimit);
|
|
104
|
+
throw new GitHubApiError(message, response.status);
|
|
74
105
|
}
|
|
106
|
+
record(status, true, undefined, rateLimit);
|
|
75
107
|
return response.json();
|
|
76
108
|
}
|
|
77
109
|
/** Visibility and default branch of a repository. */
|
|
78
110
|
export async function getRepositoryInfo(owner, repo, token) {
|
|
79
|
-
const data = await githubRequest(`/repos/${owner}/${repo}`, token,
|
|
111
|
+
const data = await githubRequest(`/repos/${owner}/${repo}`, token, {
|
|
112
|
+
label: `${owner}/${repo}`,
|
|
113
|
+
action: "Read the repository details",
|
|
114
|
+
repo: `${owner}/${repo}`,
|
|
115
|
+
});
|
|
80
116
|
return {
|
|
81
117
|
fullName: data.full_name,
|
|
82
118
|
private: !!data.private,
|
|
@@ -89,7 +125,7 @@ const MAX_BRANCH_PAGES = 10;
|
|
|
89
125
|
export async function listBranches(owner, repo, token) {
|
|
90
126
|
const names = [];
|
|
91
127
|
for (let page = 1; page <= MAX_BRANCH_PAGES; page++) {
|
|
92
|
-
const batch = await githubRequest(`/repos/${owner}/${repo}/branches?per_page=100&page=${page}`, token, `${owner}/${repo}`);
|
|
128
|
+
const batch = await githubRequest(`/repos/${owner}/${repo}/branches?per_page=100&page=${page}`, token, { label: `${owner}/${repo}`, action: `List the repository branches (page ${page})`, repo: `${owner}/${repo}` });
|
|
93
129
|
if (!Array.isArray(batch))
|
|
94
130
|
break;
|
|
95
131
|
for (const branch of batch)
|
|
@@ -101,7 +137,7 @@ export async function listBranches(owner, repo, token) {
|
|
|
101
137
|
}
|
|
102
138
|
/** The commit a branch points at, with its first message line. */
|
|
103
139
|
export async function getBranchHead(owner, repo, branch, token) {
|
|
104
|
-
const commit = await githubRequest(`/repos/${owner}/${repo}/commits/${encodeURIComponent(branch)}`, token, `${owner}/${repo}@${branch}`);
|
|
140
|
+
const commit = await githubRequest(`/repos/${owner}/${repo}/commits/${encodeURIComponent(branch)}`, token, { label: `${owner}/${repo}@${branch}`, action: `Read the head commit of ${branch}`, repo: `${owner}/${repo}` });
|
|
105
141
|
return {
|
|
106
142
|
sha: commit.sha,
|
|
107
143
|
message: String(commit.commit?.message ?? "").split("\n")[0],
|
|
@@ -110,7 +146,11 @@ export async function getBranchHead(owner, repo, branch, token) {
|
|
|
110
146
|
}
|
|
111
147
|
/** Login of the account a token belongs to. */
|
|
112
148
|
export async function getTokenLogin(token) {
|
|
113
|
-
const user = await githubRequest("/user", token,
|
|
149
|
+
const user = await githubRequest("/user", token, {
|
|
150
|
+
label: "the authenticated user",
|
|
151
|
+
action: "Identify the account the token belongs to",
|
|
152
|
+
repo: null,
|
|
153
|
+
});
|
|
114
154
|
return user.login;
|
|
115
155
|
}
|
|
116
156
|
/**
|
|
@@ -122,7 +162,7 @@ export async function searchRepositories(term, token, limit) {
|
|
|
122
162
|
const owner = slash >= 0 ? term.slice(0, slash).trim() : "";
|
|
123
163
|
const name = slash >= 0 ? term.slice(slash + 1).trim() : term.trim();
|
|
124
164
|
const qualifiers = [name, "in:name", ...(owner ? [`user:${owner}`] : [])].filter(Boolean).join(" ");
|
|
125
|
-
const data = await githubRequest(`/search/repositories?q=${encodeURIComponent(qualifiers)}&per_page=${limit}`, token, `repositories matching "${term}"`);
|
|
165
|
+
const data = await githubRequest(`/search/repositories?q=${encodeURIComponent(qualifiers)}&per_page=${limit}`, token, { label: `repositories matching "${term}"`, action: `Search repositories matching "${term}"`, repo: null });
|
|
126
166
|
return (data.items ?? []).map((item) => ({
|
|
127
167
|
fullName: item.full_name,
|
|
128
168
|
private: !!item.private,
|
|
@@ -158,8 +198,12 @@ export class GitHubCodeApi {
|
|
|
158
198
|
get label() {
|
|
159
199
|
return formatRepoSpec(this.spec);
|
|
160
200
|
}
|
|
161
|
-
request(path) {
|
|
162
|
-
return githubRequest(path, this.token,
|
|
201
|
+
request(path, action) {
|
|
202
|
+
return githubRequest(path, this.token, {
|
|
203
|
+
label: this.label,
|
|
204
|
+
action,
|
|
205
|
+
repo: `${this.spec.owner}/${this.spec.repo}`,
|
|
206
|
+
});
|
|
163
207
|
}
|
|
164
208
|
/**
|
|
165
209
|
* The commit the configured ref points at right now.
|
|
@@ -170,7 +214,7 @@ export class GitHubCodeApi {
|
|
|
170
214
|
resolveRef() {
|
|
171
215
|
return oncePerOperation(`ref:${this.label}`, async () => {
|
|
172
216
|
const ref = this.spec.ref ?? "HEAD";
|
|
173
|
-
const commit = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/commits/${encodeURIComponent(ref)}`);
|
|
217
|
+
const commit = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/commits/${encodeURIComponent(ref)}`, `Resolve ${ref} to its current commit`);
|
|
174
218
|
logger.debug(`${this.label} is at ${String(commit.sha).slice(0, 7)}`);
|
|
175
219
|
return {
|
|
176
220
|
commitSha: commit.sha,
|
|
@@ -190,7 +234,7 @@ export class GitHubCodeApi {
|
|
|
190
234
|
return this.cached(this.commitByDate, isoDate, async () => {
|
|
191
235
|
const ref = this.spec.ref ?? "HEAD";
|
|
192
236
|
const commits = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/commits` +
|
|
193
|
-
`?sha=${encodeURIComponent(ref)}&until=${encodeURIComponent(isoDate)}&per_page=1`);
|
|
237
|
+
`?sha=${encodeURIComponent(ref)}&until=${encodeURIComponent(isoDate)}&per_page=1`, `Find the last commit on ${ref} at or before ${isoDate}`);
|
|
194
238
|
const sha = Array.isArray(commits) && typeof commits[0]?.sha === "string" ? commits[0].sha : undefined;
|
|
195
239
|
logger.debug(`${this.label}: commit at or before ${isoDate} is ${sha ? sha.slice(0, 7) : "none"}`);
|
|
196
240
|
return sha;
|
|
@@ -199,7 +243,7 @@ export class GitHubCodeApi {
|
|
|
199
243
|
/** path → blob sha for every file in the commit's tree. A commit's tree never changes. */
|
|
200
244
|
getTree(commitSha) {
|
|
201
245
|
return this.cached(this.treeByCommit, commitSha, async () => {
|
|
202
|
-
const tree = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/git/trees/${commitSha}?recursive=1`);
|
|
246
|
+
const tree = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/git/trees/${commitSha}?recursive=1`, `List the files of commit ${commitSha.slice(0, 7)}`);
|
|
203
247
|
const map = new Map();
|
|
204
248
|
for (const entry of tree.tree ?? []) {
|
|
205
249
|
if (entry.type === "blob")
|
|
@@ -240,7 +284,7 @@ export class GitHubCodeApi {
|
|
|
240
284
|
if (known || !this.isTreeTruncated(commitSha))
|
|
241
285
|
return known;
|
|
242
286
|
try {
|
|
243
|
-
const contents = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/contents/${encodeURI(repoPath)}?ref=${commitSha}`);
|
|
287
|
+
const contents = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/contents/${encodeURI(repoPath)}?ref=${commitSha}`, `Look up ${repoPath} at ${commitSha.slice(0, 7)}`);
|
|
244
288
|
return typeof contents?.sha === "string" ? contents.sha : undefined;
|
|
245
289
|
}
|
|
246
290
|
catch (err) {
|
|
@@ -249,27 +293,14 @@ export class GitHubCodeApi {
|
|
|
249
293
|
throw err;
|
|
250
294
|
}
|
|
251
295
|
}
|
|
252
|
-
/**
|
|
253
|
-
|
|
296
|
+
/**
|
|
297
|
+
* A blob's bytes. Blobs are immutable, so this is cached for the life of the process.
|
|
298
|
+
* `filePath` names the file in github.log.
|
|
299
|
+
*/
|
|
300
|
+
getBlob(blobSha, filePath) {
|
|
254
301
|
return this.cached(this.blobCache, blobSha, async () => {
|
|
255
|
-
const blob = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/git/blobs/${blobSha}`);
|
|
302
|
+
const blob = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/git/blobs/${blobSha}`, filePath ? `Download ${filePath}` : `Download blob ${blobSha.slice(0, 7)}`);
|
|
256
303
|
return Buffer.from(blob.content ?? "", blob.encoding === "base64" ? "base64" : "utf-8");
|
|
257
304
|
});
|
|
258
305
|
}
|
|
259
|
-
/**
|
|
260
|
-
* Unified diff of every file that changed between two commits, keyed by path.
|
|
261
|
-
*
|
|
262
|
-
* One call covers a whole doc, so `git_diff_doc` groups its tracked files by the commit they
|
|
263
|
-
* were snapshotted at rather than asking per file. GitHub caps the response at 300 files;
|
|
264
|
-
* paths beyond that are simply absent from the map.
|
|
265
|
-
*/
|
|
266
|
-
async comparePatches(baseSha, headSha) {
|
|
267
|
-
const comparison = await this.request(`/repos/${this.spec.owner}/${this.spec.repo}/compare/${baseSha}...${headSha}`);
|
|
268
|
-
const patches = new Map();
|
|
269
|
-
for (const file of comparison.files ?? []) {
|
|
270
|
-
if (typeof file.patch === "string")
|
|
271
|
-
patches.set(file.filename, file.patch);
|
|
272
|
-
}
|
|
273
|
-
return patches;
|
|
274
|
-
}
|
|
275
306
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const GITHUB_LOG_FILE = "github.log";
|
|
2
|
+
/**
|
|
3
|
+
* One call to GitHub, as written to github.log — one JSON object per line.
|
|
4
|
+
*
|
|
5
|
+
* kind "git" for a git command reaching the remote, "api" for a REST request
|
|
6
|
+
* action what was done, in plain words
|
|
7
|
+
* command the exact git command or HTTP request
|
|
8
|
+
* repo "owner/repo" the call targets, when it targets one
|
|
9
|
+
* trigger the tool call, GUI request or server step that caused it
|
|
10
|
+
* status HTTP status, or "ok" / "rejected" / "failed" / "network error"
|
|
11
|
+
*/
|
|
12
|
+
export interface GitHubLogEntry {
|
|
13
|
+
ts: string;
|
|
14
|
+
kind: "git" | "api";
|
|
15
|
+
action: string;
|
|
16
|
+
command: string;
|
|
17
|
+
repo: string | null;
|
|
18
|
+
trigger: string;
|
|
19
|
+
ok: boolean;
|
|
20
|
+
status: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
error?: string;
|
|
23
|
+
rateLimit?: {
|
|
24
|
+
remaining: number;
|
|
25
|
+
limit: number;
|
|
26
|
+
reset: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Runs `fn` with `trigger` recorded as the cause of every GitHub call it makes. */
|
|
30
|
+
export declare function runTriggeredBy<T>(trigger: string, fn: () => T): T;
|
|
31
|
+
/** Opens github.log in `logDir` for appending. */
|
|
32
|
+
export declare function initGitHubLog(logDir: string): void;
|
|
33
|
+
/** Masks credentials embedded in URLs or tokens quoted verbatim. */
|
|
34
|
+
export declare function redactSecrets(text: string, secrets?: Array<string | undefined>): string;
|
|
35
|
+
/** "owner/repo" for a GitHub URL, the host and path for any other remote. */
|
|
36
|
+
export declare function describeRemote(remoteUrl: string): string;
|
|
37
|
+
/** Appends one call to github.log and mirrors it in server.log. */
|
|
38
|
+
export declare function recordGitHubCall(call: Omit<GitHubLogEntry, "ts" | "trigger">): void;
|