@agentprojectcontext/apx 1.57.0 → 1.59.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 (60) 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/list-skills.js +6 -3
  7. package/src/core/agent/tools/handlers/load-skill.js +9 -2
  8. package/src/core/apc/paths.js +7 -0
  9. package/src/core/stores/organization.js +152 -0
  10. package/src/core/stores/project-files.js +199 -0
  11. package/src/core/stores/tasks.js +36 -3
  12. package/src/host/daemon/api/agents.js +22 -2
  13. package/src/host/daemon/api/files-project.js +99 -0
  14. package/src/host/daemon/api/organization.js +88 -0
  15. package/src/host/daemon/api/shared.js +7 -0
  16. package/src/host/daemon/api/skills.js +301 -17
  17. package/src/host/daemon/api/tasks.js +14 -0
  18. package/src/host/daemon/api.js +4 -0
  19. package/src/interfaces/cli/commands/org.js +77 -0
  20. package/src/interfaces/cli/commands/skills.js +3 -2
  21. package/src/interfaces/cli/index.js +48 -0
  22. package/src/interfaces/web/dist/assets/index-CnQb4N6C.js +731 -0
  23. package/src/interfaces/web/dist/assets/index-CnQb4N6C.js.map +1 -0
  24. package/src/interfaces/web/dist/assets/index-Dv3X-zpx.css +1 -0
  25. package/src/interfaces/web/dist/index.html +2 -2
  26. package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +123 -0
  27. package/src/interfaces/web/src/components/common/ConfirmDialog.tsx +51 -0
  28. package/src/interfaces/web/src/components/files/FileBrowser.tsx +138 -0
  29. package/src/interfaces/web/src/components/files/FileTree.tsx +133 -0
  30. package/src/interfaces/web/src/components/files/FileViewer.tsx +167 -0
  31. package/src/interfaces/web/src/components/files/MarkdownEditor.tsx +48 -0
  32. package/src/interfaces/web/src/components/files/MarkdownPreview.tsx +146 -0
  33. package/src/interfaces/web/src/components/files/NewFileDialog.tsx +66 -0
  34. package/src/interfaces/web/src/components/settings/SkillsManager.tsx +465 -0
  35. package/src/interfaces/web/src/components/settings/SkillsSettings.tsx +49 -0
  36. package/src/interfaces/web/src/components/structure/StructureDialogs.tsx +172 -0
  37. package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +142 -0
  38. package/src/interfaces/web/src/components/tasks/taskStatus.tsx +57 -0
  39. package/src/interfaces/web/src/i18n/en.ts +171 -0
  40. package/src/interfaces/web/src/i18n/es.ts +171 -0
  41. package/src/interfaces/web/src/lib/api/organization.ts +18 -0
  42. package/src/interfaces/web/src/lib/api/projectFiles.ts +19 -0
  43. package/src/interfaces/web/src/lib/api/skills.ts +79 -8
  44. package/src/interfaces/web/src/lib/api/tasks.ts +16 -1
  45. package/src/interfaces/web/src/lib/api.ts +2 -0
  46. package/src/interfaces/web/src/lib/slug.ts +11 -0
  47. package/src/interfaces/web/src/screens/ProjectScreen.tsx +25 -2
  48. package/src/interfaces/web/src/screens/SettingsScreen.tsx +3 -3
  49. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +31 -11
  50. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +24 -7
  51. package/src/interfaces/web/src/screens/project/DocsTab.tsx +13 -0
  52. package/src/interfaces/web/src/screens/project/FilesTab.tsx +12 -0
  53. package/src/interfaces/web/src/screens/project/Overview.tsx +122 -10
  54. package/src/interfaces/web/src/screens/project/SkillsTab.tsx +13 -0
  55. package/src/interfaces/web/src/screens/project/StructureTab.tsx +147 -0
  56. package/src/interfaces/web/src/screens/project/TasksTab.tsx +101 -62
  57. package/src/interfaces/web/src/types/daemon.ts +63 -0
  58. package/src/interfaces/web/dist/assets/index-CEI8DfVg.css +0 -1
  59. package/src/interfaces/web/dist/assets/index-DJ-ocXOR.js +0 -651
  60. package/src/interfaces/web/dist/assets/index-DJ-ocXOR.js.map +0 -1
@@ -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) => {
@@ -14,7 +14,9 @@ import {
14
14
  doneTask,
15
15
  dropTask,
16
16
  reopenTask,
17
+ setTaskStatus,
17
18
  countTasks,
19
+ TASK_STATUSES,
18
20
  } from "#core/stores/tasks.js";
19
21
  import { pageEnvelope } from "./shared.js";
20
22
 
@@ -113,6 +115,18 @@ export function register(app, { project, projects }) {
113
115
  res.json(updated);
114
116
  });
115
117
 
118
+ // Move an open task through its workflow (pending → running → in_review …).
119
+ app.post("/projects/:pid/tasks/:id/status", (req, res) => {
120
+ const p = project(req, res);
121
+ if (!p) return;
122
+ const { status } = req.body || {};
123
+ if (!TASK_STATUSES.includes(status))
124
+ return res.status(400).json({ error: `status must be one of ${TASK_STATUSES.join(", ")}` });
125
+ const updated = setTaskStatus(p.storagePath, req.params.id, status);
126
+ if (!updated) return res.status(404).json({ error: "task not found" });
127
+ res.json(updated);
128
+ });
129
+
116
130
  // Lightweight summary endpoint for status displays.
117
131
  app.get("/projects/:pid/tasks-summary", (req, res) => {
118
132
  const p = project(req, res);
@@ -33,6 +33,8 @@ import { register as registerRuntimes } from "./api/runtimes.js";
33
33
  import { register as registerRoutines } from "./api/routines.js";
34
34
  import { register as registerArtifacts } from "./api/artifacts.js";
35
35
  import { register as registerTasks } from "./api/tasks.js";
36
+ import { register as registerOrganization } from "./api/organization.js";
37
+ import { register as registerProjectFiles } from "./api/files-project.js";
36
38
  import { register as registerConfig } from "./api/config.js";
37
39
  import { register as registerRun } from "./api/run.js";
38
40
  import { register as registerTopLevel } from "./api/top-level.js";
@@ -121,6 +123,8 @@ export function buildApi({
121
123
  registerRoutines(app, ctx);
122
124
  registerArtifacts(app, ctx);
123
125
  registerTasks(app, ctx);
126
+ registerOrganization(app, ctx);
127
+ registerProjectFiles(app, ctx);
124
128
  registerConfig(app, ctx);
125
129
 
126
130
  // ---- Top-level shortcuts (MCP server clients) --------------------
@@ -0,0 +1,77 @@
1
+ // apx org — organization structure (areas + roles) for a project.
2
+ // Backed by /projects/:pid/organization (core/stores/organization.js).
3
+ //
4
+ // apx org show [--project X]
5
+ // apx org area add "<name>" [--slug s] [--goal g] [--project X]
6
+ // apx org area rm <slug> [--project X]
7
+ // apx org role add "<name>" [--slug s] [--area a] [--desc d] [--project X]
8
+ // apx org role rm <slug> [--project X]
9
+ //
10
+ // Thin surface over the daemon API — the web panel calls the same routes.
11
+ import { http } from "../http.js";
12
+ import { resolveProjectId } from "./project.js";
13
+
14
+ export const ORG_USAGE = {
15
+ show: "apx org show [--project X]",
16
+ areaAdd: 'apx org area add "<name>" [--slug s] [--goal g] [--project X]',
17
+ areaRm: "apx org area rm <slug> [--project X]",
18
+ roleAdd: 'apx org role add "<name>" [--slug s] [--area a] [--desc d] [--project X]',
19
+ roleRm: "apx org role rm <slug> [--project X]",
20
+ };
21
+
22
+ function fail(key, msg) {
23
+ console.error(`apx org: ${msg}`);
24
+ console.error(`Usage: ${ORG_USAGE[key]}`);
25
+ process.exit(1);
26
+ }
27
+
28
+ export async function cmdOrgShow(args) {
29
+ const pid = await resolveProjectId(args?.flags?.project);
30
+ const org = await http.get(`/projects/${pid}/organization`);
31
+ if (!org.areas.length && !org.roles.length) {
32
+ console.log("(no organization structure yet)");
33
+ return;
34
+ }
35
+ console.log("Areas:");
36
+ for (const a of org.areas) console.log(` • ${a.name} (${a.slug})${a.goal ? ` — ${a.goal}` : ""}`);
37
+ console.log("Roles:");
38
+ for (const r of org.roles) {
39
+ console.log(` • ${r.name} (${r.slug})${r.area ? ` [${r.area}]` : ""}${r.description ? ` — ${r.description}` : ""}`);
40
+ }
41
+ }
42
+
43
+ export async function cmdOrgAreaAdd(args) {
44
+ const name = (args._ || []).slice(1).join(" ").trim();
45
+ if (!name) return fail("areaAdd", "name required");
46
+ const pid = await resolveProjectId(args?.flags?.project);
47
+ const area = await http.post(`/projects/${pid}/organization/areas`, {
48
+ name, slug: args.flags?.slug, goal: args.flags?.goal,
49
+ });
50
+ console.log(`added area ${area.name} (${area.slug})`);
51
+ }
52
+
53
+ export async function cmdOrgAreaRm(args) {
54
+ const slug = (args._ || [])[1];
55
+ if (!slug) return fail("areaRm", "slug required");
56
+ const pid = await resolveProjectId(args?.flags?.project);
57
+ await http.delete(`/projects/${pid}/organization/areas/${encodeURIComponent(slug)}`);
58
+ console.log(`removed area ${slug}`);
59
+ }
60
+
61
+ export async function cmdOrgRoleAdd(args) {
62
+ const name = (args._ || []).slice(1).join(" ").trim();
63
+ if (!name) return fail("roleAdd", "name required");
64
+ const pid = await resolveProjectId(args?.flags?.project);
65
+ const role = await http.post(`/projects/${pid}/organization/roles`, {
66
+ name, slug: args.flags?.slug, area: args.flags?.area, description: args.flags?.desc,
67
+ });
68
+ console.log(`added role ${role.name} (${role.slug})${role.area ? ` in ${role.area}` : ""}`);
69
+ }
70
+
71
+ export async function cmdOrgRoleRm(args) {
72
+ const slug = (args._ || [])[1];
73
+ if (!slug) return fail("roleRm", "slug required");
74
+ const pid = await resolveProjectId(args?.flags?.project);
75
+ await http.delete(`/projects/${pid}/organization/roles/${encodeURIComponent(slug)}`);
76
+ console.log(`removed role ${slug}`);
77
+ }
@@ -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
  }
@@ -140,6 +140,13 @@ import {
140
140
  cmdTaskReopen,
141
141
  cmdTaskPatch,
142
142
  } from "./commands/task.js";
143
+ import {
144
+ cmdOrgShow,
145
+ cmdOrgAreaAdd,
146
+ cmdOrgAreaRm,
147
+ cmdOrgRoleAdd,
148
+ cmdOrgRoleRm,
149
+ } from "./commands/org.js";
143
150
 
144
151
  const __filename = fileURLToPath(import.meta.url);
145
152
  const __dirname = path.dirname(__filename);
@@ -1531,6 +1538,24 @@ const HELP_TOPICS = new Map(Object.entries({
1531
1538
  usage: ["apx tasks <subcommand> [args] [--flags]"],
1532
1539
  examples: ["apx tasks list"],
1533
1540
  }),
1541
+ org: topic({
1542
+ title: "apx org",
1543
+ summary: "Organization structure (areas + roles) for a project — the org chart companies/enterprises use to group agents.",
1544
+ usage: ["apx org <show|area|role> [args] [--flags]"],
1545
+ commands: [
1546
+ ["show | list", "Print the project's areas and roles."],
1547
+ ["area add \"<name>\"", "Create an area. --slug, --goal optional."],
1548
+ ["area rm <slug>", "Remove an area (its roles are detached, not deleted)."],
1549
+ ["role add \"<name>\"", "Create a role. --slug, --area, --desc optional."],
1550
+ ["role rm <slug>", "Remove a role."],
1551
+ ],
1552
+ options: [["--project <name|id|path>", "Pin command to a specific project."]],
1553
+ examples: [
1554
+ "apx org area add \"Engineering\" --goal \"Build the product\"",
1555
+ "apx org role add \"Tech Lead\" --area engineering",
1556
+ "apx org show",
1557
+ ],
1558
+ }),
1534
1559
  "task add": topic({
1535
1560
  title: "apx task add",
1536
1561
  summary: "Create a task on a project's TODO list.",
@@ -2604,6 +2629,29 @@ async function dispatch(cmd, rest) {
2604
2629
  break;
2605
2630
  }
2606
2631
 
2632
+ case "org":
2633
+ case "organization": {
2634
+ const sub = rest[0];
2635
+ const a = parseArgs(rest.slice(1));
2636
+ // `apx org area add ...` / `apx org role rm ...` — the resource verb is
2637
+ // the first positional, the action the second.
2638
+ if (!sub || sub === "show" || sub === "list") await cmdOrgShow(a);
2639
+ else if (sub === "area") {
2640
+ const action = rest[1];
2641
+ const aa = parseArgs(rest.slice(1)); // keep `area` as _[0] for name parsing
2642
+ if (action === "add" || action === "new") await cmdOrgAreaAdd(aa);
2643
+ else if (action === "rm" || action === "remove" || action === "delete") await cmdOrgAreaRm(aa);
2644
+ else die("usage: apx org area <add|rm> ...");
2645
+ } else if (sub === "role") {
2646
+ const action = rest[1];
2647
+ const ra = parseArgs(rest.slice(1));
2648
+ if (action === "add" || action === "new") await cmdOrgRoleAdd(ra);
2649
+ else if (action === "rm" || action === "remove" || action === "delete") await cmdOrgRoleRm(ra);
2650
+ else die("usage: apx org role <add|rm> ...");
2651
+ } else die(`unknown org subcommand: ${sub}\nUsage: apx org <show|area|role> ...`);
2652
+ break;
2653
+ }
2654
+
2607
2655
  case "skills": {
2608
2656
  const sub = rest[0];
2609
2657
  const a = parseArgs(rest.slice(1));