@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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/core/agent/skills/index.js +9 -0
  3. package/src/core/agent/skills/inspector.js +7 -2
  4. package/src/core/agent/skills/policy.js +128 -0
  5. package/src/core/agent/super-agent.js +8 -1
  6. package/src/core/agent/tools/handlers/_asana.js +34 -0
  7. package/src/core/agent/tools/handlers/asana-create-task.js +38 -0
  8. package/src/core/agent/tools/handlers/asana-list-projects.js +19 -0
  9. package/src/core/agent/tools/handlers/asana-list-tasks.js +27 -0
  10. package/src/core/agent/tools/handlers/asana-update-task.js +32 -0
  11. package/src/core/agent/tools/handlers/list-skills.js +6 -3
  12. package/src/core/agent/tools/handlers/load-skill.js +9 -2
  13. package/src/core/agent/tools/names.js +10 -0
  14. package/src/core/agent/tools/registry.js +11 -0
  15. package/src/core/integrations/catalog.js +66 -0
  16. package/src/core/integrations/index.js +10 -0
  17. package/src/core/integrations/plugins/asana.js +231 -0
  18. package/src/core/integrations/sources.js +56 -0
  19. package/src/core/integrations/store.js +118 -0
  20. package/src/host/daemon/api/integrations.js +191 -0
  21. package/src/host/daemon/api/skills.js +301 -17
  22. package/src/host/daemon/api.js +2 -0
  23. package/src/interfaces/cli/commands/skills.js +3 -2
  24. package/src/interfaces/web/dist/assets/index-DFNV6BWh.js +761 -0
  25. package/src/interfaces/web/dist/assets/{index-DPAuXATr.js.map → index-DFNV6BWh.js.map} +1 -1
  26. package/src/interfaces/web/dist/assets/index-HU-Wt2l9.css +1 -0
  27. package/src/interfaces/web/dist/index.html +2 -2
  28. package/src/interfaces/web/src/components/integrations/AsanaPlugin.tsx +275 -0
  29. package/src/interfaces/web/src/components/integrations/ComingSoonPlugin.tsx +44 -0
  30. package/src/interfaces/web/src/components/integrations/PluginCard.tsx +61 -0
  31. package/src/interfaces/web/src/components/integrations/PluginToolsSection.tsx +39 -0
  32. package/src/interfaces/web/src/components/settings/SkillsManager.tsx +465 -0
  33. package/src/interfaces/web/src/components/settings/SkillsSettings.tsx +49 -0
  34. package/src/interfaces/web/src/i18n/en.ts +67 -0
  35. package/src/interfaces/web/src/i18n/es.ts +67 -0
  36. package/src/interfaces/web/src/lib/api/integrations.ts +106 -0
  37. package/src/interfaces/web/src/lib/api/skills.ts +79 -8
  38. package/src/interfaces/web/src/lib/api.ts +1 -0
  39. package/src/interfaces/web/src/screens/ProjectScreen.tsx +12 -4
  40. package/src/interfaces/web/src/screens/SettingsScreen.tsx +3 -3
  41. package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +146 -0
  42. package/src/interfaces/web/src/screens/project/SkillsTab.tsx +13 -0
  43. package/src/interfaces/web/dist/assets/index-Cl0WXtxF.css +0 -1
  44. package/src/interfaces/web/dist/assets/index-DPAuXATr.js +0 -705
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.58.0",
3
+ "version": "1.60.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -3,6 +3,15 @@ export { condenseSkillDescription, buildSkillsHintBlock } from "./catalog.js";
3
3
  export { tryResolveSkillCommand } from "./trigger.js";
4
4
  export { suggestSkillForPrompt, clearSkillVectorCache } from "./rag.js";
5
5
  export { listSkills, loadSkill, SKILL_LOCATIONS } from "./loader.js";
6
+ export {
7
+ isPrivateSkill,
8
+ isSkillEnabled,
9
+ filterEnabledSkills,
10
+ annotateSkills,
11
+ setSkillEnabled,
12
+ resolveScopeKey,
13
+ DEFAULT_SCOPE,
14
+ } from "./policy.js";
6
15
  export {
7
16
  inspectPromptForSkills,
8
17
  isInspectorEnabled,
@@ -25,6 +25,7 @@
25
25
 
26
26
  import { embedOne, cosineSim } from "#core/memory/embeddings.js";
27
27
  import { listSkills, loadSkill } from "./loader.js";
28
+ import { filterEnabledSkills, isSkillEnabled } from "./policy.js";
28
29
  import { readIndex, backgroundRefreshIfStale } from "./index-store.js";
29
30
 
30
31
  // Defaults — exported so the CLI/web can render them.
@@ -187,7 +188,8 @@ export async function inspectPromptForSkills({ prompt, projectPath, globalConfig
187
188
  };
188
189
  }
189
190
 
190
- const scored = scoreAgainstIndex(probe.vector, items);
191
+ const scored = scoreAgainstIndex(probe.vector, items).filter((s) =>
192
+ isSkillEnabled(s, { config: globalConfig, projectPath }));
191
193
  return await pickAndRender({ scored, projectPath, probe, cfg });
192
194
  }
193
195
 
@@ -196,7 +198,10 @@ export async function inspectPromptForSkills({ prompt, projectPath, globalConfig
196
198
  // ---------------------------------------------------------------------------
197
199
 
198
200
  async function inspectFromLive({ text, projectPath, cfg, globalConfig, embedOpts }) {
199
- const skills = listSkills({ projectPath });
201
+ const skills = filterEnabledSkills(listSkills({ projectPath }), {
202
+ config: globalConfig,
203
+ projectPath,
204
+ });
200
205
  if (!skills.length) {
201
206
  return { contextNote: "", trace: { enabled: true, reason: "no_skills" } };
202
207
  }
@@ -0,0 +1,128 @@
1
+ // Skills enable/disable policy — scope-aware gating shared by every consumer.
2
+ //
3
+ // A skill can be turned off per scope. A "scope" is either the super-agent /
4
+ // no-project baseline ("default") or a specific project (keyed by its absolute
5
+ // path). Config lives at:
6
+ //
7
+ // config.skills.policy["default"] = { "<slug>": true|false, ... }
8
+ // config.skills.policy["<projectPath>"] = { "<slug>": true|false, ... }
9
+ //
10
+ // A boolean under a scope is an explicit override; an absent slug inherits.
11
+ // Empty/missing policy = every skill enabled (the pre-feature behavior), so this
12
+ // is fully backward compatible.
13
+ //
14
+ // PRIVATE skills (source "builtin") are APX's own shipped skills. They are
15
+ // always active and can never be disabled or deleted — the UI shows them locked.
16
+ //
17
+ // Effective enabled(skill, projectPath):
18
+ // 1. builtin source → true (private, locked)
19
+ // 2. project scope explicit value → that value
20
+ // 3. "default" scope explicit value→ that value
21
+ // 4. otherwise → true
22
+
23
+ export const DEFAULT_SCOPE = "default";
24
+
25
+ // Sources whose skills ship with apx and are always active.
26
+ const PRIVATE_SOURCES = new Set(["builtin"]);
27
+
28
+ /** True for APX's own built-in skills — always active, never disableable. */
29
+ export function isPrivateSkill(skill) {
30
+ return PRIVATE_SOURCES.has(skill?.source);
31
+ }
32
+
33
+ /** Normalize a project path (or nothing) into a policy scope key. */
34
+ export function resolveScopeKey(projectPath) {
35
+ const p = typeof projectPath === "string" ? projectPath.trim() : "";
36
+ return p || DEFAULT_SCOPE;
37
+ }
38
+
39
+ function policyMap(config) {
40
+ const p = config?.skills?.policy;
41
+ return p && typeof p === "object" ? p : {};
42
+ }
43
+
44
+ function scopeOverride(config, scopeKey, slug) {
45
+ const scope = policyMap(config)[scopeKey];
46
+ if (!scope || typeof scope !== "object") return undefined;
47
+ const v = scope[slug];
48
+ return typeof v === "boolean" ? v : undefined;
49
+ }
50
+
51
+ /**
52
+ * Resolve whether a skill is enabled for the given scope.
53
+ * @param {{slug:string, source?:string}} skill
54
+ * @param {{config?:object, projectPath?:string}} ctx
55
+ * @returns {boolean}
56
+ */
57
+ export function isSkillEnabled(skill, { config, projectPath } = {}) {
58
+ if (isPrivateSkill(skill)) return true;
59
+ const slug = skill?.slug;
60
+ if (!slug) return true;
61
+
62
+ const scopeKey = resolveScopeKey(projectPath);
63
+ if (scopeKey !== DEFAULT_SCOPE) {
64
+ const own = scopeOverride(config, scopeKey, slug);
65
+ if (own !== undefined) return own;
66
+ }
67
+ const base = scopeOverride(config, DEFAULT_SCOPE, slug);
68
+ if (base !== undefined) return base;
69
+ return true;
70
+ }
71
+
72
+ /** Keep only the skills enabled for the given scope. */
73
+ export function filterEnabledSkills(skills, ctx = {}) {
74
+ if (!Array.isArray(skills)) return [];
75
+ return skills.filter((s) => isSkillEnabled(s, ctx));
76
+ }
77
+
78
+ /**
79
+ * Annotate skills for a UI client: adds `enabled`, `private`, and `overridden`
80
+ * (whether *this* scope holds an explicit override, ignoring inheritance).
81
+ */
82
+ export function annotateSkills(skills, { config, projectPath } = {}) {
83
+ if (!Array.isArray(skills)) return [];
84
+ const scopeKey = resolveScopeKey(projectPath);
85
+ return skills.map((s) => {
86
+ const priv = isPrivateSkill(s);
87
+ const own = priv ? undefined : scopeOverride(config, scopeKey, s.slug);
88
+ return {
89
+ ...s,
90
+ private: priv,
91
+ enabled: isSkillEnabled(s, { config, projectPath }),
92
+ overridden: own !== undefined,
93
+ };
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Set (or clear) a skill's enabled override for a scope. Mutates and returns the
99
+ * config object. `enabled === null|undefined` clears the override (back to
100
+ * inherit). Private/builtin skills cannot be overridden.
101
+ *
102
+ * @param {object} config the global config (mutated in place)
103
+ * @param {object} args
104
+ * @param {string} args.slug
105
+ * @param {boolean|null} args.enabled
106
+ * @param {string=} args.scope scope key ("default" or a project path)
107
+ * @param {string=} args.projectPath alternative to scope; normalized to a key
108
+ */
109
+ export function setSkillEnabled(config, { slug, enabled, scope, projectPath } = {}) {
110
+ if (!slug) throw new Error("setSkillEnabled: slug required");
111
+ const scopeKey = scope ? resolveScopeKey(scope) : resolveScopeKey(projectPath);
112
+
113
+ config.skills = config.skills || {};
114
+ config.skills.policy = config.skills.policy || {};
115
+ const map = config.skills.policy;
116
+
117
+ if (enabled === null || enabled === undefined) {
118
+ if (map[scopeKey]) {
119
+ delete map[scopeKey][slug];
120
+ if (Object.keys(map[scopeKey]).length === 0) delete map[scopeKey];
121
+ }
122
+ return config;
123
+ }
124
+
125
+ map[scopeKey] = map[scopeKey] || {};
126
+ map[scopeKey][slug] = !!enabled;
127
+ return config;
128
+ }
@@ -1,6 +1,7 @@
1
1
  // Super-agent: daemon-level action agent for Telegram, TUI, desktop, routines.
2
2
  import { createToolSession, buildLazyToolsBlock, makeToolHandlers } from "#core/agent/tools/registry.js";
3
3
  import { listSkills } from "#core/agent/skills/loader.js";
4
+ import { filterEnabledSkills } from "#core/agent/skills/policy.js";
4
5
  import {
5
6
  runAgent,
6
7
  buildSuperAgentSystem,
@@ -97,10 +98,16 @@ export async function runSuperAgent({
97
98
  // noTools callers (summarize/ask) get no session — text only.
98
99
  const toolSession = noTools ? null : createToolSession(channel, { allowedTools });
99
100
 
101
+ // Scope the catalog hint to the skills enabled for this project (or the
102
+ // super-agent baseline when no project). Built-in/private skills always pass.
103
+ const projectPath = channelMeta?.projectPath;
104
+ const scopedListSkills = (opts = {}) =>
105
+ filterEnabledSkills(listSkills(opts), { config: globalConfig, projectPath });
106
+
100
107
  const system = buildSuperAgentSystem({
101
108
  globalConfig,
102
109
  projects,
103
- listSkills,
110
+ listSkills: scopedListSkills,
104
111
  contextNote,
105
112
  channel,
106
113
  channelMeta,
@@ -0,0 +1,34 @@
1
+ // Shared helpers for the Asana agent tools (asana-*.js). Kept in an underscore
2
+ // file — like _git.js — so it holds no tool `name:` of its own. Each tool file
3
+ // stays a thin adapter: resolve the project's effective Asana integration (its
4
+ // own record wins over the default project's — see resolveIntegration), then
5
+ // call the pure REST client in core/integrations/plugins/asana.js.
6
+ import { resolveProject } from "../helpers.js";
7
+ import { resolveIntegration } from "#core/integrations/index.js";
8
+
9
+ // Resolve the active Asana token + config for a project, or throw a message the
10
+ // model can act on (tell the user to connect Asana in the web panel).
11
+ export function resolveAsana(projects, project) {
12
+ const p = resolveProject(projects, project);
13
+ const resolved = resolveIntegration({ projectStorage: p.storagePath, slug: "asana" });
14
+ if (!resolved) {
15
+ throw new Error(
16
+ "Asana is not connected for this project. Ask the user to connect it in the web panel → Integrations → Plugins → Asana.",
17
+ );
18
+ }
19
+ const config = resolved.record.config || {};
20
+ const token = config.personal_access_token;
21
+ if (!token) throw new Error("Asana integration has no token configured");
22
+ return { token, config, scope: resolved.scope };
23
+ }
24
+
25
+ export function requireWorkspace(config) {
26
+ const gid = config.workspace_gid;
27
+ if (!gid) throw new Error("No Asana workspace selected. Configure it in the web panel first.");
28
+ return gid;
29
+ }
30
+
31
+ // The optional `project` arg every Asana tool accepts (defaults to current).
32
+ export const PROJECT_ARG = {
33
+ project: { type: "string", description: "APX project id/name (optional; defaults to current)" },
34
+ };
@@ -0,0 +1,38 @@
1
+ import * as asana from "#core/integrations/plugins/asana.js";
2
+ import { resolveAsana, requireWorkspace, PROJECT_ARG } from "./_asana.js";
3
+
4
+ export default {
5
+ name: "asana_create_task",
6
+ category: "integrations",
7
+ schema: {
8
+ type: "function",
9
+ function: {
10
+ name: "asana_create_task",
11
+ description: "Create an Asana task in the connected workspace.",
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ name: { type: "string", description: "Task title" },
16
+ notes: { type: "string", description: "Task description/notes" },
17
+ project_gid: { type: "string", description: "Optional Asana project gid to add the task to" },
18
+ assignee: { type: "string", description: "Optional assignee (user gid or 'me')" },
19
+ due_on: { type: "string", description: "Optional due date (YYYY-MM-DD)" },
20
+ ...PROJECT_ARG,
21
+ },
22
+ required: ["name"],
23
+ },
24
+ },
25
+ },
26
+ makeHandler: ({ projects }) => async ({ project, name, notes, project_gid, assignee, due_on } = {}) => {
27
+ const { token, config } = resolveAsana(projects, project);
28
+ const task = await asana.createTask(token, {
29
+ workspaceGid: requireWorkspace(config),
30
+ name,
31
+ notes,
32
+ projectGid: project_gid,
33
+ assignee,
34
+ dueOn: due_on,
35
+ });
36
+ return { task };
37
+ },
38
+ };
@@ -0,0 +1,19 @@
1
+ import * as asana from "#core/integrations/plugins/asana.js";
2
+ import { resolveAsana, requireWorkspace, PROJECT_ARG } from "./_asana.js";
3
+
4
+ export default {
5
+ name: "asana_list_projects",
6
+ category: "integrations",
7
+ schema: {
8
+ type: "function",
9
+ function: {
10
+ name: "asana_list_projects",
11
+ description: "List Asana projects in the connected workspace.",
12
+ parameters: { type: "object", properties: { ...PROJECT_ARG } },
13
+ },
14
+ },
15
+ makeHandler: ({ projects }) => async ({ project } = {}) => {
16
+ const { token, config } = resolveAsana(projects, project);
17
+ return { projects: await asana.listProjects(token, requireWorkspace(config)) };
18
+ },
19
+ };
@@ -0,0 +1,27 @@
1
+ import * as asana from "#core/integrations/plugins/asana.js";
2
+ import { resolveAsana, PROJECT_ARG } from "./_asana.js";
3
+
4
+ export default {
5
+ name: "asana_list_tasks",
6
+ category: "integrations",
7
+ schema: {
8
+ type: "function",
9
+ function: {
10
+ name: "asana_list_tasks",
11
+ description: "List tasks in an Asana project.",
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ project_gid: { type: "string", description: "Asana project gid (from asana_list_projects)" },
16
+ completed: { type: "boolean", description: "Include completed tasks (default false)" },
17
+ ...PROJECT_ARG,
18
+ },
19
+ required: ["project_gid"],
20
+ },
21
+ },
22
+ },
23
+ makeHandler: ({ projects }) => async ({ project, project_gid, completed = false } = {}) => {
24
+ const { token } = resolveAsana(projects, project);
25
+ return { tasks: await asana.listTasks(token, project_gid, completed) };
26
+ },
27
+ };
@@ -0,0 +1,32 @@
1
+ import * as asana from "#core/integrations/plugins/asana.js";
2
+ import { resolveAsana, PROJECT_ARG } from "./_asana.js";
3
+
4
+ export default {
5
+ name: "asana_update_task",
6
+ category: "integrations",
7
+ schema: {
8
+ type: "function",
9
+ function: {
10
+ name: "asana_update_task",
11
+ description: "Update an Asana task's fields (rename, complete, reschedule, reassign).",
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ task_gid: { type: "string", description: "Asana task gid" },
16
+ name: { type: "string" },
17
+ notes: { type: "string" },
18
+ completed: { type: "boolean" },
19
+ due_on: { type: "string", description: "YYYY-MM-DD" },
20
+ assignee: { type: "string" },
21
+ ...PROJECT_ARG,
22
+ },
23
+ required: ["task_gid"],
24
+ },
25
+ },
26
+ },
27
+ makeHandler: ({ projects }) => async ({ project, task_gid, name, notes, completed, due_on, assignee } = {}) => {
28
+ const { token } = resolveAsana(projects, project);
29
+ const task = await asana.updateTask(token, task_gid, { name, notes, completed, dueOn: due_on, assignee });
30
+ return { task };
31
+ },
32
+ };
@@ -1,5 +1,5 @@
1
1
  import { listSkills, SKILL_LOCATIONS } from "#core/agent/skills/loader.js";
2
- import { condenseSkillDescription } from "#core/agent/skills/index.js";
2
+ import { condenseSkillDescription, filterEnabledSkills } from "#core/agent/skills/index.js";
3
3
 
4
4
  export default {
5
5
  name: "list_skills",
@@ -20,8 +20,11 @@ export default {
20
20
  },
21
21
  },
22
22
  },
23
- makeHandler: () => ({ project_path } = {}) => {
24
- const skills = listSkills({ projectPath: project_path });
23
+ makeHandler: (ctx = {}) => ({ project_path } = {}) => {
24
+ const skills = filterEnabledSkills(
25
+ listSkills({ projectPath: project_path }),
26
+ { config: ctx.globalConfig, projectPath: project_path },
27
+ );
25
28
  return {
26
29
  ok: true,
27
30
  count: skills.length,
@@ -1,4 +1,5 @@
1
1
  import { loadSkill } from "#core/agent/skills/loader.js";
2
+ import { isSkillEnabled } from "#core/agent/skills/policy.js";
2
3
 
3
4
  export default {
4
5
  name: "load_skill",
@@ -24,8 +25,14 @@ export default {
24
25
  },
25
26
  },
26
27
  },
27
- makeHandler: () => ({ slug, project_path } = {}) => {
28
+ makeHandler: (ctx = {}) => ({ slug, project_path } = {}) => {
28
29
  if (!slug) throw new Error("load_skill: slug required");
29
- return loadSkill(slug, { projectPath: project_path });
30
+ const skill = loadSkill(slug, { projectPath: project_path });
31
+ if (!isSkillEnabled(skill, { config: ctx.globalConfig, projectPath: project_path })) {
32
+ throw new Error(
33
+ `skill "${slug}" is disabled for this scope. Enable it in Settings → Skills, or pick another.`,
34
+ );
35
+ }
36
+ return skill;
30
37
  },
31
38
  };
@@ -50,6 +50,12 @@ export const TOOLS = Object.freeze({
50
50
  CALL_MCP: "call_mcp",
51
51
  CALL_RUNTIME: "call_runtime",
52
52
 
53
+ // Integrations — Asana plugin (see core/integrations/plugins/asana.js)
54
+ ASANA_LIST_PROJECTS: "asana_list_projects",
55
+ ASANA_LIST_TASKS: "asana_list_tasks",
56
+ ASANA_CREATE_TASK: "asana_create_task",
57
+ ASANA_UPDATE_TASK: "asana_update_task",
58
+
53
59
  // Side-effects
54
60
  SEND_TELEGRAM: "send_telegram",
55
61
  SET_IDENTITY: "set_identity",
@@ -94,6 +100,10 @@ export const NATIVE_TOOL_NAMES = new Set([
94
100
  TOOLS.CALL_AGENT,
95
101
  TOOLS.CALL_MCP,
96
102
  TOOLS.CALL_RUNTIME,
103
+ TOOLS.ASANA_LIST_PROJECTS,
104
+ TOOLS.ASANA_LIST_TASKS,
105
+ TOOLS.ASANA_CREATE_TASK,
106
+ TOOLS.ASANA_UPDATE_TASK,
97
107
  TOOLS.SEND_TELEGRAM,
98
108
  TOOLS.SET_IDENTITY,
99
109
  TOOLS.SET_PERMISSION_MODE,
@@ -33,6 +33,10 @@ import gitStatus from "./handlers/git-status.js";
33
33
  import gitDiff from "./handlers/git-diff.js";
34
34
  import gitLog from "./handlers/git-log.js";
35
35
  import gitShow from "./handlers/git-show.js";
36
+ import asanaListProjects from "./handlers/asana-list-projects.js";
37
+ import asanaListTasks from "./handlers/asana-list-tasks.js";
38
+ import asanaCreateTask from "./handlers/asana-create-task.js";
39
+ import asanaUpdateTask from "./handlers/asana-update-task.js";
36
40
  import { createPermissionGuard } from "./helpers.js";
37
41
  import { buildBridgedTools, DEFAULT_CATEGORIES } from "./registry-bridge.js";
38
42
  import { TOOLS, CODE_CHANNEL_TOOLS } from "./names.js";
@@ -74,6 +78,13 @@ const NATIVE_TOOLS = [
74
78
  gitDiff,
75
79
  gitLog,
76
80
  gitShow,
81
+ // Integration plugin tools (Asana). Each carries its own `category`, so it
82
+ // lands in the "integrations" group of the discover_tools catalog and stays
83
+ // lazy on chat channels until activated.
84
+ asanaListProjects,
85
+ asanaListTasks,
86
+ asanaCreateTask,
87
+ asanaUpdateTask,
77
88
  ];
78
89
 
79
90
  // Registry-backed bridges. Categories can be overridden per-process via env
@@ -0,0 +1,66 @@
1
+ // Plugin catalog — the source of truth for which integration plugins exist and
2
+ // which are wired end-to-end. The daemon API + web "Plugins" tab read this.
3
+ //
4
+ // A plugin is "implemented" when it has a service module in ./plugins/ that
5
+ // satisfies the lifecycle contract (configure/validate/status/deactivate). The
6
+ // rest are declared here with `coming_soon: true` so the Integrations page shows
7
+ // the full roster (matching PandaProject) while remaining honest about what is
8
+ // actually connectable. Adding a new plugin = drop a module in ./plugins/ and
9
+ // register it in PLUGIN_SERVICES — no API or route changes (open/closed).
10
+ import { asanaPlugin } from "./plugins/asana.js";
11
+
12
+ // slug -> live plugin service (must implement the lifecycle contract).
13
+ export const PLUGIN_SERVICES = Object.freeze({
14
+ asana: asanaPlugin,
15
+ });
16
+
17
+ // Static descriptors for the catalog UI. Implemented plugins derive their
18
+ // metadata from the service module; coming-soon ones are declared inline.
19
+ // `coming_soon` entries in PandaProject depend on domain/channel infrastructure
20
+ // (GitHub outputs↔repos, a WhatsApp-Web bridge, Telegram-voice whisper) that
21
+ // APX models differently — they are placeholders until ported natively.
22
+ 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
+ {
32
+ slug: "whatsapp",
33
+ name: "WhatsApp",
34
+ type: "channel",
35
+ description: "Conectá WhatsApp Web al orquestador — responde mensajes automáticamente",
36
+ auth: "qr",
37
+ coming_soon: true,
38
+ },
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
+ ];
48
+
49
+ // The full catalog: implemented plugins first, then coming-soon.
50
+ export function listCatalog() {
51
+ const implemented = Object.values(PLUGIN_SERVICES).map((p) => ({
52
+ slug: p.slug,
53
+ name: p.name,
54
+ type: p.type,
55
+ description: p.description,
56
+ auth: p.auth,
57
+ tools: p.tools || [],
58
+ coming_soon: false,
59
+ }));
60
+ return [...implemented, ...COMING_SOON];
61
+ }
62
+
63
+ // Resolve a live plugin service by slug (or null when not implemented).
64
+ export function getPluginService(slug) {
65
+ return PLUGIN_SERVICES[slug] || null;
66
+ }
@@ -0,0 +1,10 @@
1
+ // Public surface of the integrations core module. Daemon + agent tools import
2
+ // from here.
3
+ export {
4
+ integrationsPath,
5
+ defaultIntegrationsStorage,
6
+ readIntegrations,
7
+ writeIntegrations,
8
+ } from "./sources.js";
9
+ export { IntegrationStore, resolveIntegration, redactRecord } from "./store.js";
10
+ export { listCatalog, getPluginService, PLUGIN_SERVICES } from "./catalog.js";