@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
@@ -1,16 +1,36 @@
1
- // `/skills` listing + Skill Inspector control surface for UI clients.
1
+ // `/skills` listing + enable/disable + Skill Inspector control surface.
2
2
  //
3
- // GET /skills catalog (slug + condensed description)
4
- // GET /skills/inspector inspector config + index status
5
- // PUT /skills/inspector toggle / tune inspector config
6
- // POST /skills/index (re)build the inspector vector index
7
- // POST /skills/inspect dry-run the inspector for a prompt
3
+ // GET /skills catalog annotated with enabled/private per scope
4
+ // GET /skills/:slug/detail full body + frontmatter for the viewer
5
+ // PUT /skills/enabled toggle a skill on/off (or clear) for a scope
6
+ // POST /skills create a user skill (online editor)
7
+ // POST /skills/import/zip import a skill from an uploaded .zip
8
+ // POST /skills/import/repo import a skill by cloning a git repo
9
+ // DELETE /skills/:slug delete a user skill
10
+ // GET /skills/inspector inspector config + index status
11
+ // PUT /skills/inspector toggle / tune inspector config
12
+ // POST /skills/index (re)build the inspector vector index
13
+ // POST /skills/inspect dry-run the inspector for a prompt
8
14
  //
9
- // The listing is the same data backing `list_skills` (no auth-binding to a
10
- // project). The inspector routes mirror /embeddings/* so the web admin can
11
- // configure the skill RAG exactly like it configures the memory RAG.
12
- import { listSkills } from "#core/agent/skills/loader.js";
15
+ // A "scope" is either "default" (the super-agent / no-project baseline) or a
16
+ // project's absolute path. Creating/importing with a project_path targets that
17
+ // project's <project>/.apc/skills/ (source "project"); without it, skills land
18
+ // in ~/.apx/skills/ (source "global"). Built-in skills are private: always
19
+ // active, never disableable or deletable. The inspector routes mirror
20
+ // /embeddings/* so the web admin configures the skill RAG like the memory RAG.
21
+ import fs from "node:fs";
22
+ import os from "node:os";
23
+ import path from "node:path";
24
+ import { spawnSync } from "node:child_process";
25
+ import { listSkills, loadSkill, SKILL_LOCATIONS } from "#core/agent/skills/loader.js";
13
26
  import { condenseSkillDescription } from "#core/agent/skills/catalog.js";
27
+ import { apcSkillsDir } from "#core/apc/paths.js";
28
+ import {
29
+ annotateSkills,
30
+ setSkillEnabled,
31
+ resolveScopeKey,
32
+ isPrivateSkill,
33
+ } from "#core/agent/skills/policy.js";
14
34
  import {
15
35
  inspectPromptForSkills,
16
36
  INSPECTOR_DEFAULTS,
@@ -22,8 +42,63 @@ import {
22
42
  } from "#core/agent/skills/index-store.js";
23
43
  import { readConfig, writeConfig } from "#core/config/index.js";
24
44
 
45
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
46
+ // Only http(s)/ssh/git git remotes — never a local path or shell metachar.
47
+ const REPO_URL_RE = /^(https?:\/\/|git@|ssh:\/\/|git:\/\/)[\w.@:/\-~]+$/;
48
+
25
49
  const KNOWN_KEYS = Object.keys(INSPECTOR_DEFAULTS);
26
50
 
51
+ // Where a newly created/imported skill lands, given a scope. A project_path
52
+ // targets that project's .apc/skills/ (source "project"); otherwise the global
53
+ // ~/.apx/skills/ (source "global").
54
+ function targetSkillsDir(projectPath) {
55
+ return projectPath ? apcSkillsDir(projectPath) : SKILL_LOCATIONS.global;
56
+ }
57
+
58
+ function skillExists(slug, projectPath) {
59
+ return listSkills({ projectPath }).some((s) => s.slug === slug);
60
+ }
61
+
62
+ function writeSkillFile(dir, slug, description, body) {
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ const fmDesc = String(description || "").replace(/\r?\n/g, " ").trim();
65
+ const content =
66
+ `---\nname: ${slug}\ndescription: ${fmDesc}\n---\n\n${String(body || "").trim()}\n`;
67
+ fs.writeFileSync(path.join(dir, "SKILL.md"), content, "utf8");
68
+ }
69
+
70
+ // Find the skill root inside an extracted/cloned tree: the dir that directly
71
+ // contains a SKILL.md (the tree itself, or its single top-level subdir).
72
+ function findSkillRoot(root) {
73
+ if (fs.existsSync(path.join(root, "SKILL.md"))) return root;
74
+ let entries;
75
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return null; }
76
+ const dirs = entries.filter((e) => e.isDirectory() && e.name !== "__MACOSX");
77
+ for (const d of dirs) {
78
+ const sub = path.join(root, d.name);
79
+ if (fs.existsSync(path.join(sub, "SKILL.md"))) return sub;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function readSlugFromSkill(dir, fallback) {
85
+ try {
86
+ const raw = fs.readFileSync(path.join(dir, "SKILL.md"), "utf8");
87
+ const m = raw.match(/^---[\s\S]*?\bname\s*:\s*(.+?)\s*$/m);
88
+ if (m && SLUG_RE.test(m[1].trim())) return m[1].trim();
89
+ } catch { /* ignore */ }
90
+ return fallback;
91
+ }
92
+
93
+ // Copy an extracted skill dir into the target skills location under <slug>/.
94
+ function installSkillDir(srcDir, slug, projectPath) {
95
+ const destBase = targetSkillsDir(projectPath);
96
+ const dest = path.join(destBase, slug);
97
+ fs.mkdirSync(destBase, { recursive: true });
98
+ fs.cpSync(srcDir, dest, { recursive: true });
99
+ return dest;
100
+ }
101
+
27
102
  function mergedInspectorConfig(cfg) {
28
103
  return { ...INSPECTOR_DEFAULTS, ...(cfg?.skills?.inspector || {}) };
29
104
  }
@@ -40,17 +115,31 @@ function indexStatus() {
40
115
 
41
116
  export function register(app /*, ctx */) {
42
117
  app.get("/skills", (req, res) => {
43
- const projectPath = typeof req.query?.project_path === "string"
118
+ const projectPath = typeof req.query?.project_path === "string" && req.query.project_path
44
119
  ? req.query.project_path
45
120
  : undefined;
121
+ // A caller may ask about the "default" (super-agent) scope while still
122
+ // wanting project-scoped skills scanned — keep the two concerns separate.
123
+ const scope = typeof req.query?.scope === "string" && req.query.scope
124
+ ? req.query.scope
125
+ : undefined;
46
126
  try {
47
- const skills = listSkills({ projectPath });
127
+ const cfg = readConfig();
128
+ const scopeKey = resolveScopeKey(scope || projectPath);
129
+ const annotated = annotateSkills(listSkills({ projectPath }), {
130
+ config: cfg,
131
+ projectPath: scopeKey === "default" ? undefined : scopeKey,
132
+ });
48
133
  res.json({
49
- count: skills.length,
50
- skills: skills.map(({ slug, source, description }) => ({
51
- slug,
52
- source,
53
- description: condenseSkillDescription(description),
134
+ count: annotated.length,
135
+ scope: scopeKey,
136
+ skills: annotated.map((s) => ({
137
+ slug: s.slug,
138
+ source: s.source,
139
+ description: condenseSkillDescription(s.description),
140
+ enabled: s.enabled,
141
+ private: s.private,
142
+ overridden: s.overridden,
54
143
  })),
55
144
  });
56
145
  } catch (e) {
@@ -58,6 +147,201 @@ export function register(app /*, ctx */) {
58
147
  }
59
148
  });
60
149
 
150
+ // ---- Full detail (viewer) ----------------------------------------------
151
+
152
+ app.get("/skills/:slug/detail", (req, res) => {
153
+ try {
154
+ const projectPath = typeof req.query?.project_path === "string" && req.query.project_path
155
+ ? req.query.project_path
156
+ : undefined;
157
+ const skill = loadSkill(req.params.slug, { projectPath });
158
+ const cfg = readConfig();
159
+ const [annotated] = annotateSkills([skill], { config: cfg, projectPath });
160
+ res.json({
161
+ slug: skill.slug,
162
+ source: skill.source,
163
+ description: skill.description,
164
+ frontmatter: skill.frontmatter,
165
+ body: skill.body,
166
+ file: skill.file,
167
+ enabled: annotated?.enabled ?? true,
168
+ private: annotated?.private ?? false,
169
+ overridden: annotated?.overridden ?? false,
170
+ });
171
+ } catch (e) {
172
+ res.status(404).json({ error: e.message });
173
+ }
174
+ });
175
+
176
+ // ---- Enable / disable per scope ----------------------------------------
177
+
178
+ app.put("/skills/enabled", (req, res) => {
179
+ try {
180
+ const { slug, enabled, scope, project_path } = req.body || {};
181
+ if (!slug || typeof slug !== "string") {
182
+ return res.status(400).json({ error: "slug required" });
183
+ }
184
+ const cfg = readConfig();
185
+ const all = listSkills({ projectPath: project_path });
186
+ const target = all.find((s) => s.slug === slug);
187
+ if (!target) return res.status(404).json({ error: `skill "${slug}" not found` });
188
+ if (isPrivateSkill(target)) {
189
+ return res.status(403).json({ error: `skill "${slug}" is private (built-in) and always active` });
190
+ }
191
+ // enabled: boolean sets an override; null/undefined clears it (inherit).
192
+ const value = enabled === null || enabled === undefined ? null : !!enabled;
193
+ setSkillEnabled(cfg, { slug, enabled: value, scope, projectPath: project_path });
194
+ writeConfig(cfg);
195
+ const scopeKey = resolveScopeKey(scope || project_path);
196
+ res.json({ ok: true, slug, scope: scopeKey, enabled: value });
197
+ } catch (e) {
198
+ res.status(500).json({ error: e.message });
199
+ }
200
+ });
201
+
202
+ // ---- Create / import user skills ---------------------------------------
203
+
204
+ // Online editor: write a SKILL.md from slug + description + body.
205
+ app.post("/skills", (req, res) => {
206
+ try {
207
+ const { slug, description, body, project_path } = req.body || {};
208
+ if (!slug || typeof slug !== "string" || !SLUG_RE.test(slug)) {
209
+ return res.status(400).json({ error: "slug required (lowercase letters, digits, dashes)" });
210
+ }
211
+ if (skillExists(slug, project_path)) {
212
+ return res.status(409).json({ error: `a skill named "${slug}" already exists in this scope` });
213
+ }
214
+ const dir = path.join(targetSkillsDir(project_path), slug);
215
+ writeSkillFile(dir, slug, description, body);
216
+ res.status(201).json({ ok: true, slug, source: project_path ? "project" : "global" });
217
+ } catch (e) {
218
+ res.status(500).json({ error: e.message });
219
+ }
220
+ });
221
+
222
+ // Import from an uploaded .zip (sent as base64 in JSON — skills are tiny).
223
+ app.post("/skills/import/zip", (req, res) => {
224
+ let tmp;
225
+ try {
226
+ const { data, project_path } = req.body || {};
227
+ if (!data || typeof data !== "string") {
228
+ return res.status(400).json({ error: "zip data (base64) required" });
229
+ }
230
+ const buf = Buffer.from(data.replace(/^data:.*;base64,/, ""), "base64");
231
+ if (!buf.length) return res.status(400).json({ error: "empty zip" });
232
+
233
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), "apx-skill-zip-"));
234
+ const zipPath = path.join(tmp, "skill.zip");
235
+ fs.writeFileSync(zipPath, buf);
236
+ const out = path.join(tmp, "out");
237
+ fs.mkdirSync(out);
238
+ const unzip = spawnSync("unzip", ["-qq", "-o", zipPath, "-d", out], { encoding: "utf8" });
239
+ if (unzip.status !== 0) {
240
+ return res.status(400).json({ error: `unzip failed: ${(unzip.stderr || "bad archive").trim()}` });
241
+ }
242
+ const root = findSkillRoot(out);
243
+ if (!root) return res.status(400).json({ error: "no SKILL.md found in the zip" });
244
+ const slug = readSlugFromSkill(root, path.basename(root));
245
+ if (!SLUG_RE.test(slug)) return res.status(400).json({ error: `invalid skill name "${slug}"` });
246
+ if (skillExists(slug, project_path)) {
247
+ return res.status(409).json({ error: `a skill named "${slug}" already exists in this scope` });
248
+ }
249
+ installSkillDir(root, slug, project_path);
250
+ res.status(201).json({ ok: true, slug, source: project_path ? "project" : "global" });
251
+ } catch (e) {
252
+ res.status(500).json({ error: e.message });
253
+ } finally {
254
+ if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
255
+ }
256
+ });
257
+
258
+ // Import by cloning a git repo. The repo root (or its single skill subdir)
259
+ // must contain a SKILL.md.
260
+ app.post("/skills/import/repo", (req, res) => {
261
+ let tmp;
262
+ try {
263
+ const { url, project_path } = req.body || {};
264
+ if (!url || typeof url !== "string" || !REPO_URL_RE.test(url.trim())) {
265
+ return res.status(400).json({ error: "a valid git URL (https/ssh/git) is required" });
266
+ }
267
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), "apx-skill-repo-"));
268
+ const clone = path.join(tmp, "repo");
269
+ // Array args (no shell) — url is regex-validated above.
270
+ const git = spawnSync("git", ["clone", "--depth", "1", url.trim(), clone], {
271
+ encoding: "utf8",
272
+ timeout: 60_000,
273
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
274
+ });
275
+ if (git.status !== 0) {
276
+ return res.status(400).json({ error: `git clone failed: ${(git.stderr || "").trim().slice(0, 300)}` });
277
+ }
278
+ const root = findSkillRoot(clone);
279
+ if (!root) return res.status(400).json({ error: "no SKILL.md found in the repo" });
280
+ const slug = readSlugFromSkill(root, path.basename(root));
281
+ if (!SLUG_RE.test(slug)) return res.status(400).json({ error: `invalid skill name "${slug}"` });
282
+ if (skillExists(slug, project_path)) {
283
+ return res.status(409).json({ error: `a skill named "${slug}" already exists in this scope` });
284
+ }
285
+ // Strip the repo's .git before installing so we don't nest a git dir.
286
+ fs.rmSync(path.join(root, ".git"), { recursive: true, force: true });
287
+ installSkillDir(root, slug, project_path);
288
+ res.status(201).json({ ok: true, slug, source: project_path ? "project" : "global" });
289
+ } catch (e) {
290
+ res.status(500).json({ error: e.message });
291
+ } finally {
292
+ if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
293
+ }
294
+ });
295
+
296
+ app.delete("/skills/:slug", (req, res) => {
297
+ try {
298
+ const slug = req.params.slug;
299
+ if (!slug || !SLUG_RE.test(slug)) {
300
+ return res.status(400).json({ error: "invalid slug" });
301
+ }
302
+ const projectPath = typeof req.query?.project_path === "string" && req.query.project_path
303
+ ? req.query.project_path
304
+ : undefined;
305
+ let entry;
306
+ try { entry = loadSkill(slug, { projectPath }); } catch { entry = null; }
307
+ if (!entry) return res.status(404).json({ error: `skill "${slug}" not found` });
308
+ // Only user-managed skills (global ~/.apx/skills or project .apc/skills)
309
+ // can be deleted; built-in ones ship with apx.
310
+ if (entry.source !== "global" && entry.source !== "project") {
311
+ return res.status(403).json({ error: `built-in skill "${slug}" cannot be deleted (it ships with apx)` });
312
+ }
313
+ // The skill dir is the parent of its SKILL.md (dir-style) — never the
314
+ // shared skills root itself.
315
+ const dir = path.dirname(entry.file);
316
+ const roots = [SKILL_LOCATIONS.global, projectPath ? apcSkillsDir(projectPath) : null]
317
+ .filter(Boolean)
318
+ .map((p) => path.resolve(p));
319
+ if (roots.includes(path.resolve(dir))) {
320
+ // Flat-style <slug>.md — remove just the file.
321
+ fs.rmSync(entry.file, { force: true });
322
+ } else {
323
+ fs.rmSync(dir, { recursive: true, force: true });
324
+ }
325
+ // Drop any dangling enable/disable overrides for this slug.
326
+ const cfg = readConfig();
327
+ const pol = cfg?.skills?.policy;
328
+ if (pol && typeof pol === "object") {
329
+ let touched = false;
330
+ for (const scopeKey of Object.keys(pol)) {
331
+ if (pol[scopeKey] && slug in pol[scopeKey]) {
332
+ delete pol[scopeKey][slug];
333
+ if (Object.keys(pol[scopeKey]).length === 0) delete pol[scopeKey];
334
+ touched = true;
335
+ }
336
+ }
337
+ if (touched) writeConfig(cfg);
338
+ }
339
+ res.json({ ok: true, slug });
340
+ } catch (e) {
341
+ res.status(500).json({ error: e.message });
342
+ }
343
+ });
344
+
61
345
  // ---- Inspector config + status -----------------------------------------
62
346
 
63
347
  app.get("/skills/inspector", (_req, res) => {
@@ -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);
@@ -215,10 +215,11 @@ export async function cmdSkillsList(args = {}) {
215
215
  console.log("(no skills available)");
216
216
  return;
217
217
  }
218
- console.log(`SLUG`.padEnd(28) + "SOURCE".padEnd(10) + "DESCRIPTION");
218
+ console.log(`SLUG`.padEnd(28) + "SOURCE".padEnd(10) + "STATE".padEnd(10) + "DESCRIPTION");
219
219
  for (const s of out.skills) {
220
220
  const desc = (s.description || "").slice(0, 70);
221
- console.log(s.slug.padEnd(28) + (s.source || "?").padEnd(10) + desc);
221
+ const state = s.private ? "private" : s.enabled === false ? "off" : "on";
222
+ console.log(s.slug.padEnd(28) + (s.source || "?").padEnd(10) + state.padEnd(10) + desc);
222
223
  }
223
224
  return;
224
225
  }