@agentprojectcontext/apx 1.60.0 → 1.61.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/package.json +1 -1
- package/src/core/agent/tools/handlers/_github.js +22 -0
- package/src/core/agent/tools/handlers/github-create-issue.js +30 -0
- package/src/core/agent/tools/handlers/github-list-repos.js +20 -0
- package/src/core/agent/tools/names.js +6 -0
- package/src/core/agent/tools/registry.js +4 -0
- package/src/core/integrations/catalog.js +17 -27
- package/src/core/integrations/plugins/asana.js +42 -0
- package/src/core/integrations/plugins/github.js +172 -0
- package/src/core/stores/project-files.js +21 -2
- package/src/interfaces/web/dist/assets/index-BuII-tAi.css +1 -0
- package/src/interfaces/web/dist/assets/{index-DFNV6BWh.js → index-DRFIAiiq.js} +171 -171
- package/src/interfaces/web/dist/assets/index-DRFIAiiq.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/components/integrations/PluginConnect.tsx +287 -0
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +28 -26
- package/src/interfaces/web/src/lib/api/integrations.ts +32 -5
- package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +9 -12
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +132 -81
- package/src/interfaces/web/dist/assets/index-DFNV6BWh.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-HU-Wt2l9.css +0 -1
- package/src/interfaces/web/src/components/integrations/AsanaPlugin.tsx +0 -275
package/package.json
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Shared helper for the GitHub agent tools (github-*.js). Underscore file — no
|
|
2
|
+
// tool `name:` of its own. Resolves the project's effective GitHub integration
|
|
3
|
+
// (its own record wins over the default project's) and hands back the token.
|
|
4
|
+
import { resolveProject } from "../helpers.js";
|
|
5
|
+
import { resolveIntegration } from "#core/integrations/index.js";
|
|
6
|
+
|
|
7
|
+
export function resolveGithub(projects, project) {
|
|
8
|
+
const p = resolveProject(projects, project);
|
|
9
|
+
const resolved = resolveIntegration({ projectStorage: p.storagePath, slug: "github" });
|
|
10
|
+
if (!resolved) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"GitHub is not connected for this project. Ask the user to connect it in the web panel → Integrations → Plugins → GitHub.",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
const token = resolved.record.config?.token;
|
|
16
|
+
if (!token) throw new Error("GitHub integration has no token configured");
|
|
17
|
+
return { token, scope: resolved.scope };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const PROJECT_ARG = {
|
|
21
|
+
project: { type: "string", description: "APX project id/name (optional; defaults to current)" },
|
|
22
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as github from "#core/integrations/plugins/github.js";
|
|
2
|
+
import { resolveGithub, PROJECT_ARG } from "./_github.js";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
name: "github_create_issue",
|
|
6
|
+
category: "integrations",
|
|
7
|
+
schema: {
|
|
8
|
+
type: "function",
|
|
9
|
+
function: {
|
|
10
|
+
name: "github_create_issue",
|
|
11
|
+
description: "Open an issue in a GitHub repository.",
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
owner: { type: "string", description: "Repo owner (user or org)" },
|
|
16
|
+
repo: { type: "string", description: "Repo name" },
|
|
17
|
+
title: { type: "string", description: "Issue title" },
|
|
18
|
+
body: { type: "string", description: "Issue body (markdown)" },
|
|
19
|
+
...PROJECT_ARG,
|
|
20
|
+
},
|
|
21
|
+
required: ["owner", "repo", "title"],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
makeHandler: ({ projects }) => async ({ project, owner, repo, title, body } = {}) => {
|
|
26
|
+
const { token } = resolveGithub(projects, project);
|
|
27
|
+
const issue = await github.createIssue(token, { owner, repo, title, body });
|
|
28
|
+
return { issue: { number: issue.number, url: issue.html_url, title: issue.title } };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as github from "#core/integrations/plugins/github.js";
|
|
2
|
+
import { resolveGithub, PROJECT_ARG } from "./_github.js";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
name: "github_list_repos",
|
|
6
|
+
category: "integrations",
|
|
7
|
+
schema: {
|
|
8
|
+
type: "function",
|
|
9
|
+
function: {
|
|
10
|
+
name: "github_list_repos",
|
|
11
|
+
description: "List GitHub repositories accessible to the connected token.",
|
|
12
|
+
parameters: { type: "object", properties: { ...PROJECT_ARG } },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
makeHandler: ({ projects }) => async ({ project } = {}) => {
|
|
16
|
+
const { token } = resolveGithub(projects, project);
|
|
17
|
+
const repos = await github.listRepos(token);
|
|
18
|
+
return { repos: repos.map((r) => ({ full_name: r.full_name, private: r.private, url: r.html_url, description: r.description })) };
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -56,6 +56,10 @@ export const TOOLS = Object.freeze({
|
|
|
56
56
|
ASANA_CREATE_TASK: "asana_create_task",
|
|
57
57
|
ASANA_UPDATE_TASK: "asana_update_task",
|
|
58
58
|
|
|
59
|
+
// Integrations — GitHub plugin (see core/integrations/plugins/github.js)
|
|
60
|
+
GITHUB_LIST_REPOS: "github_list_repos",
|
|
61
|
+
GITHUB_CREATE_ISSUE: "github_create_issue",
|
|
62
|
+
|
|
59
63
|
// Side-effects
|
|
60
64
|
SEND_TELEGRAM: "send_telegram",
|
|
61
65
|
SET_IDENTITY: "set_identity",
|
|
@@ -104,6 +108,8 @@ export const NATIVE_TOOL_NAMES = new Set([
|
|
|
104
108
|
TOOLS.ASANA_LIST_TASKS,
|
|
105
109
|
TOOLS.ASANA_CREATE_TASK,
|
|
106
110
|
TOOLS.ASANA_UPDATE_TASK,
|
|
111
|
+
TOOLS.GITHUB_LIST_REPOS,
|
|
112
|
+
TOOLS.GITHUB_CREATE_ISSUE,
|
|
107
113
|
TOOLS.SEND_TELEGRAM,
|
|
108
114
|
TOOLS.SET_IDENTITY,
|
|
109
115
|
TOOLS.SET_PERMISSION_MODE,
|
|
@@ -37,6 +37,8 @@ import asanaListProjects from "./handlers/asana-list-projects.js";
|
|
|
37
37
|
import asanaListTasks from "./handlers/asana-list-tasks.js";
|
|
38
38
|
import asanaCreateTask from "./handlers/asana-create-task.js";
|
|
39
39
|
import asanaUpdateTask from "./handlers/asana-update-task.js";
|
|
40
|
+
import githubListRepos from "./handlers/github-list-repos.js";
|
|
41
|
+
import githubCreateIssue from "./handlers/github-create-issue.js";
|
|
40
42
|
import { createPermissionGuard } from "./helpers.js";
|
|
41
43
|
import { buildBridgedTools, DEFAULT_CATEGORIES } from "./registry-bridge.js";
|
|
42
44
|
import { TOOLS, CODE_CHANNEL_TOOLS } from "./names.js";
|
|
@@ -85,6 +87,8 @@ const NATIVE_TOOLS = [
|
|
|
85
87
|
asanaListTasks,
|
|
86
88
|
asanaCreateTask,
|
|
87
89
|
asanaUpdateTask,
|
|
90
|
+
githubListRepos,
|
|
91
|
+
githubCreateIssue,
|
|
88
92
|
];
|
|
89
93
|
|
|
90
94
|
// Registry-backed bridges. Categories can be overridden per-process via env
|
|
@@ -2,32 +2,27 @@
|
|
|
2
2
|
// which are wired end-to-end. The daemon API + web "Plugins" tab read this.
|
|
3
3
|
//
|
|
4
4
|
// A plugin is "implemented" when it has a service module in ./plugins/ that
|
|
5
|
-
// satisfies the lifecycle contract (configure/validate/status/deactivate)
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// satisfies the lifecycle contract (configure/validate/status/deactivate) and
|
|
6
|
+
// declares a `ui` descriptor the generic PluginConnect component renders. The
|
|
7
|
+
// rest carry `coming_soon: true`. Adding a plugin = drop a module in ./plugins/
|
|
8
|
+
// + register it in PLUGIN_SERVICES (open/closed) — no API/route changes.
|
|
9
|
+
//
|
|
10
|
+
// Scope of this catalog (per product decision): Asana, GitHub, WhatsApp only.
|
|
11
|
+
// Telegram is intentionally absent — it's a channel, configured under its own
|
|
12
|
+
// surface, not a service plugin. Transcription lives with the desktop STT stack.
|
|
10
13
|
import { asanaPlugin } from "./plugins/asana.js";
|
|
14
|
+
import { githubPlugin } from "./plugins/github.js";
|
|
11
15
|
|
|
12
16
|
// slug -> live plugin service (must implement the lifecycle contract).
|
|
13
17
|
export const PLUGIN_SERVICES = Object.freeze({
|
|
14
18
|
asana: asanaPlugin,
|
|
19
|
+
github: githubPlugin,
|
|
15
20
|
});
|
|
16
21
|
|
|
17
|
-
// Static descriptors for
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// (GitHub outputs↔repos, a WhatsApp-Web bridge, Telegram-voice whisper) that
|
|
21
|
-
// APX models differently — they are placeholders until ported natively.
|
|
22
|
+
// Static descriptors for plugins that are declared but not yet connectable.
|
|
23
|
+
// WhatsApp needs a WhatsApp-Web bridge (QR pairing) that APX doesn't ship yet,
|
|
24
|
+
// so it stays coming-soon rather than pretending to connect.
|
|
22
25
|
const COMING_SOON = [
|
|
23
|
-
{
|
|
24
|
-
slug: "github",
|
|
25
|
-
name: "GitHub",
|
|
26
|
-
type: "source_control",
|
|
27
|
-
description: "Vinculá repos, issues y PRs para que los agentes trabajen sobre tu código",
|
|
28
|
-
auth: "token",
|
|
29
|
-
coming_soon: true,
|
|
30
|
-
},
|
|
31
26
|
{
|
|
32
27
|
slug: "whatsapp",
|
|
33
28
|
name: "WhatsApp",
|
|
@@ -36,17 +31,11 @@ const COMING_SOON = [
|
|
|
36
31
|
auth: "qr",
|
|
37
32
|
coming_soon: true,
|
|
38
33
|
},
|
|
39
|
-
{
|
|
40
|
-
slug: "local-transcription",
|
|
41
|
-
name: "Transcripción Local",
|
|
42
|
-
type: "transcription",
|
|
43
|
-
description: "Transcripción de audio con faster-whisper local — sin depender de una API externa",
|
|
44
|
-
auth: "none",
|
|
45
|
-
coming_soon: true,
|
|
46
|
-
},
|
|
47
34
|
];
|
|
48
35
|
|
|
49
|
-
// The full catalog: implemented plugins first, then coming-soon.
|
|
36
|
+
// The full catalog: implemented plugins first, then coming-soon. Implemented
|
|
37
|
+
// entries carry their `ui` descriptor + tools so the generic component can
|
|
38
|
+
// render their config form.
|
|
50
39
|
export function listCatalog() {
|
|
51
40
|
const implemented = Object.values(PLUGIN_SERVICES).map((p) => ({
|
|
52
41
|
slug: p.slug,
|
|
@@ -55,6 +44,7 @@ export function listCatalog() {
|
|
|
55
44
|
description: p.description,
|
|
56
45
|
auth: p.auth,
|
|
57
46
|
tools: p.tools || [],
|
|
47
|
+
ui: p.ui || null,
|
|
58
48
|
coming_soon: false,
|
|
59
49
|
}));
|
|
60
50
|
return [...implemented, ...COMING_SOON];
|
|
@@ -142,6 +142,48 @@ export const asanaPlugin = {
|
|
|
142
142
|
{ slug: "asana_update_task", desc: "Actualizar estado o campos de una tarea" },
|
|
143
143
|
],
|
|
144
144
|
|
|
145
|
+
// Declarative UI descriptor consumed by the generic PluginConnect component
|
|
146
|
+
// (see web/components/integrations/PluginConnect.tsx). configFields render as
|
|
147
|
+
// inputs; `select` is a post-validate picker sourced from an action; and
|
|
148
|
+
// connectedFields are the status keys shown once connected.
|
|
149
|
+
ui: {
|
|
150
|
+
accent: "rose",
|
|
151
|
+
configFields: [
|
|
152
|
+
{
|
|
153
|
+
key: "personal_access_token",
|
|
154
|
+
label: "Personal Access Token",
|
|
155
|
+
type: "password",
|
|
156
|
+
placeholder: "1/1234567890abcdef:...",
|
|
157
|
+
help: {
|
|
158
|
+
label: "¿Cómo obtener el token?",
|
|
159
|
+
url: "https://app.asana.com/0/my-apps",
|
|
160
|
+
urlLabel: "app.asana.com/0/my-apps",
|
|
161
|
+
steps: [
|
|
162
|
+
"Abrí app.asana.com/0/my-apps en el navegador.",
|
|
163
|
+
'Bajá hasta la sección "Personal access tokens" (no tus apps OAuth).',
|
|
164
|
+
'Hacé clic en "+ New access token".',
|
|
165
|
+
"Dale un nombre y confirmá.",
|
|
166
|
+
'Copiá el token completo — empieza con "1/..." y tiene un ":" en el medio.',
|
|
167
|
+
"Pegalo en el campo de abajo.",
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
select: {
|
|
173
|
+
key: "workspace_gid",
|
|
174
|
+
label: "Seleccioná el workspace a usar",
|
|
175
|
+
action: "workspaces",
|
|
176
|
+
listKey: "workspaces",
|
|
177
|
+
valueKey: "gid",
|
|
178
|
+
labelKey: "name",
|
|
179
|
+
},
|
|
180
|
+
connectedFields: [
|
|
181
|
+
{ key: "user_name", label: "Conectado como" },
|
|
182
|
+
{ key: "user_email", label: "Email" },
|
|
183
|
+
{ key: "workspace_name", label: "Workspace" },
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
|
|
145
187
|
// Save the PAT and/or the target workspace. Returns a patch to persist.
|
|
146
188
|
configure(record, body = {}) {
|
|
147
189
|
const pat = (body.personal_access_token || "").trim();
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// GitHub integration plugin — Personal Access Token auth. Same shape as the
|
|
2
|
+
// Asana plugin: a pure REST client + a lifecycle descriptor the daemon API
|
|
3
|
+
// dispatches to. Self-contained (GitHub REST over fetch); no GitHub App / OAuth
|
|
4
|
+
// broker yet — a PAT is enough to connect, list repos and open issues.
|
|
5
|
+
|
|
6
|
+
const GITHUB_API_BASE = "https://api.github.com";
|
|
7
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
8
|
+
|
|
9
|
+
function headers(token) {
|
|
10
|
+
return {
|
|
11
|
+
Authorization: `Bearer ${token}`,
|
|
12
|
+
Accept: "application/vnd.github+json",
|
|
13
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
14
|
+
"User-Agent": "apx-integrations",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function request(token, method, apiPath, { params, payload } = {}) {
|
|
19
|
+
let url = `${GITHUB_API_BASE}${apiPath}`;
|
|
20
|
+
if (params) {
|
|
21
|
+
const qs = new URLSearchParams();
|
|
22
|
+
for (const [k, v] of Object.entries(params)) {
|
|
23
|
+
if (v !== undefined && v !== null && v !== "") qs.set(k, String(v));
|
|
24
|
+
}
|
|
25
|
+
const s = qs.toString();
|
|
26
|
+
if (s) url += `?${s}`;
|
|
27
|
+
}
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
30
|
+
let res;
|
|
31
|
+
try {
|
|
32
|
+
res = await fetch(url, {
|
|
33
|
+
method,
|
|
34
|
+
headers: headers(token),
|
|
35
|
+
body: payload !== undefined ? JSON.stringify(payload) : undefined,
|
|
36
|
+
signal: controller.signal,
|
|
37
|
+
});
|
|
38
|
+
} catch (e) {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
if (e.name === "AbortError") throw new Error("GitHub request timed out");
|
|
41
|
+
throw new Error(`GitHub request failed: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
let detail = `${res.status}`;
|
|
46
|
+
try {
|
|
47
|
+
const body = await res.json();
|
|
48
|
+
detail = body?.message || JSON.stringify(body);
|
|
49
|
+
} catch {
|
|
50
|
+
/* non-JSON error body */
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`GitHub API ${res.status}: ${detail}`);
|
|
53
|
+
}
|
|
54
|
+
return res.json();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── REST client ──────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export async function validateToken(token) {
|
|
60
|
+
const user = await request(token, "GET", "/user");
|
|
61
|
+
if (!user?.login) throw new Error("GitHub returned an empty user response");
|
|
62
|
+
return user;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function listRepos(token, { affiliation = "owner,collaborator,organization_member", perPage = 30 } = {}) {
|
|
66
|
+
return request(token, "GET", "/user/repos", {
|
|
67
|
+
params: { affiliation, per_page: perPage, sort: "updated" },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function createIssue(token, { owner, repo, title, body }) {
|
|
72
|
+
return request(token, "POST", `/repos/${owner}/${repo}/issues`, { payload: { title, body } });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Plugin descriptor + lifecycle ────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function safeToken(record) {
|
|
78
|
+
const token = record?.config?.token || "";
|
|
79
|
+
if (!token) throw new Error("GitHub token not configured");
|
|
80
|
+
return token;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const githubPlugin = {
|
|
84
|
+
slug: "github",
|
|
85
|
+
name: "GitHub",
|
|
86
|
+
type: "source_control",
|
|
87
|
+
description: "Conectá GitHub con un token para que los agentes listen repos y abran issues",
|
|
88
|
+
auth: "token",
|
|
89
|
+
tools: [
|
|
90
|
+
{ slug: "github_list_repos", desc: "Listar repositorios accesibles" },
|
|
91
|
+
{ slug: "github_create_issue", desc: "Crear un issue en un repo" },
|
|
92
|
+
],
|
|
93
|
+
ui: {
|
|
94
|
+
accent: "slate",
|
|
95
|
+
configFields: [
|
|
96
|
+
{
|
|
97
|
+
key: "token",
|
|
98
|
+
label: "Personal Access Token",
|
|
99
|
+
type: "password",
|
|
100
|
+
placeholder: "ghp_... o github_pat_...",
|
|
101
|
+
help: {
|
|
102
|
+
label: "¿Cómo obtener el token?",
|
|
103
|
+
url: "https://github.com/settings/tokens",
|
|
104
|
+
urlLabel: "github.com/settings/tokens",
|
|
105
|
+
steps: [
|
|
106
|
+
"Abrí github.com/settings/tokens.",
|
|
107
|
+
'Generá un token (classic o fine-grained) con scope "repo".',
|
|
108
|
+
"Copiá el token — empieza con ghp_ o github_pat_.",
|
|
109
|
+
"Pegalo en el campo de abajo.",
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
connectedFields: [
|
|
115
|
+
{ key: "user_login", label: "Conectado como" },
|
|
116
|
+
{ key: "user_name", label: "Nombre" },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
configure(record, body = {}) {
|
|
121
|
+
const token = (body.token || "").trim();
|
|
122
|
+
if (!token && !record) throw new Error("Provide a GitHub token");
|
|
123
|
+
const config = {};
|
|
124
|
+
if (token) config.token = token;
|
|
125
|
+
const patch = { name: "GitHub", type: this.type, description: this.description, config };
|
|
126
|
+
if (token) patch.status = "pending_validation";
|
|
127
|
+
return { patch };
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async validate(record) {
|
|
131
|
+
const token = safeToken(record);
|
|
132
|
+
let user;
|
|
133
|
+
try {
|
|
134
|
+
user = await validateToken(token);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return {
|
|
137
|
+
patch: { status: "error", is_enabled: false, config: { last_error: String(e.message || e) } },
|
|
138
|
+
result: { ok: false, error: String(e.message || e) },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const config = { user_login: user.login, user_name: user.name || null, last_error: null };
|
|
142
|
+
return {
|
|
143
|
+
patch: { status: "active", is_enabled: true, config },
|
|
144
|
+
result: { ok: true, user_login: user.login, user_name: user.name || null },
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
status(record) {
|
|
149
|
+
const config = record?.config || {};
|
|
150
|
+
return {
|
|
151
|
+
slug: this.slug,
|
|
152
|
+
status: record?.status || "disconnected",
|
|
153
|
+
is_enabled: !!record?.is_enabled,
|
|
154
|
+
user_login: config.user_login || null,
|
|
155
|
+
user_name: config.user_name || null,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
deactivate() {
|
|
160
|
+
return { patch: { status: "inactive", is_enabled: false } };
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
actions: {
|
|
164
|
+
async repos(record) {
|
|
165
|
+
const token = safeToken(record);
|
|
166
|
+
const repos = await listRepos(token);
|
|
167
|
+
return { repos: repos.map((r) => ({ full_name: r.full_name, private: r.private, url: r.html_url })) };
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export default githubPlugin;
|
|
@@ -23,6 +23,10 @@ const SKIP_DIRS = new Set([
|
|
|
23
23
|
"node_modules", ".git", "dist", "build", ".next", ".turbo", ".nuxt",
|
|
24
24
|
"coverage", ".cache", ".venv", "venv", "__pycache__", ".pytest_cache",
|
|
25
25
|
".idea", ".vscode", ".DS_Store",
|
|
26
|
+
// Dependency / build caches from other ecosystems — these dominate the tree
|
|
27
|
+
// (a Composer `vendor/` alone can be tens of thousands of files) and would
|
|
28
|
+
// otherwise exhaust the node budget before the project's own files list.
|
|
29
|
+
"vendor", "bower_components", "Pods", ".terraform", ".gradle",
|
|
26
30
|
]);
|
|
27
31
|
|
|
28
32
|
const TEXT_EXTS = new Set([
|
|
@@ -103,20 +107,35 @@ function walk(absDir, relPrefix, budget) {
|
|
|
103
107
|
dirs.sort((a, b) => a.name.localeCompare(b.name));
|
|
104
108
|
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
105
109
|
|
|
110
|
+
// Two passes so a folder's OWN contents are never hidden by a deep subtree:
|
|
111
|
+
// 1) list every direct child (dirs + files) of this level, then
|
|
112
|
+
// 2) descend into the subdirs. Without this, a huge branch (e.g. `vendor/`)
|
|
113
|
+
// consumed the whole node budget depth-first and the sibling files that
|
|
114
|
+
// sort after the dirs — the project's root files — never got listed.
|
|
106
115
|
const out = [];
|
|
116
|
+
const pending = []; // dir nodes to recurse into after the direct listing
|
|
107
117
|
for (const ent of [...dirs, ...files]) {
|
|
108
118
|
if (budget.count >= MAX_NODES) {
|
|
109
119
|
budget.truncated = true;
|
|
110
|
-
|
|
120
|
+
return out;
|
|
111
121
|
}
|
|
112
122
|
budget.count += 1;
|
|
113
123
|
const rel = relPrefix ? `${relPrefix}/${ent.name}` : ent.name;
|
|
114
124
|
if (ent.isDirectory()) {
|
|
115
|
-
|
|
125
|
+
const node = { name: ent.name, path: rel, type: "dir", children: [] };
|
|
126
|
+
out.push(node);
|
|
127
|
+
pending.push({ node, abs: path.join(absDir, ent.name), rel });
|
|
116
128
|
} else {
|
|
117
129
|
out.push({ name: ent.name, path: rel, type: "file", kind: classifyKind(ent.name) });
|
|
118
130
|
}
|
|
119
131
|
}
|
|
132
|
+
for (const { node, abs, rel } of pending) {
|
|
133
|
+
if (budget.count >= MAX_NODES) {
|
|
134
|
+
budget.truncated = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
node.children = walk(abs, rel, budget);
|
|
138
|
+
}
|
|
120
139
|
return out;
|
|
121
140
|
}
|
|
122
141
|
|