@morphllm/gitmorph-sdk 0.2.1 → 0.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.
- package/README.md +429 -0
- package/dist/ai/index.d.ts +48 -0
- package/dist/ai/index.js +25 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/anthropic/index.d.ts +85 -0
- package/dist/anthropic/index.js +64 -0
- package/dist/anthropic/index.js.map +1 -0
- package/dist/chunk-A3AOZFPE.js +474 -0
- package/dist/chunk-A3AOZFPE.js.map +1 -0
- package/dist/chunk-FXEX72CO.js +43 -0
- package/dist/chunk-FXEX72CO.js.map +1 -0
- package/dist/chunk-JXXUBMF6.js +61 -0
- package/dist/chunk-JXXUBMF6.js.map +1 -0
- package/dist/chunk-QT3Y4ZIS.js +547 -0
- package/dist/chunk-QT3Y4ZIS.js.map +1 -0
- package/dist/{types.d.ts → client-B_I_0i1T.d.ts} +69 -37
- package/dist/index.d.ts +21 -5
- package/dist/index.js +3 -547
- package/dist/index.js.map +1 -0
- package/dist/instructions-BE0g1eFs.d.ts +284 -0
- package/dist/mcp/bin.d.ts +1 -0
- package/dist/mcp/bin.js +21 -0
- package/dist/mcp/bin.js.map +1 -0
- package/dist/mcp/index.d.ts +29 -0
- package/dist/mcp/index.js +5 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/openai/index.d.ts +60 -0
- package/dist/openai/index.js +75 -0
- package/dist/openai/index.js.map +1 -0
- package/package.json +56 -8
- package/dist/api.d.ts +0 -10
- package/dist/api.d.ts.map +0 -1
- package/dist/client.d.ts +0 -13
- package/dist/client.d.ts.map +0 -1
- package/dist/config.d.ts +0 -8
- package/dist/config.d.ts.map +0 -1
- package/dist/errors.d.ts +0 -18
- package/dist/errors.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/mirror.d.ts +0 -6
- package/dist/mirror.d.ts.map +0 -1
- package/dist/repo.d.ts +0 -28
- package/dist/repo.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/utils.d.ts +0 -5
- package/dist/utils.d.ts.map +0 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { toJsonSchema, clampOutput } from './chunk-FXEX72CO.js';
|
|
2
|
+
import { resolveToolSet } from './chunk-A3AOZFPE.js';
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
|
|
7
|
+
function createGitmorphMcpServer(gm, opts = {}) {
|
|
8
|
+
const normalized = resolveToolSet(gm, opts);
|
|
9
|
+
const byName = new Map(normalized.map((t) => [t.name, t]));
|
|
10
|
+
const maxBytes = opts.maxOutputBytes;
|
|
11
|
+
const server = new Server(
|
|
12
|
+
{ name: "gitmorph", version: "0.3.0" },
|
|
13
|
+
{ capabilities: { tools: {} } }
|
|
14
|
+
);
|
|
15
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
16
|
+
tools: normalized.map((t) => ({
|
|
17
|
+
name: t.name,
|
|
18
|
+
description: t.description,
|
|
19
|
+
inputSchema: toJsonSchema(t.schema)
|
|
20
|
+
}))
|
|
21
|
+
}));
|
|
22
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
23
|
+
const def = byName.get(req.params.name);
|
|
24
|
+
if (!def) {
|
|
25
|
+
return {
|
|
26
|
+
isError: true,
|
|
27
|
+
content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const parsed = def.schema.safeParse(req.params.arguments ?? {});
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return {
|
|
33
|
+
isError: true,
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: `Invalid input for ${req.params.name}: ${parsed.error.message}`
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const result = await def.execute(parsed.data);
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text", text: clampOutput(result, maxBytes) }]
|
|
46
|
+
};
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
49
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return server;
|
|
53
|
+
}
|
|
54
|
+
async function startGitmorphMcpServer(gm, opts) {
|
|
55
|
+
const server = createGitmorphMcpServer(gm, opts);
|
|
56
|
+
await server.connect(new StdioServerTransport());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { createGitmorphMcpServer, startGitmorphMcpServer };
|
|
60
|
+
//# sourceMappingURL=chunk-JXXUBMF6.js.map
|
|
61
|
+
//# sourceMappingURL=chunk-JXXUBMF6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mcp/index.ts"],"names":[],"mappings":";;;;;;AA+BO,SAAS,uBAAA,CACd,EAAA,EACA,IAAA,GAA2B,EAAC,EACpB;AACR,EAAA,MAAM,UAAA,GAAa,cAAA,CAAe,EAAA,EAAI,IAAI,CAAA;AAC1C,EAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,IAAA,EAAM,CAAC,CAAC,CAAC,CAAA;AACzD,EAAA,MAAM,WAAW,IAAA,CAAK,cAAA;AAEtB,EAAA,MAAM,SAAS,IAAI,MAAA;AAAA,IACjB,EAAE,IAAA,EAAM,UAAA,EAAY,OAAA,EAAS,OAAA,EAAQ;AAAA,IACrC,EAAE,YAAA,EAAc,EAAE,KAAA,EAAO,IAAG;AAAE,GAChC;AAEA,EAAA,MAAA,CAAO,iBAAA,CAAkB,wBAAwB,aAAa;AAAA,IAC5D,KAAA,EAAO,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MAC5B,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,aAAa,CAAA,CAAE,WAAA;AAAA,MACf,WAAA,EAAa,YAAA,CAAa,CAAA,CAAE,MAAM;AAAA,KACpC,CAAE;AAAA,GACJ,CAAE,CAAA;AAEF,EAAA,MAAA,CAAO,iBAAA,CAAkB,qBAAA,EAAuB,OAAO,GAAA,KAAQ;AAC7D,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,GAAA,CAAI,GAAA,CAAI,OAAO,IAAI,CAAA;AACtC,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,IAAA;AAAA,QACT,OAAA,EAAS,CAAC,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,CAAA,cAAA,EAAiB,GAAA,CAAI,MAAA,CAAO,IAAI,CAAA,CAAA,EAAI;AAAA,OACtE;AAAA,IACF;AACA,IAAA,MAAM,MAAA,GAAS,IAAI,MAAA,CAAO,SAAA,CAAU,IAAI,MAAA,CAAO,SAAA,IAAa,EAAE,CAAA;AAC9D,IAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACnB,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,IAAA;AAAA,QACT,OAAA,EAAS;AAAA,UACP;AAAA,YACE,IAAA,EAAM,MAAA;AAAA,YACN,IAAA,EAAM,qBAAqB,GAAA,CAAI,MAAA,CAAO,IAAI,CAAA,EAAA,EAAK,MAAA,CAAO,MAAM,OAAO,CAAA;AAAA;AACrE;AACF,OACF;AAAA,IACF;AACA,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,GAAA,CAAI,OAAA,CAAQ,OAAO,IAAI,CAAA;AAC5C,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,CAAC,EAAE,IAAA,EAAM,MAAA,EAAQ,MAAM,WAAA,CAAY,MAAA,EAAQ,QAAQ,CAAA,EAAG;AAAA,OACjE;AAAA,IACF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,MAAA,OAAO,EAAE,OAAA,EAAS,IAAA,EAAM,OAAA,EAAS,CAAC,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,CAAA,EAAE;AAAA,IACrE;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;AAMA,eAAsB,sBAAA,CACpB,IACA,IAAA,EACe;AACf,EAAA,MAAM,MAAA,GAAS,uBAAA,CAAwB,EAAA,EAAI,IAAI,CAAA;AAC/C,EAAA,MAAM,MAAA,CAAO,OAAA,CAAQ,IAAI,oBAAA,EAAsB,CAAA;AACjD","file":"chunk-JXXUBMF6.js","sourcesContent":["import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport type { GitMorph } from \"../client.js\";\nimport { resolveToolSet, type CreateToolsOptions } from \"../tools/core.js\";\nimport { toJsonSchema } from \"../tools/json-schema.js\";\nimport { clampOutput } from \"../tools/truncate.js\";\n\nexport { GITMORPH_SYSTEM_PROMPT } from \"../tools/instructions.js\";\nexport { DEFAULT_TOOL_NAMES } from \"../tools/registry.js\";\nexport type { CreateToolsOptions } from \"../tools/core.js\";\nexport type { GitmorphToolName } from \"../tools/registry.js\";\n\n/**\n * Build an MCP server that exposes the GitMorph toolkit over stdio.\n *\n * Use this when you want to register GitMorph with any MCP-compatible client\n * (Claude Desktop, Cursor, Claude Code, etc.). For the common case, prefer\n * the shipped CLI: `npx @morphllm/gitmorph-sdk mcp`.\n *\n * @example\n * ```ts\n * import { GitMorph } from \"@morphllm/gitmorph-sdk\";\n * import { startGitmorphMcpServer } from \"@morphllm/gitmorph-sdk/mcp\";\n *\n * await startGitmorphMcpServer(new GitMorph());\n * ```\n */\nexport function createGitmorphMcpServer(\n gm: GitMorph,\n opts: CreateToolsOptions = {},\n): Server {\n const normalized = resolveToolSet(gm, opts);\n const byName = new Map(normalized.map((t) => [t.name, t]));\n const maxBytes = opts.maxOutputBytes;\n\n const server = new Server(\n { name: \"gitmorph\", version: \"0.3.0\" },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: normalized.map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: toJsonSchema(t.schema),\n })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (req) => {\n const def = byName.get(req.params.name);\n if (!def) {\n return {\n isError: true,\n content: [{ type: \"text\", text: `Unknown tool: ${req.params.name}` }],\n };\n }\n const parsed = def.schema.safeParse(req.params.arguments ?? {});\n if (!parsed.success) {\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: `Invalid input for ${req.params.name}: ${parsed.error.message}`,\n },\n ],\n };\n }\n try {\n const result = await def.execute(parsed.data);\n return {\n content: [{ type: \"text\", text: clampOutput(result, maxBytes) }],\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return { isError: true, content: [{ type: \"text\", text: message }] };\n }\n });\n\n return server;\n}\n\n/**\n * Start an MCP stdio server for the GitMorph toolkit. Resolves when the\n * server's transport disconnects.\n */\nexport async function startGitmorphMcpServer(\n gm: GitMorph,\n opts?: CreateToolsOptions,\n): Promise<void> {\n const server = createGitmorphMcpServer(gm, opts);\n await server.connect(new StdioServerTransport());\n}\n"]}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var GitMorphError = class extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "GitMorphError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var AuthenticationError = class extends GitMorphError {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(
|
|
14
|
+
message ?? "Not authenticated. Provide a token or configure ~/.config/gm/config.json"
|
|
15
|
+
);
|
|
16
|
+
this.name = "AuthenticationError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var ApiError = class extends GitMorphError {
|
|
20
|
+
status;
|
|
21
|
+
url;
|
|
22
|
+
constructor(message, status, url) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "ApiError";
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.url = url;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var MirrorError = class extends GitMorphError {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "MirrorError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var GrepError = class extends GitMorphError {
|
|
36
|
+
constructor(message) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "GrepError";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/api.ts
|
|
43
|
+
function buildUrl(path, options) {
|
|
44
|
+
const baseUrl = `https://${options.host}/api/v1`;
|
|
45
|
+
const url = new URL(path.startsWith("/") ? `${baseUrl}${path}` : `${baseUrl}/${path}`);
|
|
46
|
+
if (options.params) {
|
|
47
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
48
|
+
url.searchParams.set(key, value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return url;
|
|
52
|
+
}
|
|
53
|
+
function buildHeaders(options) {
|
|
54
|
+
const headers = {
|
|
55
|
+
Authorization: `token ${options.token}`
|
|
56
|
+
};
|
|
57
|
+
if (options.body) {
|
|
58
|
+
headers["Content-Type"] = "application/json";
|
|
59
|
+
headers["Accept"] = "application/json";
|
|
60
|
+
} else {
|
|
61
|
+
headers["Accept"] = "application/json";
|
|
62
|
+
}
|
|
63
|
+
return headers;
|
|
64
|
+
}
|
|
65
|
+
async function handleErrorResponse(res, url) {
|
|
66
|
+
let message = `API request failed: ${res.status}`;
|
|
67
|
+
try {
|
|
68
|
+
const body = await res.json();
|
|
69
|
+
if (body.message) message = body.message;
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
throw new ApiError(message, res.status, url.toString());
|
|
73
|
+
}
|
|
74
|
+
async function apiRequest(method, path, options) {
|
|
75
|
+
const url = buildUrl(path, options);
|
|
76
|
+
const headers = buildHeaders(options);
|
|
77
|
+
const res = await fetch(url.toString(), {
|
|
78
|
+
method,
|
|
79
|
+
headers,
|
|
80
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) return handleErrorResponse(res, url);
|
|
83
|
+
if (res.status === 204) return void 0;
|
|
84
|
+
return await res.json();
|
|
85
|
+
}
|
|
86
|
+
async function apiRequestRaw(method, path, options) {
|
|
87
|
+
const url = buildUrl(path, options);
|
|
88
|
+
const headers = {
|
|
89
|
+
Authorization: `token ${options.token}`
|
|
90
|
+
};
|
|
91
|
+
const res = await fetch(url.toString(), { method, headers });
|
|
92
|
+
if (!res.ok) return handleErrorResponse(res, url);
|
|
93
|
+
return res.text();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/repo.ts
|
|
97
|
+
var GitMorphRepo = class {
|
|
98
|
+
owner;
|
|
99
|
+
name;
|
|
100
|
+
auth;
|
|
101
|
+
treeCache = /* @__PURE__ */ new Map();
|
|
102
|
+
defaultRef;
|
|
103
|
+
repoId;
|
|
104
|
+
constructor(owner, name, auth) {
|
|
105
|
+
this.owner = owner;
|
|
106
|
+
this.name = name;
|
|
107
|
+
this.auth = auth;
|
|
108
|
+
}
|
|
109
|
+
async resolveRepoInfo() {
|
|
110
|
+
if (this.defaultRef && this.repoId) {
|
|
111
|
+
return { ref: this.defaultRef, id: this.repoId };
|
|
112
|
+
}
|
|
113
|
+
const info = await apiRequest(
|
|
114
|
+
"GET",
|
|
115
|
+
`/repos/${this.owner}/${this.name}`,
|
|
116
|
+
this.auth
|
|
117
|
+
);
|
|
118
|
+
this.defaultRef = info.default_branch;
|
|
119
|
+
this.repoId = info.id;
|
|
120
|
+
return { ref: this.defaultRef, id: this.repoId };
|
|
121
|
+
}
|
|
122
|
+
async resolveRef(ref) {
|
|
123
|
+
if (ref) return ref;
|
|
124
|
+
const info = await this.resolveRepoInfo();
|
|
125
|
+
return info.ref;
|
|
126
|
+
}
|
|
127
|
+
async resolveRepoId() {
|
|
128
|
+
const info = await this.resolveRepoInfo();
|
|
129
|
+
return info.id;
|
|
130
|
+
}
|
|
131
|
+
async fetchTree(ref) {
|
|
132
|
+
if (this.treeCache.has(ref)) {
|
|
133
|
+
return { entries: this.treeCache.get(ref), truncated: false };
|
|
134
|
+
}
|
|
135
|
+
const encodedRef = encodeURIComponent(ref);
|
|
136
|
+
const firstPage = await apiRequest(
|
|
137
|
+
"GET",
|
|
138
|
+
`/repos/${this.owner}/${this.name}/git/trees/${encodedRef}`,
|
|
139
|
+
{ ...this.auth, params: { recursive: "true", per_page: "10000" } }
|
|
140
|
+
);
|
|
141
|
+
let allEntries = [...firstPage.tree];
|
|
142
|
+
let truncated = firstPage.truncated;
|
|
143
|
+
if (truncated && firstPage.total_count && firstPage.total_count > allEntries.length) {
|
|
144
|
+
let page = 2;
|
|
145
|
+
while (allEntries.length < firstPage.total_count) {
|
|
146
|
+
const nextPage = await apiRequest(
|
|
147
|
+
"GET",
|
|
148
|
+
`/repos/${this.owner}/${this.name}/git/trees/${encodedRef}`,
|
|
149
|
+
{ ...this.auth, params: { recursive: "true", per_page: "10000", page: String(page) } }
|
|
150
|
+
);
|
|
151
|
+
if (!nextPage.tree.length) break;
|
|
152
|
+
allEntries = allEntries.concat(nextPage.tree);
|
|
153
|
+
truncated = nextPage.truncated;
|
|
154
|
+
page++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
this.treeCache.set(ref, allEntries);
|
|
158
|
+
return { entries: allEntries, truncated };
|
|
159
|
+
}
|
|
160
|
+
async fetchRaw(path, ref) {
|
|
161
|
+
const encodedRef = encodeURIComponent(ref);
|
|
162
|
+
return apiRequestRaw(
|
|
163
|
+
"GET",
|
|
164
|
+
`/repos/${this.owner}/${this.name}/raw/${path}`,
|
|
165
|
+
{ ...this.auth, params: { ref: encodedRef } }
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
// ── readFile ──────────────────────────────────────────────────────────────
|
|
169
|
+
async readFile(path, options) {
|
|
170
|
+
const ref = await this.resolveRef(options?.ref);
|
|
171
|
+
if (options?.lines) {
|
|
172
|
+
for (const range of options.lines) {
|
|
173
|
+
if (range.start < 1) {
|
|
174
|
+
throw new GitMorphError(`LineRange.start must be >= 1, got ${range.start}`);
|
|
175
|
+
}
|
|
176
|
+
if (range.end < range.start) {
|
|
177
|
+
throw new GitMorphError(`LineRange.end (${range.end}) must be >= start (${range.start})`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const rawContent = await this.fetchRaw(path, ref);
|
|
182
|
+
const allLines = rawContent.split("\n");
|
|
183
|
+
const totalLines = allLines.length;
|
|
184
|
+
if (!options?.lines) {
|
|
185
|
+
return { path, content: rawContent, totalLines };
|
|
186
|
+
}
|
|
187
|
+
const clampedRanges = [];
|
|
188
|
+
const extracted = [];
|
|
189
|
+
for (const range of options.lines) {
|
|
190
|
+
const start = Math.min(range.start, totalLines);
|
|
191
|
+
const end = Math.min(range.end, totalLines);
|
|
192
|
+
clampedRanges.push({ start, end });
|
|
193
|
+
extracted.push(allLines.slice(start - 1, end).join("\n"));
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
path,
|
|
197
|
+
content: extracted.join("\n"),
|
|
198
|
+
totalLines,
|
|
199
|
+
lines: clampedRanges
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// ── grep (server-side via Bleve/git-grep) ─────────────────────────────────
|
|
203
|
+
async grep(options) {
|
|
204
|
+
if (!options.pattern) {
|
|
205
|
+
throw new GrepError("pattern is required");
|
|
206
|
+
}
|
|
207
|
+
const repoId = await this.resolveRepoId();
|
|
208
|
+
const params = {
|
|
209
|
+
q: options.pattern,
|
|
210
|
+
repo_id: String(repoId)
|
|
211
|
+
};
|
|
212
|
+
if (options.language) params.l = options.language;
|
|
213
|
+
if (options.maxMatches) params.limit = String(Math.min(options.maxMatches, 50));
|
|
214
|
+
if (options.page) params.page = String(options.page);
|
|
215
|
+
const response = await apiRequest(
|
|
216
|
+
"GET",
|
|
217
|
+
"/repos/code/search",
|
|
218
|
+
{ ...this.auth, params }
|
|
219
|
+
);
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
throw new GrepError("Code search returned ok=false");
|
|
222
|
+
}
|
|
223
|
+
const matches = [];
|
|
224
|
+
const caseSensitive = options.caseSensitive ?? false;
|
|
225
|
+
const flags = caseSensitive ? "g" : "gi";
|
|
226
|
+
let regex = null;
|
|
227
|
+
try {
|
|
228
|
+
regex = new RegExp(options.pattern, flags);
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
231
|
+
for (const item of response.data) {
|
|
232
|
+
for (const line of item.lines) {
|
|
233
|
+
const plainContent = line.content.replace(/<[^>]*>/g, "");
|
|
234
|
+
const submatches = [];
|
|
235
|
+
if (regex) {
|
|
236
|
+
regex.lastIndex = 0;
|
|
237
|
+
let m;
|
|
238
|
+
while ((m = regex.exec(plainContent)) !== null) {
|
|
239
|
+
submatches.push({
|
|
240
|
+
match: m[0],
|
|
241
|
+
startOffset: m.index,
|
|
242
|
+
endOffset: m.index + m[0].length
|
|
243
|
+
});
|
|
244
|
+
if (!regex.global) break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
matches.push({
|
|
248
|
+
path: item.filename,
|
|
249
|
+
lineNumber: line.num,
|
|
250
|
+
lineContent: plainContent,
|
|
251
|
+
submatches
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { matches, total: response.total };
|
|
256
|
+
}
|
|
257
|
+
// ── getFileContents (batch) ───────────────────────────────────────────────
|
|
258
|
+
async getFileContents(paths, options) {
|
|
259
|
+
if (paths.length === 0) return [];
|
|
260
|
+
const ref = await this.resolveRef(options?.ref);
|
|
261
|
+
const encodedBody = JSON.stringify({ files: paths });
|
|
262
|
+
const raw = await apiRequest(
|
|
263
|
+
"GET",
|
|
264
|
+
`/repos/${this.owner}/${this.name}/file-contents`,
|
|
265
|
+
{
|
|
266
|
+
...this.auth,
|
|
267
|
+
params: { ref, body: encodedBody }
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
return raw.map((item) => {
|
|
271
|
+
if (!item) return null;
|
|
272
|
+
return {
|
|
273
|
+
name: item.name,
|
|
274
|
+
path: item.path,
|
|
275
|
+
sha: item.sha,
|
|
276
|
+
size: item.size,
|
|
277
|
+
encoding: item.encoding,
|
|
278
|
+
content: item.content ? item.encoding === "base64" ? Buffer.from(item.content, "base64").toString("utf-8") : item.content : null,
|
|
279
|
+
downloadUrl: item.download_url
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
// ── glob ──────────────────────────────────────────────────────────────────
|
|
284
|
+
async glob(options) {
|
|
285
|
+
const ref = await this.resolveRef(options.ref);
|
|
286
|
+
const encodedRef = encodeURIComponent(ref);
|
|
287
|
+
const params = new URLSearchParams();
|
|
288
|
+
for (const p of options.patterns) {
|
|
289
|
+
params.append("pattern", p);
|
|
290
|
+
}
|
|
291
|
+
if (options.prefix) params.set("prefix", options.prefix);
|
|
292
|
+
if (options.sizes === false) params.set("sizes", "false");
|
|
293
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
294
|
+
const url = `/repos/${this.owner}/${this.name}/git/trees/${encodedRef}/glob?${params.toString()}`;
|
|
295
|
+
const data = await apiRequest("GET", url, this.auth);
|
|
296
|
+
return {
|
|
297
|
+
entries: data.tree,
|
|
298
|
+
truncated: data.truncated
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// ── listBranches ──────────────────────────────────────────────────────────
|
|
302
|
+
async listBranches(options) {
|
|
303
|
+
const params = {};
|
|
304
|
+
if (options?.page) params.page = String(options.page);
|
|
305
|
+
if (options?.limit) params.limit = String(options.limit);
|
|
306
|
+
const raw = await apiRequest(
|
|
307
|
+
"GET",
|
|
308
|
+
`/repos/${this.owner}/${this.name}/branches`,
|
|
309
|
+
{ ...this.auth, params }
|
|
310
|
+
);
|
|
311
|
+
return raw.map((b) => ({
|
|
312
|
+
name: b.name,
|
|
313
|
+
commit: {
|
|
314
|
+
id: b.commit?.id ?? "",
|
|
315
|
+
message: b.commit?.message ?? "",
|
|
316
|
+
url: b.commit?.url ?? "",
|
|
317
|
+
timestamp: b.commit?.timestamp ?? "",
|
|
318
|
+
author: {
|
|
319
|
+
name: b.commit?.author?.name ?? "",
|
|
320
|
+
email: b.commit?.author?.email ?? ""
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
protected: b.protected ?? false
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
// ── listCommits ──────────────────────────────────────────────────────────
|
|
327
|
+
async listCommits(options) {
|
|
328
|
+
const params = {
|
|
329
|
+
stat: "false",
|
|
330
|
+
verification: "false",
|
|
331
|
+
files: "false"
|
|
332
|
+
};
|
|
333
|
+
if (options?.sha) params.sha = options.sha;
|
|
334
|
+
if (options?.path) params.path = options.path;
|
|
335
|
+
if (options?.since) params.since = options.since;
|
|
336
|
+
if (options?.until) params.until = options.until;
|
|
337
|
+
if (options?.page) params.page = String(options.page);
|
|
338
|
+
if (options?.limit) params.limit = String(options.limit);
|
|
339
|
+
const raw = await apiRequest(
|
|
340
|
+
"GET",
|
|
341
|
+
`/repos/${this.owner}/${this.name}/commits`,
|
|
342
|
+
{ ...this.auth, params }
|
|
343
|
+
);
|
|
344
|
+
return raw.map((c) => ({
|
|
345
|
+
sha: c.sha ?? "",
|
|
346
|
+
message: c.commit?.message ?? "",
|
|
347
|
+
author: {
|
|
348
|
+
name: c.commit?.author?.name ?? "",
|
|
349
|
+
email: c.commit?.author?.email ?? "",
|
|
350
|
+
date: c.commit?.author?.date ?? ""
|
|
351
|
+
},
|
|
352
|
+
committer: {
|
|
353
|
+
name: c.commit?.committer?.name ?? "",
|
|
354
|
+
email: c.commit?.committer?.email ?? "",
|
|
355
|
+
date: c.commit?.committer?.date ?? ""
|
|
356
|
+
},
|
|
357
|
+
html_url: c.html_url ?? "",
|
|
358
|
+
parents: (c.parents ?? []).map((p) => ({
|
|
359
|
+
sha: p.sha ?? "",
|
|
360
|
+
url: p.url ?? ""
|
|
361
|
+
}))
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
// ── listDir ───────────────────────────────────────────────────────────────
|
|
365
|
+
async listDir(options) {
|
|
366
|
+
const ref = await this.resolveRef(options?.ref);
|
|
367
|
+
const { entries: treeEntries, truncated } = await this.fetchTree(ref);
|
|
368
|
+
const prefix = options?.path?.replace(/\/$/, "") ?? "";
|
|
369
|
+
const filtered = treeEntries.filter((e) => {
|
|
370
|
+
if (prefix && !e.path.startsWith(prefix)) return false;
|
|
371
|
+
if (!options?.recursive) {
|
|
372
|
+
const relative = prefix ? e.path.slice(prefix.length).replace(/^\//, "") : e.path;
|
|
373
|
+
if (relative.includes("/")) return false;
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
});
|
|
377
|
+
const entries = filtered.map((e) => ({
|
|
378
|
+
path: e.path,
|
|
379
|
+
type: e.type === "blob" ? "file" : "dir",
|
|
380
|
+
size: e.size,
|
|
381
|
+
mode: e.mode
|
|
382
|
+
}));
|
|
383
|
+
return { entries, truncated };
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
var DEFAULT_HOST = "gitmorph.com";
|
|
387
|
+
function configPath() {
|
|
388
|
+
const dir = process.env.GM_CONFIG_DIR || join(process.env.HOME || "", ".config", "gm");
|
|
389
|
+
return join(dir, "config.json");
|
|
390
|
+
}
|
|
391
|
+
function loadConfig() {
|
|
392
|
+
try {
|
|
393
|
+
const raw = readFileSync(configPath(), "utf-8");
|
|
394
|
+
return JSON.parse(raw);
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function resolveAuth(options) {
|
|
400
|
+
const token = options?.token || process.env.GM_TOKEN || loadConfig()?.token || null;
|
|
401
|
+
if (!token) {
|
|
402
|
+
throw new AuthenticationError();
|
|
403
|
+
}
|
|
404
|
+
const host = options?.host || process.env.GM_HOST || loadConfig()?.host || DEFAULT_HOST;
|
|
405
|
+
return { token, host };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/mirror.ts
|
|
409
|
+
var SERVICE_MAP = {
|
|
410
|
+
github: "github",
|
|
411
|
+
gitea: "gitea",
|
|
412
|
+
gitlab: "gitlab"
|
|
413
|
+
};
|
|
414
|
+
function parseSource(source, options) {
|
|
415
|
+
let cloneAddr;
|
|
416
|
+
let service;
|
|
417
|
+
let owner;
|
|
418
|
+
let repoName;
|
|
419
|
+
if (source.startsWith("https://") || source.startsWith("http://")) {
|
|
420
|
+
const url = new URL(source);
|
|
421
|
+
const pathParts = url.pathname.replace(/^\//, "").replace(/\.git$/, "").split("/");
|
|
422
|
+
if (pathParts.length < 2 || !pathParts[0] || !pathParts[1]) {
|
|
423
|
+
throw new MirrorError(`Cannot parse source URL: "${source}". Expected "https://host/owner/repo".`);
|
|
424
|
+
}
|
|
425
|
+
owner = pathParts[0];
|
|
426
|
+
repoName = pathParts[1];
|
|
427
|
+
cloneAddr = `${url.origin}/${owner}/${repoName}.git`;
|
|
428
|
+
const hostname = url.hostname;
|
|
429
|
+
if (hostname.includes("github")) service = SERVICE_MAP.github;
|
|
430
|
+
else if (hostname.includes("gitlab")) service = SERVICE_MAP.gitlab;
|
|
431
|
+
else if (hostname.includes("gitea")) service = SERVICE_MAP.gitea;
|
|
432
|
+
else service = SERVICE_MAP[options?.source ?? "github"];
|
|
433
|
+
} else {
|
|
434
|
+
const parts = source.replace(/\.git$/, "").split("/");
|
|
435
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
436
|
+
throw new MirrorError(`Invalid source: "${source}". Expected "owner/repo" or a full URL.`);
|
|
437
|
+
}
|
|
438
|
+
owner = parts[0];
|
|
439
|
+
repoName = parts[1];
|
|
440
|
+
const serviceKey = options?.source ?? "github";
|
|
441
|
+
service = SERVICE_MAP[serviceKey];
|
|
442
|
+
const hostMap = {
|
|
443
|
+
github: "https://github.com",
|
|
444
|
+
gitlab: "https://gitlab.com",
|
|
445
|
+
gitea: "https://gitea.com"
|
|
446
|
+
};
|
|
447
|
+
cloneAddr = `${hostMap[serviceKey]}/${owner}/${repoName}.git`;
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
cloneAddr,
|
|
451
|
+
service,
|
|
452
|
+
repoName: options?.repoName ?? repoName,
|
|
453
|
+
owner
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
async function mirrorRepository(source, auth, options) {
|
|
457
|
+
const { cloneAddr, service, repoName } = parseSource(source, options);
|
|
458
|
+
const body = {
|
|
459
|
+
clone_addr: cloneAddr,
|
|
460
|
+
repo_name: repoName,
|
|
461
|
+
service,
|
|
462
|
+
mirror: true,
|
|
463
|
+
private: options?.private ?? false,
|
|
464
|
+
wiki: options?.wiki ?? false,
|
|
465
|
+
issues: options?.issues ?? false,
|
|
466
|
+
pull_requests: options?.pullRequests ?? false,
|
|
467
|
+
releases: options?.releases ?? false,
|
|
468
|
+
labels: options?.labels ?? false,
|
|
469
|
+
milestones: options?.milestones ?? false,
|
|
470
|
+
lfs: options?.lfs ?? false
|
|
471
|
+
};
|
|
472
|
+
try {
|
|
473
|
+
return await apiRequest("POST", "/repos/migrate", {
|
|
474
|
+
...auth,
|
|
475
|
+
body
|
|
476
|
+
});
|
|
477
|
+
} catch (err) {
|
|
478
|
+
if (err instanceof Error && "status" in err) throw err;
|
|
479
|
+
throw new MirrorError(err.message);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/utils.ts
|
|
484
|
+
function parseRepoSlug(input) {
|
|
485
|
+
const cleaned = input.replace(/\.git$/, "");
|
|
486
|
+
const parts = cleaned.split("/");
|
|
487
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
488
|
+
throw new GitMorphError(
|
|
489
|
+
`Invalid repo format: "${input}". Expected "owner/repo".`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return { owner: parts[0], name: parts[1] };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/client.ts
|
|
496
|
+
var GitMorph = class {
|
|
497
|
+
token;
|
|
498
|
+
host;
|
|
499
|
+
constructor(options) {
|
|
500
|
+
const auth = resolveAuth(options);
|
|
501
|
+
this.token = auth.token;
|
|
502
|
+
this.host = auth.host;
|
|
503
|
+
}
|
|
504
|
+
get auth() {
|
|
505
|
+
return { token: this.token, host: this.host };
|
|
506
|
+
}
|
|
507
|
+
repo(slug) {
|
|
508
|
+
const { owner, name } = parseRepoSlug(slug);
|
|
509
|
+
return new GitMorphRepo(owner, name, this.auth);
|
|
510
|
+
}
|
|
511
|
+
async getRepositoryInfo(slug) {
|
|
512
|
+
const { owner, name } = parseRepoSlug(slug);
|
|
513
|
+
return apiRequest("GET", `/repos/${owner}/${name}`, this.auth);
|
|
514
|
+
}
|
|
515
|
+
async mirror(source, options) {
|
|
516
|
+
const repository = await mirrorRepository(source, this.auth, options);
|
|
517
|
+
const repo = this.repo(repository.full_name);
|
|
518
|
+
return { repository, repo };
|
|
519
|
+
}
|
|
520
|
+
async grepAll(options) {
|
|
521
|
+
const params = {
|
|
522
|
+
q: options.pattern
|
|
523
|
+
};
|
|
524
|
+
if (options.language) params.l = options.language;
|
|
525
|
+
if (options.page) params.page = String(options.page);
|
|
526
|
+
if (options.limit) params.limit = String(Math.min(options.limit, 50));
|
|
527
|
+
if (options.sortByStars) params.sort_by_stars = "true";
|
|
528
|
+
const response = await apiRequest(
|
|
529
|
+
"GET",
|
|
530
|
+
"/repos/code/search",
|
|
531
|
+
{ ...this.auth, params }
|
|
532
|
+
);
|
|
533
|
+
const matches = response.data.map((item) => ({
|
|
534
|
+
repoId: item.repo_id,
|
|
535
|
+
repoName: item.repo_name,
|
|
536
|
+
filename: item.filename,
|
|
537
|
+
commitId: item.commit_id,
|
|
538
|
+
language: item.language,
|
|
539
|
+
lines: item.lines.map((l) => ({ num: l.num, content: l.content }))
|
|
540
|
+
}));
|
|
541
|
+
return { matches, total: response.total };
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
export { ApiError, AuthenticationError, GitMorph, GitMorphError, GitMorphRepo, GrepError, MirrorError };
|
|
546
|
+
//# sourceMappingURL=chunk-QT3Y4ZIS.js.map
|
|
547
|
+
//# sourceMappingURL=chunk-QT3Y4ZIS.js.map
|