@agentprojectcontext/apx 1.58.0 → 1.60.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/skills/index.js +9 -0
- package/src/core/agent/skills/inspector.js +7 -2
- package/src/core/agent/skills/policy.js +128 -0
- package/src/core/agent/super-agent.js +8 -1
- package/src/core/agent/tools/handlers/_asana.js +34 -0
- package/src/core/agent/tools/handlers/asana-create-task.js +38 -0
- package/src/core/agent/tools/handlers/asana-list-projects.js +19 -0
- package/src/core/agent/tools/handlers/asana-list-tasks.js +27 -0
- package/src/core/agent/tools/handlers/asana-update-task.js +32 -0
- package/src/core/agent/tools/handlers/list-skills.js +6 -3
- package/src/core/agent/tools/handlers/load-skill.js +9 -2
- package/src/core/agent/tools/names.js +10 -0
- package/src/core/agent/tools/registry.js +11 -0
- package/src/core/integrations/catalog.js +66 -0
- package/src/core/integrations/index.js +10 -0
- package/src/core/integrations/plugins/asana.js +231 -0
- package/src/core/integrations/sources.js +56 -0
- package/src/core/integrations/store.js +118 -0
- package/src/host/daemon/api/integrations.js +191 -0
- package/src/host/daemon/api/skills.js +301 -17
- package/src/host/daemon/api.js +2 -0
- package/src/interfaces/cli/commands/skills.js +3 -2
- package/src/interfaces/web/dist/assets/index-DFNV6BWh.js +761 -0
- package/src/interfaces/web/dist/assets/{index-DPAuXATr.js.map → index-DFNV6BWh.js.map} +1 -1
- package/src/interfaces/web/dist/assets/index-HU-Wt2l9.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/components/integrations/AsanaPlugin.tsx +275 -0
- package/src/interfaces/web/src/components/integrations/ComingSoonPlugin.tsx +44 -0
- package/src/interfaces/web/src/components/integrations/PluginCard.tsx +61 -0
- package/src/interfaces/web/src/components/integrations/PluginToolsSection.tsx +39 -0
- package/src/interfaces/web/src/components/settings/SkillsManager.tsx +465 -0
- package/src/interfaces/web/src/components/settings/SkillsSettings.tsx +49 -0
- package/src/interfaces/web/src/i18n/en.ts +67 -0
- package/src/interfaces/web/src/i18n/es.ts +67 -0
- package/src/interfaces/web/src/lib/api/integrations.ts +106 -0
- package/src/interfaces/web/src/lib/api/skills.ts +79 -8
- package/src/interfaces/web/src/lib/api.ts +1 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +12 -4
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +3 -3
- package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +146 -0
- package/src/interfaces/web/src/screens/project/SkillsTab.tsx +13 -0
- package/src/interfaces/web/dist/assets/index-Cl0WXtxF.css +0 -1
- package/src/interfaces/web/dist/assets/index-DPAuXATr.js +0 -705
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { http } from "../http";
|
|
2
|
+
|
|
3
|
+
// Where an integration record is stored. "global" targets the default project's
|
|
4
|
+
// store (shared across projects); "project" targets the current project. A
|
|
5
|
+
// project uses its own record when present, otherwise the global one.
|
|
6
|
+
export type IntegrationScope = "project" | "global";
|
|
7
|
+
|
|
8
|
+
// Status returned by a plugin's status endpoint. Common fields plus
|
|
9
|
+
// plugin-specific extras (Asana adds user/workspace metadata).
|
|
10
|
+
export interface IntegrationStatus {
|
|
11
|
+
slug: string;
|
|
12
|
+
status: string;
|
|
13
|
+
is_enabled: boolean;
|
|
14
|
+
user_name?: string | null;
|
|
15
|
+
user_email?: string | null;
|
|
16
|
+
workspace_gid?: string | null;
|
|
17
|
+
workspace_name?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PluginTool {
|
|
21
|
+
slug: string;
|
|
22
|
+
desc: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// One entry of the plugin catalog with its resolved status for this project.
|
|
26
|
+
export interface CatalogEntry {
|
|
27
|
+
slug: string;
|
|
28
|
+
name: string;
|
|
29
|
+
type: string;
|
|
30
|
+
description: string;
|
|
31
|
+
auth: string;
|
|
32
|
+
tools?: PluginTool[];
|
|
33
|
+
coming_soon: boolean;
|
|
34
|
+
status: IntegrationStatus;
|
|
35
|
+
resolved_scope: IntegrationScope | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A stored integration record (secrets redacted; `<key>_set` booleans instead).
|
|
39
|
+
export interface IntegrationRecord {
|
|
40
|
+
slug: string;
|
|
41
|
+
name: string;
|
|
42
|
+
type: string;
|
|
43
|
+
description: string;
|
|
44
|
+
source: string;
|
|
45
|
+
status: string;
|
|
46
|
+
is_enabled: boolean;
|
|
47
|
+
config: Record<string, unknown>;
|
|
48
|
+
created_at: string;
|
|
49
|
+
updated_at: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AsanaConfigureBody {
|
|
53
|
+
personalAccessToken?: string;
|
|
54
|
+
workspaceGid?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AsanaValidateResult {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
user_name?: string | null;
|
|
60
|
+
user_email?: string | null;
|
|
61
|
+
workspace_gid?: string | null;
|
|
62
|
+
workspace_name?: string | null;
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AsanaWorkspaces {
|
|
67
|
+
workspaces: { gid: string; name: string }[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const q = (scope: IntegrationScope) => `?scope=${scope}`;
|
|
71
|
+
|
|
72
|
+
export const Integrations = {
|
|
73
|
+
catalog: (pid: string) =>
|
|
74
|
+
http.get<CatalogEntry[]>(`/projects/${pid}/integrations/catalog`),
|
|
75
|
+
|
|
76
|
+
list: (pid: string, scope: IntegrationScope = "project") =>
|
|
77
|
+
http.get<IntegrationRecord[]>(`/projects/${pid}/integrations${q(scope)}`),
|
|
78
|
+
|
|
79
|
+
status: (pid: string, slug: string, scope: IntegrationScope = "project") =>
|
|
80
|
+
http.get<IntegrationStatus>(`/projects/${pid}/integrations/${slug}${q(scope)}`),
|
|
81
|
+
|
|
82
|
+
configure: (pid: string, slug: string, scope: IntegrationScope, body: Record<string, unknown>) =>
|
|
83
|
+
http.post<IntegrationRecord>(`/projects/${pid}/integrations/${slug}/configure${q(scope)}`, body),
|
|
84
|
+
|
|
85
|
+
validate: (pid: string, slug: string, scope: IntegrationScope = "project") =>
|
|
86
|
+
http.post<AsanaValidateResult>(`/projects/${pid}/integrations/${slug}/validate${q(scope)}`, {}),
|
|
87
|
+
|
|
88
|
+
deactivate: (pid: string, slug: string, scope: IntegrationScope = "project") =>
|
|
89
|
+
http.post<IntegrationStatus>(`/projects/${pid}/integrations/${slug}/deactivate${q(scope)}`, {}),
|
|
90
|
+
|
|
91
|
+
action: <T>(pid: string, slug: string, action: string, scope: IntegrationScope = "project") =>
|
|
92
|
+
http.post<T>(`/projects/${pid}/integrations/${slug}/action/${action}${q(scope)}`, {}),
|
|
93
|
+
|
|
94
|
+
remove: (pid: string, slug: string, scope: IntegrationScope = "project") =>
|
|
95
|
+
http.del<void>(`/projects/${pid}/integrations/${slug}${q(scope)}`),
|
|
96
|
+
|
|
97
|
+
// ── Asana convenience wrappers ──────────────────────────────────────────────
|
|
98
|
+
asanaConfigure: (pid: string, scope: IntegrationScope, body: AsanaConfigureBody) =>
|
|
99
|
+
Integrations.configure(pid, "asana", scope, {
|
|
100
|
+
personal_access_token: body.personalAccessToken,
|
|
101
|
+
workspace_gid: body.workspaceGid,
|
|
102
|
+
}),
|
|
103
|
+
asanaValidate: (pid: string, scope: IntegrationScope) => Integrations.validate(pid, "asana", scope),
|
|
104
|
+
asanaWorkspaces: (pid: string, scope: IntegrationScope) =>
|
|
105
|
+
Integrations.action<AsanaWorkspaces>(pid, "asana", "workspaces", scope),
|
|
106
|
+
};
|
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
import { http } from "../http";
|
|
2
2
|
|
|
3
|
+
export type SkillSource = "builtin" | "global" | "project" | string;
|
|
4
|
+
|
|
3
5
|
export type SkillEntry = {
|
|
4
6
|
slug: string;
|
|
5
|
-
source:
|
|
7
|
+
source: SkillSource;
|
|
6
8
|
description: string;
|
|
9
|
+
/** Effective enabled state for the requested scope. */
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
/** Built-in APX skill — always active, never disableable. */
|
|
12
|
+
private?: boolean;
|
|
13
|
+
/** Whether THIS scope holds an explicit override (vs. inherited). */
|
|
14
|
+
overridden?: boolean;
|
|
7
15
|
};
|
|
8
16
|
|
|
9
17
|
export type SkillsList = {
|
|
10
18
|
count: number;
|
|
19
|
+
/** Echoed scope key: "default" (super-agent) or a project path. */
|
|
20
|
+
scope?: string;
|
|
11
21
|
skills: SkillEntry[];
|
|
12
22
|
};
|
|
13
23
|
|
|
24
|
+
export interface SkillDetail {
|
|
25
|
+
slug: string;
|
|
26
|
+
source: SkillSource;
|
|
27
|
+
description: string;
|
|
28
|
+
frontmatter: Record<string, string>;
|
|
29
|
+
body: string;
|
|
30
|
+
file: string;
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
private: boolean;
|
|
33
|
+
overridden: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CreateResult {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
slug: string;
|
|
39
|
+
source: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
export interface InspectorConfig {
|
|
15
43
|
enabled: boolean;
|
|
16
44
|
load_threshold: number;
|
|
@@ -62,16 +90,59 @@ export interface InspectResult {
|
|
|
62
90
|
|
|
63
91
|
export const Skills = {
|
|
64
92
|
/**
|
|
65
|
-
* List installed skills (
|
|
66
|
-
*
|
|
93
|
+
* List installed skills (built-in + user + optional project-scoped), each
|
|
94
|
+
* annotated with `enabled`/`private` for the requested scope. Pass a project
|
|
95
|
+
* path to both scan that project's skills AND resolve enabled-state against
|
|
96
|
+
* it; omit it for the super-agent ("default") scope.
|
|
67
97
|
*/
|
|
68
|
-
list: (projectPath?: string) =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
list: (projectPath?: string) => {
|
|
99
|
+
const qs = new URLSearchParams();
|
|
100
|
+
if (projectPath) {
|
|
101
|
+
qs.set("project_path", projectPath);
|
|
102
|
+
qs.set("scope", projectPath);
|
|
103
|
+
}
|
|
104
|
+
const q = qs.toString();
|
|
105
|
+
return http.get<SkillsList>(q ? `/skills?${q}` : "/skills");
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/** Full body + frontmatter of a skill, for the viewer. */
|
|
109
|
+
detail: (slug: string, projectPath?: string) => {
|
|
110
|
+
const qs = projectPath ? `?project_path=${encodeURIComponent(projectPath)}` : "";
|
|
111
|
+
return http.get<SkillDetail>(`/skills/${encodeURIComponent(slug)}/detail${qs}`);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Enable/disable a skill for a scope. `enabled: null` clears the override so
|
|
116
|
+
* the skill inherits the super-agent default again. `scope` is "default" or a
|
|
117
|
+
* project path.
|
|
118
|
+
*/
|
|
119
|
+
setEnabled: (body: { slug: string; enabled: boolean | null; scope?: string }) =>
|
|
120
|
+
http.put<{ ok: boolean; slug: string; scope: string; enabled: boolean | null }>(
|
|
121
|
+
"/skills/enabled",
|
|
122
|
+
body,
|
|
73
123
|
),
|
|
74
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Create a user skill from the online editor. With `project_path` it lands in
|
|
127
|
+
* that project's .apc/skills/; otherwise in ~/.apx/skills/.
|
|
128
|
+
*/
|
|
129
|
+
create: (body: { slug: string; description?: string; body?: string; project_path?: string }) =>
|
|
130
|
+
http.post<CreateResult>("/skills", body),
|
|
131
|
+
|
|
132
|
+
/** Import a skill from an uploaded .zip (base64-encoded). */
|
|
133
|
+
importZip: (body: { data: string; project_path?: string }) =>
|
|
134
|
+
http.post<CreateResult>("/skills/import/zip", body),
|
|
135
|
+
|
|
136
|
+
/** Import a skill by cloning a git repo. */
|
|
137
|
+
importRepo: (body: { url: string; project_path?: string }) =>
|
|
138
|
+
http.post<CreateResult>("/skills/import/repo", body),
|
|
139
|
+
|
|
140
|
+
/** Delete a user skill (global or project-scoped). */
|
|
141
|
+
remove: (slug: string, projectPath?: string) => {
|
|
142
|
+
const qs = projectPath ? `?project_path=${encodeURIComponent(projectPath)}` : "";
|
|
143
|
+
return http.del<{ ok: boolean; slug: string }>(`/skills/${encodeURIComponent(slug)}${qs}`);
|
|
144
|
+
},
|
|
145
|
+
|
|
75
146
|
/** Skill Inspector config + index status. */
|
|
76
147
|
inspector: () => http.get<InspectorState>("/skills/inspector"),
|
|
77
148
|
|
|
@@ -9,6 +9,7 @@ export * from "./api/conversations";
|
|
|
9
9
|
export * from "./api/routines";
|
|
10
10
|
export * from "./api/tasks";
|
|
11
11
|
export * from "./api/mcps";
|
|
12
|
+
export * from "./api/integrations";
|
|
12
13
|
export * from "./api/vars";
|
|
13
14
|
export * from "./api/messages";
|
|
14
15
|
export * from "./api/sessions";
|
|
@@ -3,8 +3,8 @@ import { useParams, Routes, Route, Navigate, useLocation, useNavigate } from "re
|
|
|
3
3
|
import {
|
|
4
4
|
Bot, Heart, Zap, Puzzle, FolderKanban, Settings,
|
|
5
5
|
MessagesSquare, Send, KeyRound,
|
|
6
|
-
LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain, FileCode2,
|
|
7
|
-
Building2, FileText, FolderTree,
|
|
6
|
+
LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain, FileCode2, Cable,
|
|
7
|
+
Building2, FileText, FolderTree, Sparkles,
|
|
8
8
|
} from "lucide-react";
|
|
9
9
|
import { useNavCollapse, type TabSection } from "../components/common/TabNav";
|
|
10
10
|
import { TabLayout } from "../components/common/TabLayout";
|
|
@@ -25,6 +25,7 @@ import { AgentsTab } from "./project/AgentsTab";
|
|
|
25
25
|
import { RoutinesTab } from "./project/RoutinesTab";
|
|
26
26
|
import { TasksTab } from "./project/TasksTab";
|
|
27
27
|
import { McpsTab } from "./project/McpsTab";
|
|
28
|
+
import { IntegrationsTab } from "./project/IntegrationsTab";
|
|
28
29
|
import { VarsTab } from "./project/VarsTab";
|
|
29
30
|
import { ChatTab } from "./project/ChatTab";
|
|
30
31
|
import { TelegramTab } from "./project/TelegramTab";
|
|
@@ -34,11 +35,12 @@ import { AgentDetailScreen } from "./project/AgentDetailScreen";
|
|
|
34
35
|
import { StructureTab } from "./project/StructureTab";
|
|
35
36
|
import { DocsTab } from "./project/DocsTab";
|
|
36
37
|
import { FilesTab } from "./project/FilesTab";
|
|
38
|
+
import { SkillsTab } from "./project/SkillsTab";
|
|
37
39
|
|
|
38
40
|
type NavKey =
|
|
39
41
|
| "" | "chat" | "config" | "telegram"
|
|
40
|
-
| "agents" | "routines" | "tasks" | "mcps" | "vars" | "logs" | "memories" | "artifacts"
|
|
41
|
-
| "structure" | "docs" | "files";
|
|
42
|
+
| "agents" | "routines" | "tasks" | "mcps" | "integrations" | "vars" | "logs" | "memories" | "artifacts"
|
|
43
|
+
| "structure" | "docs" | "files" | "skills";
|
|
42
44
|
|
|
43
45
|
export function ProjectScreen() {
|
|
44
46
|
const navigate = useNavigate();
|
|
@@ -75,8 +77,10 @@ export function ProjectScreen() {
|
|
|
75
77
|
items: [
|
|
76
78
|
{ key: "agents", label: t("project.nav.agents"), icon: Bot },
|
|
77
79
|
{ key: "memories", label: t("project.nav.memories"), icon: Brain },
|
|
80
|
+
{ key: "skills", label: t("skills_page.title"), icon: Sparkles },
|
|
78
81
|
{ key: "routines", label: t("project.nav.routines"), icon: Heart },
|
|
79
82
|
{ key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
|
|
83
|
+
{ key: "integrations", label: "Integrations", icon: Cable },
|
|
80
84
|
{ key: "vars", label: t("project.nav.vars"), icon: KeyRound },
|
|
81
85
|
{ key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
|
|
82
86
|
{ key: "config", label: t("project.nav.config"), icon: Settings },
|
|
@@ -97,6 +101,7 @@ export function ProjectScreen() {
|
|
|
97
101
|
{ key: "agents", label: t("project.nav.agents"), icon: Bot },
|
|
98
102
|
...(isCompany ? [{ key: "structure", label: t("project.nav.structure"), icon: Building2 }] : []),
|
|
99
103
|
{ key: "memories", label: t("project.nav.memories"), icon: Brain },
|
|
104
|
+
{ key: "skills", label: t("skills_page.title"), icon: Sparkles },
|
|
100
105
|
],
|
|
101
106
|
},
|
|
102
107
|
{
|
|
@@ -112,6 +117,7 @@ export function ProjectScreen() {
|
|
|
112
117
|
{ key: "routines", label: t("project.nav.routines"), icon: Heart },
|
|
113
118
|
{ key: "tasks", label: t("project.nav.tasks"), icon: Zap },
|
|
114
119
|
{ key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
|
|
120
|
+
{ key: "integrations", label: "Integrations", icon: Cable },
|
|
115
121
|
{ key: "vars", label: t("project.nav.vars"), icon: KeyRound },
|
|
116
122
|
{ key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
|
|
117
123
|
{ key: "logs", label: t("project.nav.logs"), icon: ScrollText },
|
|
@@ -175,9 +181,11 @@ export function ProjectScreen() {
|
|
|
175
181
|
<Route path="docs" element={<DocsTab pid={pid} />} />
|
|
176
182
|
<Route path="files" element={<FilesTab pid={pid} />} />
|
|
177
183
|
<Route path="memories" element={<MemoriesTab pid={pid} />} />
|
|
184
|
+
<Route path="skills" element={<SkillsTab pid={pid} />} />
|
|
178
185
|
<Route path="routines" element={<RoutinesTab pid={pid} />} />
|
|
179
186
|
<Route path="tasks" element={isBase ? <GlobalTasksTab /> : <TasksTab pid={pid} />} />
|
|
180
187
|
<Route path="mcps" element={<McpsTab pid={pid} />} />
|
|
188
|
+
<Route path="integrations" element={<IntegrationsTab pid={pid} />} />
|
|
181
189
|
<Route path="artifacts" element={<ArtifactsTab pid={pid} />} />
|
|
182
190
|
<Route path="vars" element={<VarsTab pid={pid} />} />
|
|
183
191
|
<Route path="threads" element={<Navigate to={`/p/${pid}/chat`} replace />} />
|
|
@@ -8,7 +8,7 @@ import { TabLayout } from "../components/common/TabLayout";
|
|
|
8
8
|
import { IdentityPanel } from "../components/settings/IdentityPanel";
|
|
9
9
|
import { SuperAgentPanel } from "../components/settings/SuperAgentPanel";
|
|
10
10
|
import { MemoryPanel } from "../components/settings/MemoryPanel";
|
|
11
|
-
import {
|
|
11
|
+
import { SkillsSettings } from "../components/settings/SkillsSettings";
|
|
12
12
|
import { ModelsTab } from "./base/ModelsTab";
|
|
13
13
|
import { TelegramSettingsTabs } from "../components/settings/TelegramSettingsTabs";
|
|
14
14
|
import { DevicesPanel } from "../components/settings/DevicesPanel";
|
|
@@ -37,7 +37,7 @@ const SECTIONS: TabSection[] = [
|
|
|
37
37
|
{ key: "super_agent", label: t("settings.tabs.super_agent"), icon: Bot },
|
|
38
38
|
{ key: "engines", label: t("settings.tabs.engines"), icon: Cpu },
|
|
39
39
|
{ key: "memory", label: "Memory (RAG)", icon: Database },
|
|
40
|
-
{ key: "skills", label: "
|
|
40
|
+
{ key: "skills", label: t("skills_page.title"), icon: Sparkles },
|
|
41
41
|
],
|
|
42
42
|
},
|
|
43
43
|
{
|
|
@@ -75,7 +75,7 @@ const PANELS: Record<TabKey, () => ReactElement> = {
|
|
|
75
75
|
super_agent: () => <SuperAgentPanel />,
|
|
76
76
|
engines: () => <ModelsTab />,
|
|
77
77
|
memory: () => <MemoryPanel />,
|
|
78
|
-
skills: () => <
|
|
78
|
+
skills: () => <SkillsSettings />,
|
|
79
79
|
telegram: () => <TelegramSettingsTabs />,
|
|
80
80
|
devices: () => <DevicesPanel />,
|
|
81
81
|
voice: () => <VoiceScreen />,
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import { Network, Puzzle, Wrench } from "lucide-react";
|
|
4
|
+
import { Integrations, type CatalogEntry, type IntegrationScope } from "../../lib/api";
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
import { Section } from "../../components/Section";
|
|
7
|
+
import { Empty, Loading } from "../../components/ui";
|
|
8
|
+
import { AsanaPlugin } from "../../components/integrations/AsanaPlugin";
|
|
9
|
+
import { ComingSoonPlugin } from "../../components/integrations/ComingSoonPlugin";
|
|
10
|
+
import { McpsTab } from "./McpsTab";
|
|
11
|
+
|
|
12
|
+
type SubTab = "plugins" | "mcp" | "tools";
|
|
13
|
+
|
|
14
|
+
const SUBTABS: { value: SubTab; label: string; icon: typeof Puzzle }[] = [
|
|
15
|
+
{ value: "plugins", label: "Plugins", icon: Puzzle },
|
|
16
|
+
{ value: "mcp", label: "MCP Servers", icon: Network },
|
|
17
|
+
{ value: "tools", label: "Tools", icon: Wrench },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Renders the live plugin (Asana) or a coming-soon placeholder per catalog entry.
|
|
21
|
+
function PluginRow({ pid, scope, entry }: { pid: string; scope: IntegrationScope; entry: CatalogEntry }) {
|
|
22
|
+
if (entry.coming_soon) return <ComingSoonPlugin entry={entry} />;
|
|
23
|
+
if (entry.slug === "asana") return <AsanaPlugin pid={pid} scope={scope} />;
|
|
24
|
+
// Implemented plugin without a bespoke UI yet — fall back to the placeholder.
|
|
25
|
+
return <ComingSoonPlugin entry={entry} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function PluginsSection({ pid, scope }: { pid: string; scope: IntegrationScope }) {
|
|
29
|
+
const { data: catalog, isLoading } = useSWR(`integrations-catalog-${pid}`, () => Integrations.catalog(pid));
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-3">
|
|
33
|
+
<p className="text-xs text-muted-foreground">
|
|
34
|
+
Plugins de canal y servicio instalables por proyecto. Se guardan en el ámbito
|
|
35
|
+
seleccionado arriba.
|
|
36
|
+
</p>
|
|
37
|
+
{isLoading && <Loading />}
|
|
38
|
+
{(catalog || []).map((entry) => (
|
|
39
|
+
<PluginRow key={entry.slug} pid={pid} scope={scope} entry={entry} />
|
|
40
|
+
))}
|
|
41
|
+
<div className="rounded-xl border border-dashed border-border p-6 text-center">
|
|
42
|
+
<p className="text-sm text-muted-foreground">Más plugins próximamente…</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ToolsSection({ pid }: { pid: string }) {
|
|
49
|
+
const { data: catalog } = useSWR(`integrations-catalog-${pid}`, () => Integrations.catalog(pid));
|
|
50
|
+
const rows = (catalog || [])
|
|
51
|
+
.filter((c) => !c.coming_soon && (c.tools?.length ?? 0) > 0)
|
|
52
|
+
.flatMap((c) => (c.tools || []).map((t) => ({ ...t, plugin: c.name, active: c.status.is_enabled })));
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="space-y-3">
|
|
56
|
+
<p className="text-xs text-muted-foreground">
|
|
57
|
+
Tools que los plugins conectados exponen a los agentes de este proyecto.
|
|
58
|
+
</p>
|
|
59
|
+
{rows.length === 0 ? (
|
|
60
|
+
<Empty>No hay tools de integraciones. Conectá un plugin para habilitarlas.</Empty>
|
|
61
|
+
) : (
|
|
62
|
+
<ul className="space-y-2">
|
|
63
|
+
{rows.map((t) => (
|
|
64
|
+
<li key={t.slug} className={cn("rounded-md border border-border bg-muted/30 px-3 py-2", !t.active && "opacity-55")}>
|
|
65
|
+
<div className="flex items-center gap-2">
|
|
66
|
+
<Wrench className="h-3.5 w-3.5 text-muted-foreground" />
|
|
67
|
+
<span className="font-mono text-xs text-foreground">{t.slug}</span>
|
|
68
|
+
<span className="ml-auto text-[10px] text-muted-foreground">{t.plugin}</span>
|
|
69
|
+
<span
|
|
70
|
+
className={cn(
|
|
71
|
+
"rounded border px-1.5 py-0.5 text-[10px]",
|
|
72
|
+
t.active
|
|
73
|
+
? "border-emerald-700/40 bg-emerald-900/20 text-emerald-400"
|
|
74
|
+
: "border-border bg-muted text-muted-foreground",
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{t.active ? "activo" : "inactivo"}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
<p className="mt-0.5 pl-5 text-[10px] text-muted-foreground">{t.desc}</p>
|
|
81
|
+
</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function IntegrationsTab({ pid }: { pid: string }) {
|
|
90
|
+
const isBase = String(pid) === "0";
|
|
91
|
+
const [tab, setTab] = useState<SubTab>("plugins");
|
|
92
|
+
// On the base project, project scope IS the global (default) store.
|
|
93
|
+
const [scope, setScope] = useState<IntegrationScope>(isBase ? "global" : "project");
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Section
|
|
97
|
+
title="Integrations"
|
|
98
|
+
description="Plugins, MCP servers y tools disponibles para este proyecto"
|
|
99
|
+
>
|
|
100
|
+
{/* Scope selector — a real project can use its own integrations or the
|
|
101
|
+
global (default-project) ones. */}
|
|
102
|
+
{!isBase && (
|
|
103
|
+
<div className="mb-4 flex items-center gap-2">
|
|
104
|
+
<span className="text-xs text-muted-foreground">Ámbito:</span>
|
|
105
|
+
{(["project", "global"] as const).map((s) => (
|
|
106
|
+
<button
|
|
107
|
+
key={s}
|
|
108
|
+
onClick={() => setScope(s)}
|
|
109
|
+
className={cn(
|
|
110
|
+
"rounded-md border px-2.5 py-1 text-xs transition-colors",
|
|
111
|
+
scope === s
|
|
112
|
+
? "border-primary/50 bg-primary/10 text-foreground"
|
|
113
|
+
: "border-border bg-muted/30 text-muted-foreground hover:bg-muted/50",
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
{s === "project" ? "Este proyecto" : "Global (default)"}
|
|
117
|
+
</button>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{/* Sub-tabs */}
|
|
123
|
+
<div className="mb-4 inline-flex rounded-lg border border-border bg-muted/30 p-0.5">
|
|
124
|
+
{SUBTABS.map((s) => {
|
|
125
|
+
const Icon = s.icon;
|
|
126
|
+
return (
|
|
127
|
+
<button
|
|
128
|
+
key={s.value}
|
|
129
|
+
onClick={() => setTab(s.value)}
|
|
130
|
+
className={cn(
|
|
131
|
+
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs transition-colors",
|
|
132
|
+
tab === s.value ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
<Icon className="h-3.5 w-3.5" /> {s.label}
|
|
136
|
+
</button>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{tab === "plugins" && <PluginsSection pid={pid} scope={scope} />}
|
|
142
|
+
{tab === "mcp" && <McpsTab pid={pid} />}
|
|
143
|
+
{tab === "tools" && <ToolsSection pid={pid} />}
|
|
144
|
+
</Section>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useProject } from "../../hooks/useProjects";
|
|
2
|
+
import { Loading } from "../../components/ui";
|
|
3
|
+
import { SkillsManager } from "../../components/settings/SkillsManager";
|
|
4
|
+
|
|
5
|
+
// Per-project skills view: the Claude-Desktop-style manager locked to THIS
|
|
6
|
+
// project's scope so you can enable/disable and add skills while working in it.
|
|
7
|
+
// The base project (pid "0" = super-agent admin) manages the "default" scope.
|
|
8
|
+
export function SkillsTab({ pid }: { pid: string }) {
|
|
9
|
+
const { project } = useProject(pid);
|
|
10
|
+
const scope = pid === "0" ? "default" : project?.path;
|
|
11
|
+
if (!scope) return <Loading />;
|
|
12
|
+
return <SkillsManager scope={scope} />;
|
|
13
|
+
}
|