@agentprojectcontext/apx 1.59.0 → 1.60.1
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/_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/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/core/stores/project-files.js +21 -2
- package/src/host/daemon/api/integrations.js +191 -0
- package/src/host/daemon/api.js +2 -0
- package/src/interfaces/web/dist/assets/{index-CnQb4N6C.js → index-DFNV6BWh.js} +193 -163
- package/src/interfaces/web/dist/assets/index-DFNV6BWh.js.map +1 -0
- 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/lib/api/integrations.ts +106 -0
- package/src/interfaces/web/src/lib/api.ts +1 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +6 -2
- package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +146 -0
- package/src/interfaces/web/dist/assets/index-CnQb4N6C.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-Dv3X-zpx.css +0 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// IntegrationStore: CRUD over one project's integrations.json, plus the
|
|
2
|
+
// cross-scope resolution used by agent tools. Pure filesystem + data logic —
|
|
3
|
+
// no Express, no plugin HTTP calls (those live in ./plugins/*). This keeps the
|
|
4
|
+
// storage layer testable in isolation (see tests/integrations.test.js).
|
|
5
|
+
import {
|
|
6
|
+
readIntegrations,
|
|
7
|
+
writeIntegrations,
|
|
8
|
+
defaultIntegrationsStorage,
|
|
9
|
+
} from "./sources.js";
|
|
10
|
+
|
|
11
|
+
// Keys inside `config` that hold secrets — never sent to the web UI in clear.
|
|
12
|
+
const SECRET_KEYS = new Set([
|
|
13
|
+
"personal_access_token",
|
|
14
|
+
"token",
|
|
15
|
+
"api_key",
|
|
16
|
+
"client_secret",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
// Return a shallow copy of a record with secret config values masked, safe to
|
|
20
|
+
// send to the browser. A present secret becomes `true` under `config.<key>_set`
|
|
21
|
+
// so the UI can show "configured" without leaking the value.
|
|
22
|
+
export function redactRecord(record) {
|
|
23
|
+
if (!record) return record;
|
|
24
|
+
const config = { ...(record.config || {}) };
|
|
25
|
+
for (const key of Object.keys(config)) {
|
|
26
|
+
if (SECRET_KEYS.has(key)) {
|
|
27
|
+
const hasValue = typeof config[key] === "string" && config[key].length > 0;
|
|
28
|
+
delete config[key];
|
|
29
|
+
config[`${key}_set`] = hasValue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { ...record, config };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class IntegrationStore {
|
|
36
|
+
// `storagePath` is a project's ~/.apx/projects/<apxId>/ directory.
|
|
37
|
+
constructor(storagePath) {
|
|
38
|
+
if (!storagePath) throw new Error("IntegrationStore: storagePath required");
|
|
39
|
+
this.storagePath = storagePath;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
list() {
|
|
43
|
+
return readIntegrations(this.storagePath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get(slug) {
|
|
47
|
+
return this.list().find((r) => r.slug === slug) || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Insert or merge a record by slug. `patch` is shallow-merged; `patch.config`
|
|
51
|
+
// is deep-merged one level so callers can update a single config key without
|
|
52
|
+
// clobbering the token. Returns the persisted record.
|
|
53
|
+
upsert(slug, patch = {}) {
|
|
54
|
+
const entries = this.list();
|
|
55
|
+
const idx = entries.findIndex((r) => r.slug === slug);
|
|
56
|
+
const now = new Date().toISOString();
|
|
57
|
+
if (idx === -1) {
|
|
58
|
+
const record = {
|
|
59
|
+
slug,
|
|
60
|
+
name: patch.name || slug,
|
|
61
|
+
type: patch.type || "custom",
|
|
62
|
+
description: patch.description || "",
|
|
63
|
+
source: patch.source || "builtin",
|
|
64
|
+
status: patch.status || "disconnected",
|
|
65
|
+
is_enabled: patch.is_enabled ?? false,
|
|
66
|
+
config: patch.config || {},
|
|
67
|
+
created_at: now,
|
|
68
|
+
updated_at: now,
|
|
69
|
+
};
|
|
70
|
+
entries.push(record);
|
|
71
|
+
writeIntegrations(this.storagePath, entries);
|
|
72
|
+
return record;
|
|
73
|
+
}
|
|
74
|
+
const prev = entries[idx];
|
|
75
|
+
const merged = {
|
|
76
|
+
...prev,
|
|
77
|
+
...patch,
|
|
78
|
+
config: { ...(prev.config || {}), ...(patch.config || {}) },
|
|
79
|
+
updated_at: now,
|
|
80
|
+
};
|
|
81
|
+
entries[idx] = merged;
|
|
82
|
+
writeIntegrations(this.storagePath, entries);
|
|
83
|
+
return merged;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
remove(slug) {
|
|
87
|
+
const entries = this.list();
|
|
88
|
+
const next = entries.filter((r) => r.slug !== slug);
|
|
89
|
+
if (next.length === entries.length) return false;
|
|
90
|
+
writeIntegrations(this.storagePath, next);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Resolve the effective integration for `slug` in a project, applying the
|
|
96
|
+
// project→default precedence: a project's OWN enabled record wins; otherwise
|
|
97
|
+
// the default project's enabled record is used. Returns { record, scope } or
|
|
98
|
+
// null when neither is usable. `defaultStorage` defaults to the default
|
|
99
|
+
// project's storage so callers only need the current project's storagePath.
|
|
100
|
+
export function resolveIntegration({
|
|
101
|
+
projectStorage,
|
|
102
|
+
slug,
|
|
103
|
+
defaultStorage = defaultIntegrationsStorage(),
|
|
104
|
+
requireEnabled = true,
|
|
105
|
+
}) {
|
|
106
|
+
const usable = (record) =>
|
|
107
|
+
!!record && (!requireEnabled || (record.is_enabled && record.status === "active"));
|
|
108
|
+
|
|
109
|
+
if (projectStorage) {
|
|
110
|
+
const own = new IntegrationStore(projectStorage).get(slug);
|
|
111
|
+
if (usable(own)) return { record: own, scope: "project", storagePath: projectStorage };
|
|
112
|
+
}
|
|
113
|
+
if (defaultStorage && defaultStorage !== projectStorage) {
|
|
114
|
+
const fallback = new IntegrationStore(defaultStorage).get(slug);
|
|
115
|
+
if (usable(fallback)) return { record: fallback, scope: "global", storagePath: defaultStorage };
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
@@ -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
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Integration plugins per project. Companion to api/mcps.js: MCP servers are
|
|
2
|
+
// raw tool endpoints; integrations are higher-level plugins (Asana today) that
|
|
3
|
+
// own a credential + lifecycle and expose named tools to agents.
|
|
4
|
+
//
|
|
5
|
+
// Scoping (see core/integrations/store.js): every record lives in one project's
|
|
6
|
+
// integrations.json. `?scope=global` targets the DEFAULT project's store, so
|
|
7
|
+
// "global" integrations are literally the default project's — a project without
|
|
8
|
+
// its own record falls back to that one. This keeps "which Asana runs here?"
|
|
9
|
+
// unambiguous when you have both a base Asana and a per-project Asana.
|
|
10
|
+
//
|
|
11
|
+
// GET /projects/:pid/integrations?scope=project|global list stored (redacted)
|
|
12
|
+
// GET /projects/:pid/integrations/catalog roster + resolved status
|
|
13
|
+
// GET /projects/:pid/integrations/:slug?scope=… one plugin status
|
|
14
|
+
// POST /projects/:pid/integrations/:slug/configure?scope=… save credentials
|
|
15
|
+
// POST /projects/:pid/integrations/:slug/validate?scope=… verify against provider
|
|
16
|
+
// POST /projects/:pid/integrations/:slug/deactivate?scope=… disable
|
|
17
|
+
// POST /projects/:pid/integrations/:slug/action/:action?scope=… plugin read action
|
|
18
|
+
// DELETE /projects/:pid/integrations/:slug?scope=… remove
|
|
19
|
+
import {
|
|
20
|
+
IntegrationStore,
|
|
21
|
+
resolveIntegration,
|
|
22
|
+
redactRecord,
|
|
23
|
+
defaultIntegrationsStorage,
|
|
24
|
+
listCatalog,
|
|
25
|
+
getPluginService,
|
|
26
|
+
} from "#core/integrations/index.js";
|
|
27
|
+
|
|
28
|
+
function normalizeScope(raw) {
|
|
29
|
+
if (!raw) return "project";
|
|
30
|
+
const s = String(raw).toLowerCase();
|
|
31
|
+
if (s === "global" || s === "default") return "global";
|
|
32
|
+
if (s === "project" || s === "shared" || s === "runtime") return "project";
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Resolve the storagePath for the requested scope. `global` → default project
|
|
37
|
+
// store; `project` → the current project's store.
|
|
38
|
+
function storagePathForScope(scope, p, projects) {
|
|
39
|
+
if (scope === "global") {
|
|
40
|
+
const base = projects.get(0);
|
|
41
|
+
return base?.storagePath || defaultIntegrationsStorage();
|
|
42
|
+
}
|
|
43
|
+
return p.storagePath || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function register(app, { projects, project }) {
|
|
47
|
+
// List stored integrations in the chosen scope (secrets redacted).
|
|
48
|
+
app.get("/projects/:pid/integrations", (req, res) => {
|
|
49
|
+
const p = project(req, res);
|
|
50
|
+
if (!p) return;
|
|
51
|
+
const scope = normalizeScope(req.query?.scope);
|
|
52
|
+
if (scope === null) return res.status(400).json({ error: `unknown scope "${req.query?.scope}"` });
|
|
53
|
+
const storagePath = storagePathForScope(scope, p, projects);
|
|
54
|
+
if (!storagePath) return res.status(400).json({ error: "project has no storage path" });
|
|
55
|
+
const records = new IntegrationStore(storagePath).list().map(redactRecord);
|
|
56
|
+
res.json(records);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// The full plugin roster with each plugin's resolved status for this project
|
|
60
|
+
// (project record wins over the default/global one).
|
|
61
|
+
app.get("/projects/:pid/integrations/catalog", (req, res) => {
|
|
62
|
+
const p = project(req, res);
|
|
63
|
+
if (!p) return;
|
|
64
|
+
const catalog = listCatalog().map((entry) => {
|
|
65
|
+
const svc = getPluginService(entry.slug);
|
|
66
|
+
let status = { slug: entry.slug, status: "disconnected", is_enabled: false };
|
|
67
|
+
let scope = null;
|
|
68
|
+
if (svc) {
|
|
69
|
+
const resolved = resolveIntegration({
|
|
70
|
+
projectStorage: p.storagePath,
|
|
71
|
+
slug: entry.slug,
|
|
72
|
+
requireEnabled: false,
|
|
73
|
+
});
|
|
74
|
+
status = svc.status(resolved?.record || null);
|
|
75
|
+
scope = resolved?.scope || null;
|
|
76
|
+
}
|
|
77
|
+
return { ...entry, status, resolved_scope: scope };
|
|
78
|
+
});
|
|
79
|
+
res.json(catalog);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Status for a single plugin in the chosen scope.
|
|
83
|
+
app.get("/projects/:pid/integrations/:slug", (req, res) => {
|
|
84
|
+
const p = project(req, res);
|
|
85
|
+
if (!p) return;
|
|
86
|
+
const svc = getPluginService(req.params.slug);
|
|
87
|
+
if (!svc) return res.status(404).json({ error: `unknown plugin "${req.params.slug}"` });
|
|
88
|
+
const scope = normalizeScope(req.query?.scope);
|
|
89
|
+
if (scope === null) return res.status(400).json({ error: `unknown scope "${req.query?.scope}"` });
|
|
90
|
+
const storagePath = storagePathForScope(scope, p, projects);
|
|
91
|
+
if (!storagePath) return res.status(400).json({ error: "project has no storage path" });
|
|
92
|
+
const record = new IntegrationStore(storagePath).get(req.params.slug);
|
|
93
|
+
res.json(svc.status(record));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Save credentials / config. Creates the record if missing.
|
|
97
|
+
app.post("/projects/:pid/integrations/:slug/configure", (req, res) => {
|
|
98
|
+
const p = project(req, res);
|
|
99
|
+
if (!p) return;
|
|
100
|
+
const svc = getPluginService(req.params.slug);
|
|
101
|
+
if (!svc) return res.status(404).json({ error: `unknown plugin "${req.params.slug}"` });
|
|
102
|
+
const scope = normalizeScope(req.query?.scope);
|
|
103
|
+
if (scope === null) return res.status(400).json({ error: `unknown scope "${req.query?.scope}"` });
|
|
104
|
+
const storagePath = storagePathForScope(scope, p, projects);
|
|
105
|
+
if (!storagePath) return res.status(400).json({ error: "project has no storage path" });
|
|
106
|
+
const store = new IntegrationStore(storagePath);
|
|
107
|
+
try {
|
|
108
|
+
const { patch } = svc.configure(store.get(req.params.slug), req.body || {});
|
|
109
|
+
const record = store.upsert(req.params.slug, patch);
|
|
110
|
+
res.status(201).json(redactRecord(record));
|
|
111
|
+
} catch (e) {
|
|
112
|
+
res.status(400).json({ error: e.message });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Verify the stored credentials against the provider, then persist the result.
|
|
117
|
+
app.post("/projects/:pid/integrations/:slug/validate", async (req, res) => {
|
|
118
|
+
const p = project(req, res);
|
|
119
|
+
if (!p) return;
|
|
120
|
+
const svc = getPluginService(req.params.slug);
|
|
121
|
+
if (!svc) return res.status(404).json({ error: `unknown plugin "${req.params.slug}"` });
|
|
122
|
+
const scope = normalizeScope(req.query?.scope);
|
|
123
|
+
if (scope === null) return res.status(400).json({ error: `unknown scope "${req.query?.scope}"` });
|
|
124
|
+
const storagePath = storagePathForScope(scope, p, projects);
|
|
125
|
+
if (!storagePath) return res.status(400).json({ error: "project has no storage path" });
|
|
126
|
+
const store = new IntegrationStore(storagePath);
|
|
127
|
+
const record = store.get(req.params.slug);
|
|
128
|
+
if (!record) return res.status(404).json({ error: "integration not configured" });
|
|
129
|
+
try {
|
|
130
|
+
const { patch, result } = await svc.validate(record);
|
|
131
|
+
store.upsert(req.params.slug, patch);
|
|
132
|
+
if (result && result.ok === false) return res.status(400).json(result);
|
|
133
|
+
res.json(result);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
res.status(400).json({ error: e.message });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Disable a plugin without deleting its stored credentials.
|
|
140
|
+
app.post("/projects/:pid/integrations/:slug/deactivate", (req, res) => {
|
|
141
|
+
const p = project(req, res);
|
|
142
|
+
if (!p) return;
|
|
143
|
+
const svc = getPluginService(req.params.slug);
|
|
144
|
+
if (!svc) return res.status(404).json({ error: `unknown plugin "${req.params.slug}"` });
|
|
145
|
+
const scope = normalizeScope(req.query?.scope);
|
|
146
|
+
if (scope === null) return res.status(400).json({ error: `unknown scope "${req.query?.scope}"` });
|
|
147
|
+
const storagePath = storagePathForScope(scope, p, projects);
|
|
148
|
+
if (!storagePath) return res.status(400).json({ error: "project has no storage path" });
|
|
149
|
+
const store = new IntegrationStore(storagePath);
|
|
150
|
+
if (!store.get(req.params.slug)) return res.status(404).json({ error: "integration not configured" });
|
|
151
|
+
const { patch } = svc.deactivate(store.get(req.params.slug));
|
|
152
|
+
const record = store.upsert(req.params.slug, patch);
|
|
153
|
+
res.json(svc.status(record));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Plugin-specific read action (e.g. Asana → list workspaces for the token).
|
|
157
|
+
app.post("/projects/:pid/integrations/:slug/action/:action", async (req, res) => {
|
|
158
|
+
const p = project(req, res);
|
|
159
|
+
if (!p) return;
|
|
160
|
+
const svc = getPluginService(req.params.slug);
|
|
161
|
+
if (!svc) return res.status(404).json({ error: `unknown plugin "${req.params.slug}"` });
|
|
162
|
+
const fn = svc.actions?.[req.params.action];
|
|
163
|
+
if (typeof fn !== "function") {
|
|
164
|
+
return res.status(404).json({ error: `unknown action "${req.params.action}"` });
|
|
165
|
+
}
|
|
166
|
+
const scope = normalizeScope(req.query?.scope);
|
|
167
|
+
if (scope === null) return res.status(400).json({ error: `unknown scope "${req.query?.scope}"` });
|
|
168
|
+
const storagePath = storagePathForScope(scope, p, projects);
|
|
169
|
+
if (!storagePath) return res.status(400).json({ error: "project has no storage path" });
|
|
170
|
+
const record = new IntegrationStore(storagePath).get(req.params.slug);
|
|
171
|
+
if (!record) return res.status(404).json({ error: "integration not configured" });
|
|
172
|
+
try {
|
|
173
|
+
res.json(await fn.call(svc.actions, record));
|
|
174
|
+
} catch (e) {
|
|
175
|
+
res.status(400).json({ error: e.message });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Remove a stored integration entirely.
|
|
180
|
+
app.delete("/projects/:pid/integrations/:slug", (req, res) => {
|
|
181
|
+
const p = project(req, res);
|
|
182
|
+
if (!p) return;
|
|
183
|
+
const scope = normalizeScope(req.query?.scope);
|
|
184
|
+
if (scope === null) return res.status(400).json({ error: `unknown scope "${req.query?.scope}"` });
|
|
185
|
+
const storagePath = storagePathForScope(scope, p, projects);
|
|
186
|
+
if (!storagePath) return res.status(400).json({ error: "project has no storage path" });
|
|
187
|
+
const removed = new IntegrationStore(storagePath).remove(req.params.slug);
|
|
188
|
+
if (!removed) return res.status(404).end();
|
|
189
|
+
res.status(204).end();
|
|
190
|
+
});
|
|
191
|
+
}
|
package/src/host/daemon/api.js
CHANGED
|
@@ -18,6 +18,7 @@ import { register as registerProjects } from "./api/projects.js";
|
|
|
18
18
|
import { register as registerAgents } from "./api/agents.js";
|
|
19
19
|
import { register as registerSessions } from "./api/sessions.js";
|
|
20
20
|
import { register as registerMcps } from "./api/mcps.js";
|
|
21
|
+
import { register as registerIntegrations } from "./api/integrations.js";
|
|
21
22
|
import { register as registerVars } from "./api/vars.js";
|
|
22
23
|
import { register as registerMessages } from "./api/messages.js";
|
|
23
24
|
import { register as registerTelegram } from "./api/telegram.js";
|
|
@@ -109,6 +110,7 @@ export function buildApi({
|
|
|
109
110
|
registerAgents(app, ctx);
|
|
110
111
|
registerSessions(app, ctx);
|
|
111
112
|
registerMcps(app, ctx);
|
|
113
|
+
registerIntegrations(app, ctx);
|
|
112
114
|
registerVars(app, ctx);
|
|
113
115
|
registerMessages(app, ctx);
|
|
114
116
|
registerEngines(app, ctx);
|