ltcai 3.1.0 → 3.3.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 (54) hide show
  1. package/README.md +35 -8
  2. package/docs/CHANGELOG.md +53 -0
  3. package/docs/V3_2_AUDIT.md +82 -0
  4. package/docs/V3_FRONTEND.md +1 -1
  5. package/docs/architecture.md +6 -0
  6. package/latticeai/__init__.py +1 -1
  7. package/latticeai/api/agent_registry.py +103 -0
  8. package/latticeai/api/hooks.py +113 -0
  9. package/latticeai/api/marketplace.py +13 -0
  10. package/latticeai/api/memory.py +109 -0
  11. package/latticeai/core/agent_registry.py +234 -0
  12. package/latticeai/core/hooks.py +284 -0
  13. package/latticeai/core/marketplace.py +87 -2
  14. package/latticeai/core/multi_agent.py +1 -1
  15. package/latticeai/core/workspace_os.py +1 -1
  16. package/latticeai/server_app.py +41 -0
  17. package/latticeai/services/memory_service.py +324 -0
  18. package/package.json +2 -2
  19. package/scripts/build_v3_assets.mjs +7 -1
  20. package/static/css/{tokens.5a595671.css → tokens.8b8e31bd.css} +1 -1
  21. package/static/css/tokens.css +1 -1
  22. package/static/v3/asset-manifest.json +22 -14
  23. package/static/v3/css/{lattice.views.3ee19d4e.css → lattice.views.1d326beb.css} +5 -0
  24. package/static/v3/css/lattice.views.css +5 -0
  25. package/static/v3/js/{app.46fb61d9.js → app.cf5bb712.js} +1 -1
  26. package/static/v3/js/core/{api.22a41d42.js → api.113660c5.js} +123 -4
  27. package/static/v3/js/core/api.js +123 -4
  28. package/static/v3/js/core/{routes.f935dd50.js → routes.07ad6696.js} +11 -0
  29. package/static/v3/js/core/routes.js +11 -0
  30. package/static/v3/js/core/{shell.1b6199d6.js → shell.9e707234.js} +2 -2
  31. package/static/v3/js/views/{agents.14e48bdd.js → agents.c373d48c.js} +100 -0
  32. package/static/v3/js/views/agents.js +100 -0
  33. package/static/v3/js/views/{chat.718144ce.js → chat.c48fd9e2.js} +1 -1
  34. package/static/v3/js/views/chat.js +1 -1
  35. package/static/v3/js/views/{files.4935197e.js → files.8464634a.js} +64 -18
  36. package/static/v3/js/views/files.js +64 -18
  37. package/static/v3/js/views/hooks.f3edebca.js +99 -0
  38. package/static/v3/js/views/hooks.js +99 -0
  39. package/static/v3/js/views/marketplace.ab0583d4.js +141 -0
  40. package/static/v3/js/views/marketplace.js +141 -0
  41. package/static/v3/js/views/mcp.99b5c6a7.js +114 -0
  42. package/static/v3/js/views/mcp.js +114 -0
  43. package/static/v3/js/views/memory.4ebdf474.js +147 -0
  44. package/static/v3/js/views/memory.js +147 -0
  45. package/static/v3/js/views/planning.9ac3e313.js +153 -0
  46. package/static/v3/js/views/planning.js +153 -0
  47. package/static/v3/js/views/{settings.4f777210.js → settings.c7b0cc05.js} +16 -2
  48. package/static/v3/js/views/settings.js +16 -2
  49. package/static/v3/js/views/skills.c6c2f965.js +109 -0
  50. package/static/v3/js/views/skills.js +109 -0
  51. package/static/v3/js/views/tools.e4f11276.js +108 -0
  52. package/static/v3/js/views/tools.js +108 -0
  53. package/static/v3/js/views/workflows.26c57290.js +128 -0
  54. package/static/v3/js/views/workflows.js +128 -0
@@ -82,9 +82,35 @@ export const api = {
82
82
 
83
83
  /* ── Documented future surfaces ─────────────────────────────────────── */
84
84
 
85
- /** GET /api/index/status — KG + Vector + Hybrid pipeline state. */
86
- indexStatus() {
87
- return withFallback("/api/index/status", {}, EMPTY_INDEX_STATUS);
85
+ /** GET /api/index/status — KG + Vector + Hybrid pipeline state.
86
+ * The backend endpoint is vector-centric (status/storage/source_items/…); the
87
+ * home pillars + topbar chip want a `pipelines` view keyed by
88
+ * knowledge_graph / vector_index / hybrid. Synthesize that shape from the real
89
+ * index status (vectors) plus the KG stats endpoint (entities). Nothing is
90
+ * fabricated: if the index endpoint is unavailable we report unavailable (so
91
+ * the UI shows the honest empty state), and a missing graph-stats count yields
92
+ * an "unavailable" graph pillar rather than a fake number. */
93
+ async indexStatus() {
94
+ const res = await raw("/api/index/status");
95
+ if (!(res.ok && res.data && !res.data.raw)) {
96
+ return { ok: false, status: res.status, data: EMPTY_INDEX_STATUS, source: "unavailable", error: res.error };
97
+ }
98
+ const idx = res.data;
99
+ let entities = null;
100
+ const gs = await raw("/knowledge-graph/stats");
101
+ if (gs.ok && gs.data && !gs.data.raw) {
102
+ const g = gs.data;
103
+ const n = g.total_nodes ?? g.nodes_total ?? (g.nodes && (g.nodes.total ?? g.nodes.count));
104
+ if (n !== undefined && n !== null) entities = Number(n) || 0;
105
+ }
106
+ const vectors = Number(idx.indexed_items ?? idx.ready_items) || 0;
107
+ const vstate = idx.status === "ready" ? "ready" : "pending";
108
+ const pipelines = {
109
+ knowledge_graph: { state: entities === null ? "unavailable" : "ready", entities: entities ?? 0 },
110
+ vector_index: { state: vstate, vectors },
111
+ hybrid: { state: vstate, strategy: vstate === "ready" ? "fused" : "pending" },
112
+ };
113
+ return { ok: true, status: res.status, data: { ...idx, pipelines }, source: "live" };
88
114
  },
89
115
 
90
116
  /** POST /api/index/rebuild — rebuild the derived vector index (real run). */
@@ -176,6 +202,31 @@ export const api = {
176
202
  },
177
203
  sysinfo() { return withFallback("/local/sysinfo", {}, EMPTY_SYSINFO); },
178
204
 
205
+ /** POST /upload/document — manual document ingest (multipart/form-data).
206
+ * Real backend path: parse → chunk → embed → knowledge-graph ingest
207
+ * (latticeai/api/tools.py:/upload/document). Returns { ok, status, data,
208
+ * source }; never throws. FormData must NOT carry a JSON Content-Type — the
209
+ * browser sets the multipart boundary itself. */
210
+ async uploadDocument(file) {
211
+ const ws = store.get().workspaceId;
212
+ const form = new FormData();
213
+ form.append("file", file);
214
+ try {
215
+ const res = await fetch("/upload/document", {
216
+ method: "POST",
217
+ credentials: "same-origin",
218
+ headers: { "Accept": "application/json", ...(ws ? { "X-Workspace-Id": ws } : {}) },
219
+ body: form,
220
+ });
221
+ let data = null;
222
+ const text = await res.text();
223
+ if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
224
+ return { ok: res.ok, status: res.status, data, source: res.ok ? "live" : "unavailable" };
225
+ } catch (err) {
226
+ return { ok: false, status: 0, data: null, source: "unavailable", error: String(err) };
227
+ }
228
+ },
229
+
179
230
  adminSummary() { return withFallback("/admin/summary", {}, EMPTY_ADMIN.summary); },
180
231
  adminUsers() { return withFallback("/admin/users", {}, EMPTY_ADMIN.users); },
181
232
  adminAudit() { return withFallback("/admin/audit", {}, EMPTY_ADMIN.audit); },
@@ -310,7 +361,11 @@ export const api = {
310
361
  const rawData = line.slice(5).trim();
311
362
  if (rawData === "[DONE]") return { source: "live", text, trace, model };
312
363
  let data; try { data = JSON.parse(rawData); } catch { continue; }
313
- if (data.chunk) { text += data.chunk; onChunk && onChunk(data.chunk, text); }
364
+ // Standard chat streams `chunk`; the document-generation path streams
365
+ // `text` (report body + footnotes). Accept both so doc requests render
366
+ // instead of falsely reporting the backend as unreachable.
367
+ const delta = data.chunk || data.text;
368
+ if (delta) { text += delta; onChunk && onChunk(delta, text); }
314
369
  if (data.model) model = data.model;
315
370
  if (data.trace) { trace = data.trace; onTrace && onTrace(trace); }
316
371
  }
@@ -321,6 +376,70 @@ export const api = {
321
376
  }
322
377
  return { source: "live", text, trace, model };
323
378
  },
379
+
380
+ /* ── v3.2 platform surfaces (all fallback-safe; never fabricate) ─────── */
381
+
382
+ // Agent Registry (Part 2)
383
+ agentRegistry(type) { return withFallback(`/agents/api/registry${type ? "?type=" + encodeURIComponent(type) : ""}`, {}, { agents: [], counts: {}, types: [] }); },
384
+ agentCapabilities() { return withFallback("/agents/api/registry/capabilities", {}, { capabilities: {} }); },
385
+ registerAgent(body) { return raw("/agents/api/registry", { method: "POST", body }); },
386
+ updateAgent(id, body) { return raw(`/agents/api/registry/${encodeURIComponent(id)}`, { method: "PATCH", body }); },
387
+ removeAgent(id) { return raw(`/agents/api/registry/${encodeURIComponent(id)}`, { method: "DELETE" }); },
388
+ agentRunDetail(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}`); },
389
+ agentRunReplay(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}/replay`); },
390
+ stopAgentRun(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}/stop`, { method: "POST" }); },
391
+
392
+ // Marketplace + Templates (Parts 3, 4)
393
+ templates(kind) { return withFallback(`/marketplace/templates${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { templates: [], kinds: [] }); },
394
+ templateRegistry() { return withFallback("/marketplace/templates/registry", {}, { registry: [] }); },
395
+ exportTemplate(kind, id) { return raw(`/marketplace/templates/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/export`); },
396
+ importTemplate(data) { return raw("/marketplace/templates/import", { method: "POST", body: { data } }); },
397
+ installTemplate(data) { return raw("/marketplace/templates/install", { method: "POST", body: { data } }); },
398
+ cloneTemplate(kind, id, name) { return raw(`/marketplace/templates/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/clone`, { method: "POST", body: { name } }); },
399
+ pluginsRegistry() { return withFallback("/plugins/registry", {}, { plugins: [] }); },
400
+ pluginsDirectory() { return withFallback("/plugins/directory", {}, { plugins: [], categories: [] }); },
401
+
402
+ // Workflow Agents (Part 5)
403
+ workflowDefinitions() { return withFallback("/workflows/api/definitions", {}, { workflows: [] }); },
404
+ createWorkflow(body) { return raw("/workflows/api/definitions", { method: "POST", body }); },
405
+ runWorkflow(id, body = {}) { return raw(`/workflows/api/definitions/${encodeURIComponent(id)}/run`, { method: "POST", body }); },
406
+ workflowRuns() { return withFallback("/workflows/api/runs", {}, { runs: [] }); },
407
+ workflowReplay(runId) { return raw(`/workflows/api/runs/${encodeURIComponent(runId)}/replay`); },
408
+
409
+ // Long-Term Memory + Memory Manager (Parts 7, 8)
410
+ memoryManager() { return withFallback("/api/memory/manager", {}, { sources: [], tiers: [], usage: {} }); },
411
+ memoryTiers() { return withFallback("/api/memory/tiers", {}, { tiers: [], workspace_kinds: [] }); },
412
+ memoryInspect(source, limit = 50) { return withFallback(`/api/memory/inspect?source=${encodeURIComponent(source)}&limit=${limit}`, {}, { items: [] }); },
413
+ memoryRecall(query, limit = 20) { return raw("/api/memory/recall", { method: "POST", body: { query, limit } }); },
414
+ memoryPrune(body) { return raw("/api/memory/prune", { method: "POST", body }); },
415
+ memoryCompact() { return raw("/api/memory/compact", { method: "POST", body: {} }); },
416
+ memoryRebuild(target = "vector") { return raw("/api/memory/rebuild", { method: "POST", body: { target } }); },
417
+ memoryClear(scope, confirm = true) { return raw("/api/memory/clear", { method: "POST", body: { scope, confirm } }); },
418
+ workspaceMemories(kind) { return withFallback(`/workspace/memories${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { memories: [] }); },
419
+
420
+ // Skills Registry (Part 9)
421
+ skills() { return withFallback("/workspace/skills", {}, { skills: [] }); },
422
+ skillEnable(skill) { return raw("/workspace/skills/enable", { method: "POST", body: { skill } }); },
423
+ skillDisable(skill) { return raw("/workspace/skills/disable", { method: "POST", body: { skill } }); },
424
+ skillInstall(skill, plugin) { return raw("/workspace/skills/install", { method: "POST", body: { skill, plugin: plugin || "" } }); },
425
+ skillUninstall(skill) { return raw("/workspace/skills/uninstall", { method: "POST", body: { skill } }); },
426
+ skillsMarketplace() { return withFallback("/skills/marketplace", {}, { skills: [], categories: [] }); },
427
+
428
+ // Hooks Registry (Part 10)
429
+ hooks(kind) { return withFallback(`/api/hooks${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { hooks: [], kinds: [], counts: {} }); },
430
+ hookEnable(hook_id, enabled = true) { return raw("/api/hooks/enable", { method: "POST", body: { hook_id, enabled } }); },
431
+ hookDisable(hook_id) { return raw("/api/hooks/disable", { method: "POST", body: { hook_id, enabled: false } }); },
432
+ hookReorder(kind, ordered_ids) { return raw("/api/hooks/reorder", { method: "POST", body: { kind, ordered_ids } }); },
433
+ hookRegister(body) { return raw("/api/hooks/register", { method: "POST", body }); },
434
+ hookRemove(hook_id) { return raw(`/api/hooks/${encodeURIComponent(hook_id)}`, { method: "DELETE" }); },
435
+
436
+ // Tool Registry + MCP (Parts 11, 12)
437
+ toolPermissions() { return withFallback("/tools/permissions", {}, { permissions: [] }); },
438
+ mcpTools() { return withFallback("/mcp/tools", {}, { tools: [], installed_mcps: [] }); },
439
+ mcpInstalled() { return withFallback("/mcp/installed", {}, { installed: [] }); },
440
+ mcpClaudeServers() { return withFallback("/mcp/claude-code-servers", {}, { servers: [] }); },
441
+ mcpCustom() { return withFallback("/mcp/custom", {}, { custom: [] }); },
442
+ mcpRecommend(query, limit = 6) { return raw("/mcp/recommend", { method: "POST", body: { query, limit } }); },
324
443
  };
325
444
 
326
445
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -82,9 +82,35 @@ export const api = {
82
82
 
83
83
  /* ── Documented future surfaces ─────────────────────────────────────── */
84
84
 
85
- /** GET /api/index/status — KG + Vector + Hybrid pipeline state. */
86
- indexStatus() {
87
- return withFallback("/api/index/status", {}, EMPTY_INDEX_STATUS);
85
+ /** GET /api/index/status — KG + Vector + Hybrid pipeline state.
86
+ * The backend endpoint is vector-centric (status/storage/source_items/…); the
87
+ * home pillars + topbar chip want a `pipelines` view keyed by
88
+ * knowledge_graph / vector_index / hybrid. Synthesize that shape from the real
89
+ * index status (vectors) plus the KG stats endpoint (entities). Nothing is
90
+ * fabricated: if the index endpoint is unavailable we report unavailable (so
91
+ * the UI shows the honest empty state), and a missing graph-stats count yields
92
+ * an "unavailable" graph pillar rather than a fake number. */
93
+ async indexStatus() {
94
+ const res = await raw("/api/index/status");
95
+ if (!(res.ok && res.data && !res.data.raw)) {
96
+ return { ok: false, status: res.status, data: EMPTY_INDEX_STATUS, source: "unavailable", error: res.error };
97
+ }
98
+ const idx = res.data;
99
+ let entities = null;
100
+ const gs = await raw("/knowledge-graph/stats");
101
+ if (gs.ok && gs.data && !gs.data.raw) {
102
+ const g = gs.data;
103
+ const n = g.total_nodes ?? g.nodes_total ?? (g.nodes && (g.nodes.total ?? g.nodes.count));
104
+ if (n !== undefined && n !== null) entities = Number(n) || 0;
105
+ }
106
+ const vectors = Number(idx.indexed_items ?? idx.ready_items) || 0;
107
+ const vstate = idx.status === "ready" ? "ready" : "pending";
108
+ const pipelines = {
109
+ knowledge_graph: { state: entities === null ? "unavailable" : "ready", entities: entities ?? 0 },
110
+ vector_index: { state: vstate, vectors },
111
+ hybrid: { state: vstate, strategy: vstate === "ready" ? "fused" : "pending" },
112
+ };
113
+ return { ok: true, status: res.status, data: { ...idx, pipelines }, source: "live" };
88
114
  },
89
115
 
90
116
  /** POST /api/index/rebuild — rebuild the derived vector index (real run). */
@@ -176,6 +202,31 @@ export const api = {
176
202
  },
177
203
  sysinfo() { return withFallback("/local/sysinfo", {}, EMPTY_SYSINFO); },
178
204
 
205
+ /** POST /upload/document — manual document ingest (multipart/form-data).
206
+ * Real backend path: parse → chunk → embed → knowledge-graph ingest
207
+ * (latticeai/api/tools.py:/upload/document). Returns { ok, status, data,
208
+ * source }; never throws. FormData must NOT carry a JSON Content-Type — the
209
+ * browser sets the multipart boundary itself. */
210
+ async uploadDocument(file) {
211
+ const ws = store.get().workspaceId;
212
+ const form = new FormData();
213
+ form.append("file", file);
214
+ try {
215
+ const res = await fetch("/upload/document", {
216
+ method: "POST",
217
+ credentials: "same-origin",
218
+ headers: { "Accept": "application/json", ...(ws ? { "X-Workspace-Id": ws } : {}) },
219
+ body: form,
220
+ });
221
+ let data = null;
222
+ const text = await res.text();
223
+ if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
224
+ return { ok: res.ok, status: res.status, data, source: res.ok ? "live" : "unavailable" };
225
+ } catch (err) {
226
+ return { ok: false, status: 0, data: null, source: "unavailable", error: String(err) };
227
+ }
228
+ },
229
+
179
230
  adminSummary() { return withFallback("/admin/summary", {}, EMPTY_ADMIN.summary); },
180
231
  adminUsers() { return withFallback("/admin/users", {}, EMPTY_ADMIN.users); },
181
232
  adminAudit() { return withFallback("/admin/audit", {}, EMPTY_ADMIN.audit); },
@@ -310,7 +361,11 @@ export const api = {
310
361
  const rawData = line.slice(5).trim();
311
362
  if (rawData === "[DONE]") return { source: "live", text, trace, model };
312
363
  let data; try { data = JSON.parse(rawData); } catch { continue; }
313
- if (data.chunk) { text += data.chunk; onChunk && onChunk(data.chunk, text); }
364
+ // Standard chat streams `chunk`; the document-generation path streams
365
+ // `text` (report body + footnotes). Accept both so doc requests render
366
+ // instead of falsely reporting the backend as unreachable.
367
+ const delta = data.chunk || data.text;
368
+ if (delta) { text += delta; onChunk && onChunk(delta, text); }
314
369
  if (data.model) model = data.model;
315
370
  if (data.trace) { trace = data.trace; onTrace && onTrace(trace); }
316
371
  }
@@ -321,6 +376,70 @@ export const api = {
321
376
  }
322
377
  return { source: "live", text, trace, model };
323
378
  },
379
+
380
+ /* ── v3.2 platform surfaces (all fallback-safe; never fabricate) ─────── */
381
+
382
+ // Agent Registry (Part 2)
383
+ agentRegistry(type) { return withFallback(`/agents/api/registry${type ? "?type=" + encodeURIComponent(type) : ""}`, {}, { agents: [], counts: {}, types: [] }); },
384
+ agentCapabilities() { return withFallback("/agents/api/registry/capabilities", {}, { capabilities: {} }); },
385
+ registerAgent(body) { return raw("/agents/api/registry", { method: "POST", body }); },
386
+ updateAgent(id, body) { return raw(`/agents/api/registry/${encodeURIComponent(id)}`, { method: "PATCH", body }); },
387
+ removeAgent(id) { return raw(`/agents/api/registry/${encodeURIComponent(id)}`, { method: "DELETE" }); },
388
+ agentRunDetail(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}`); },
389
+ agentRunReplay(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}/replay`); },
390
+ stopAgentRun(runId) { return raw(`/agents/api/runs/${encodeURIComponent(runId)}/stop`, { method: "POST" }); },
391
+
392
+ // Marketplace + Templates (Parts 3, 4)
393
+ templates(kind) { return withFallback(`/marketplace/templates${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { templates: [], kinds: [] }); },
394
+ templateRegistry() { return withFallback("/marketplace/templates/registry", {}, { registry: [] }); },
395
+ exportTemplate(kind, id) { return raw(`/marketplace/templates/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/export`); },
396
+ importTemplate(data) { return raw("/marketplace/templates/import", { method: "POST", body: { data } }); },
397
+ installTemplate(data) { return raw("/marketplace/templates/install", { method: "POST", body: { data } }); },
398
+ cloneTemplate(kind, id, name) { return raw(`/marketplace/templates/${encodeURIComponent(kind)}/${encodeURIComponent(id)}/clone`, { method: "POST", body: { name } }); },
399
+ pluginsRegistry() { return withFallback("/plugins/registry", {}, { plugins: [] }); },
400
+ pluginsDirectory() { return withFallback("/plugins/directory", {}, { plugins: [], categories: [] }); },
401
+
402
+ // Workflow Agents (Part 5)
403
+ workflowDefinitions() { return withFallback("/workflows/api/definitions", {}, { workflows: [] }); },
404
+ createWorkflow(body) { return raw("/workflows/api/definitions", { method: "POST", body }); },
405
+ runWorkflow(id, body = {}) { return raw(`/workflows/api/definitions/${encodeURIComponent(id)}/run`, { method: "POST", body }); },
406
+ workflowRuns() { return withFallback("/workflows/api/runs", {}, { runs: [] }); },
407
+ workflowReplay(runId) { return raw(`/workflows/api/runs/${encodeURIComponent(runId)}/replay`); },
408
+
409
+ // Long-Term Memory + Memory Manager (Parts 7, 8)
410
+ memoryManager() { return withFallback("/api/memory/manager", {}, { sources: [], tiers: [], usage: {} }); },
411
+ memoryTiers() { return withFallback("/api/memory/tiers", {}, { tiers: [], workspace_kinds: [] }); },
412
+ memoryInspect(source, limit = 50) { return withFallback(`/api/memory/inspect?source=${encodeURIComponent(source)}&limit=${limit}`, {}, { items: [] }); },
413
+ memoryRecall(query, limit = 20) { return raw("/api/memory/recall", { method: "POST", body: { query, limit } }); },
414
+ memoryPrune(body) { return raw("/api/memory/prune", { method: "POST", body }); },
415
+ memoryCompact() { return raw("/api/memory/compact", { method: "POST", body: {} }); },
416
+ memoryRebuild(target = "vector") { return raw("/api/memory/rebuild", { method: "POST", body: { target } }); },
417
+ memoryClear(scope, confirm = true) { return raw("/api/memory/clear", { method: "POST", body: { scope, confirm } }); },
418
+ workspaceMemories(kind) { return withFallback(`/workspace/memories${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { memories: [] }); },
419
+
420
+ // Skills Registry (Part 9)
421
+ skills() { return withFallback("/workspace/skills", {}, { skills: [] }); },
422
+ skillEnable(skill) { return raw("/workspace/skills/enable", { method: "POST", body: { skill } }); },
423
+ skillDisable(skill) { return raw("/workspace/skills/disable", { method: "POST", body: { skill } }); },
424
+ skillInstall(skill, plugin) { return raw("/workspace/skills/install", { method: "POST", body: { skill, plugin: plugin || "" } }); },
425
+ skillUninstall(skill) { return raw("/workspace/skills/uninstall", { method: "POST", body: { skill } }); },
426
+ skillsMarketplace() { return withFallback("/skills/marketplace", {}, { skills: [], categories: [] }); },
427
+
428
+ // Hooks Registry (Part 10)
429
+ hooks(kind) { return withFallback(`/api/hooks${kind ? "?kind=" + encodeURIComponent(kind) : ""}`, {}, { hooks: [], kinds: [], counts: {} }); },
430
+ hookEnable(hook_id, enabled = true) { return raw("/api/hooks/enable", { method: "POST", body: { hook_id, enabled } }); },
431
+ hookDisable(hook_id) { return raw("/api/hooks/disable", { method: "POST", body: { hook_id, enabled: false } }); },
432
+ hookReorder(kind, ordered_ids) { return raw("/api/hooks/reorder", { method: "POST", body: { kind, ordered_ids } }); },
433
+ hookRegister(body) { return raw("/api/hooks/register", { method: "POST", body }); },
434
+ hookRemove(hook_id) { return raw(`/api/hooks/${encodeURIComponent(hook_id)}`, { method: "DELETE" }); },
435
+
436
+ // Tool Registry + MCP (Parts 11, 12)
437
+ toolPermissions() { return withFallback("/tools/permissions", {}, { permissions: [] }); },
438
+ mcpTools() { return withFallback("/mcp/tools", {}, { tools: [], installed_mcps: [] }); },
439
+ mcpInstalled() { return withFallback("/mcp/installed", {}, { installed: [] }); },
440
+ mcpClaudeServers() { return withFallback("/mcp/claude-code-servers", {}, { servers: [] }); },
441
+ mcpCustom() { return withFallback("/mcp/custom", {}, { custom: [] }); },
442
+ mcpRecommend(query, limit = 6) { return raw("/mcp/recommend", { method: "POST", body: { query, limit } }); },
324
443
  };
325
444
 
326
445
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -14,6 +14,7 @@ export const GROUPS = [
14
14
  { id: "retrieval", label: "Retrieval" },
15
15
  { id: "data", label: "Data" },
16
16
  { id: "compute", label: "Compute" },
17
+ { id: "platform", label: "Platform" },
17
18
  { id: "system", label: "System" },
18
19
  { id: "admin", label: "Administration", adminOnly: true },
19
20
  ];
@@ -30,6 +31,7 @@ export const ROUTES = [
30
31
  // Retrieval (the product identity)
31
32
  { key: "knowledge-graph", label: "Knowledge Graph", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "Entities and relations extracted from your workspace." },
32
33
  { key: "hybrid-search", label: "Hybrid Search", icon: "arrows-join", group: "retrieval", minMode: "basic", view: "hybrid-search", title: "Hybrid Search", desc: "Graph structure fused with vector similarity." },
34
+ { key: "memory", label: "Memory", icon: "brain", group: "retrieval", minMode: "basic", view: "memory", title: "Memory", desc: "Long-term workspace, project, agent, and conversation memory." },
33
35
 
34
36
  // Data
35
37
  { key: "files", label: "Files", icon: "folders", group: "data", minMode: "basic", view: "files", title: "Files", desc: "Connected sources and indexed documents." },
@@ -37,9 +39,18 @@ export const ROUTES = [
37
39
 
38
40
  // Compute
39
41
  { key: "agents", label: "Agents", icon: "robot", group: "compute", minMode: "advanced", view: "agents", title: "Agents", desc: "Multi-agent roles, runs, and handoffs." },
42
+ { key: "workflows", label: "Workflows", icon: "sitemap", group: "compute", minMode: "advanced", view: "workflows", title: "Workflow Agents", desc: "Trigger → agent chain → tools → memory → result." },
43
+ { key: "planning", label: "Planning", icon: "target-arrow", group: "compute", minMode: "advanced", view: "planning", title: "Autonomous Planning", desc: "Goal → plan → execute → review → replan." },
40
44
  { key: "models", label: "Models", icon: "cpu", group: "compute", minMode: "basic", view: "models", title: "Models", desc: "Local MLX models and embeddings." },
41
45
  { key: "my-computer", label: "My Computer", icon: "device-desktop-analytics", group: "compute", minMode: "advanced", view: "my-computer", title: "My Computer", desc: "Local hardware, memory, and runtime." },
42
46
 
47
+ // Platform (the agent ecosystem)
48
+ { key: "marketplace", label: "Marketplace", icon: "building-store", group: "platform", minMode: "advanced", view: "marketplace", title: "Marketplace", desc: "Agent templates, agents, plugins, and skills." },
49
+ { key: "skills", label: "Skills", icon: "puzzle", group: "platform", minMode: "advanced", view: "skills", title: "Skills", desc: "Install, enable, and manage skills." },
50
+ { key: "hooks", label: "Hooks", icon: "webhook", group: "platform", minMode: "advanced", view: "hooks", title: "Hooks", desc: "Lifecycle hooks across runs, tools, and workflows." },
51
+ { key: "tools", label: "Tools", icon: "tools", group: "platform", minMode: "advanced", view: "tools", title: "Tool Registry", desc: "Local, workspace, and MCP tools with governance." },
52
+ { key: "mcp", label: "MCP", icon: "plug-connected", group: "platform", minMode: "advanced", view: "mcp", title: "MCP Manager", desc: "Connected MCP servers, available tools, and health." },
53
+
43
54
  // System
44
55
  { key: "settings", label: "Settings", icon: "settings", group: "system", minMode: "basic", view: "settings", title: "Settings", desc: "Appearance, workspace, and integrations." },
45
56
 
@@ -14,6 +14,7 @@ export const GROUPS = [
14
14
  { id: "retrieval", label: "Retrieval" },
15
15
  { id: "data", label: "Data" },
16
16
  { id: "compute", label: "Compute" },
17
+ { id: "platform", label: "Platform" },
17
18
  { id: "system", label: "System" },
18
19
  { id: "admin", label: "Administration", adminOnly: true },
19
20
  ];
@@ -30,6 +31,7 @@ export const ROUTES = [
30
31
  // Retrieval (the product identity)
31
32
  { key: "knowledge-graph", label: "Knowledge Graph", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "Entities and relations extracted from your workspace." },
32
33
  { key: "hybrid-search", label: "Hybrid Search", icon: "arrows-join", group: "retrieval", minMode: "basic", view: "hybrid-search", title: "Hybrid Search", desc: "Graph structure fused with vector similarity." },
34
+ { key: "memory", label: "Memory", icon: "brain", group: "retrieval", minMode: "basic", view: "memory", title: "Memory", desc: "Long-term workspace, project, agent, and conversation memory." },
33
35
 
34
36
  // Data
35
37
  { key: "files", label: "Files", icon: "folders", group: "data", minMode: "basic", view: "files", title: "Files", desc: "Connected sources and indexed documents." },
@@ -37,9 +39,18 @@ export const ROUTES = [
37
39
 
38
40
  // Compute
39
41
  { key: "agents", label: "Agents", icon: "robot", group: "compute", minMode: "advanced", view: "agents", title: "Agents", desc: "Multi-agent roles, runs, and handoffs." },
42
+ { key: "workflows", label: "Workflows", icon: "sitemap", group: "compute", minMode: "advanced", view: "workflows", title: "Workflow Agents", desc: "Trigger → agent chain → tools → memory → result." },
43
+ { key: "planning", label: "Planning", icon: "target-arrow", group: "compute", minMode: "advanced", view: "planning", title: "Autonomous Planning", desc: "Goal → plan → execute → review → replan." },
40
44
  { key: "models", label: "Models", icon: "cpu", group: "compute", minMode: "basic", view: "models", title: "Models", desc: "Local MLX models and embeddings." },
41
45
  { key: "my-computer", label: "My Computer", icon: "device-desktop-analytics", group: "compute", minMode: "advanced", view: "my-computer", title: "My Computer", desc: "Local hardware, memory, and runtime." },
42
46
 
47
+ // Platform (the agent ecosystem)
48
+ { key: "marketplace", label: "Marketplace", icon: "building-store", group: "platform", minMode: "advanced", view: "marketplace", title: "Marketplace", desc: "Agent templates, agents, plugins, and skills." },
49
+ { key: "skills", label: "Skills", icon: "puzzle", group: "platform", minMode: "advanced", view: "skills", title: "Skills", desc: "Install, enable, and manage skills." },
50
+ { key: "hooks", label: "Hooks", icon: "webhook", group: "platform", minMode: "advanced", view: "hooks", title: "Hooks", desc: "Lifecycle hooks across runs, tools, and workflows." },
51
+ { key: "tools", label: "Tools", icon: "tools", group: "platform", minMode: "advanced", view: "tools", title: "Tool Registry", desc: "Local, workspace, and MCP tools with governance." },
52
+ { key: "mcp", label: "MCP", icon: "plug-connected", group: "platform", minMode: "advanced", view: "mcp", title: "MCP Manager", desc: "Connected MCP servers, available tools, and health." },
53
+
43
54
  // System
44
55
  { key: "settings", label: "Settings", icon: "settings", group: "system", minMode: "basic", view: "settings", title: "Settings", desc: "Appearance, workspace, and integrations." },
45
56
 
@@ -7,10 +7,10 @@
7
7
 
8
8
  import { h, icon, $, $$ } from "./dom.a2773eb0.js";
9
9
  import { store } from "./store.34ebd5e6.js";
10
- import { api } from "./api.22a41d42.js";
10
+ import { api } from "./api.113660c5.js";
11
11
  import * as c from "./components.4c83e0a9.js";
12
12
  import { createRouter } from "./router.584570f2.js";
13
- import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.f935dd50.js";
13
+ import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.07ad6696.js";
14
14
 
15
15
  const MODES = [
16
16
  { key: "basic", label: "Basic", icon: "circle" },
@@ -9,8 +9,10 @@ export async function render(ctx) {
9
9
  const { h, icon, c } = ctx;
10
10
 
11
11
  const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
12
+ const registryHost = h("div", c.loading({ lines: 3, block: true }));
12
13
  const rosterHost = h("div", c.loading({ lines: 2, block: true }));
13
14
  const runsHost = h("div", c.loading({ lines: 4 }));
15
+ const registrySrc = h("span", c.sourceBadge("pending"));
14
16
  const rosterSrc = h("span", c.sourceBadge("pending"));
15
17
  const runsSrc = h("span", c.sourceBadge("pending"));
16
18
  const healthSlot = h("span", c.sourceBadge("pending"));
@@ -23,6 +25,10 @@ export async function render(ctx) {
23
25
  actions: [healthSlot],
24
26
  }),
25
27
  statHost,
28
+ h("section",
29
+ c.sectionHead("Agent Registry", registrySrc),
30
+ registryHost,
31
+ ),
26
32
  h("section",
27
33
  c.sectionHead("Agent roster", rosterSrc),
28
34
  rosterHost,
@@ -40,6 +46,7 @@ export async function render(ctx) {
40
46
  );
41
47
 
42
48
  hydrate(ctx, { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot });
49
+ loadRegistry(ctx, { registryHost, registrySrc });
43
50
  return root;
44
51
  }
45
52
 
@@ -109,6 +116,83 @@ async function hydrate(ctx, hosts) {
109
116
  );
110
117
  }
111
118
 
119
+ async function loadRegistry(ctx, hosts) {
120
+ const { h, c } = ctx;
121
+ const { registryHost, registrySrc } = hosts;
122
+ const [registryRes, capsRes] = await Promise.all([ctx.api.agentRegistry(), ctx.api.agentCapabilities()]);
123
+ const agents = normalizeRegistry(registryRes.data);
124
+ const caps = (capsRes.data && capsRes.data.capabilities) || {};
125
+ registrySrc.replaceChildren(c.sourceBadge(registryRes.source === "live" || capsRes.source === "live" ? "live" : "unavailable"));
126
+
127
+ const nameInput = h("input.lt3-input", { type: "text", placeholder: "Custom agent name", "aria-label": "Custom agent name" });
128
+ const capsInput = h("input.lt3-input", { type: "text", placeholder: "capability-a, capability-b", "aria-label": "Custom agent capabilities" });
129
+ const registerBtn = h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: register } }, c.icon("plus"), "Register");
130
+
131
+ const capList = Object.keys(caps).sort();
132
+ const body = h("div.lt3-stack-4",
133
+ h("div.lt3-grid-2",
134
+ h("div.lt3-field", h("label", "Name"), nameInput),
135
+ h("div.lt3-field", h("label", "Capabilities"), capsInput),
136
+ ),
137
+ h("div.lt3-row-2", registerBtn,
138
+ h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Custom agents persist in the local registry.")),
139
+ capList.length
140
+ ? h("div.lt3-cluster", capList.slice(0, 18).map((cap) => h("span.lt3-chip", c.icon("sparkles"), `${cap} (${caps[cap].length})`)))
141
+ : h("p.lt3-faint", { style: { margin: 0 } }, "Capabilities appear here when the registry is live."),
142
+ agents.length
143
+ ? h("div.lt3-grid-auto", agents.map((agent) => registryCard(ctx, agent)))
144
+ : c.emptyState({ icon: "robot-off", title: "Agent registry unavailable", body: "Start the local server to register and configure agents." }),
145
+ );
146
+ registryHost.replaceChildren(c.panel({ title: "Registry controls", sub: "Register, discover, and configure built-in or custom agents.", children: body }));
147
+
148
+ async function register() {
149
+ const name = nameInput.value.trim();
150
+ if (!name) { ctx.toast("Enter an agent name", "info"); return; }
151
+ const capabilities = capsInput.value.split(",").map((s) => s.trim()).filter(Boolean);
152
+ registerBtn.disabled = true;
153
+ const res = await ctx.api.registerAgent({ name, type: "custom", capabilities });
154
+ registerBtn.disabled = false;
155
+ if (res && res.ok) {
156
+ ctx.toast(`Registered ${name}`, "ok");
157
+ loadRegistry(ctx, hosts);
158
+ } else {
159
+ ctx.toast("Register unavailable", "err");
160
+ }
161
+ }
162
+ }
163
+
164
+ function registryCard(ctx, agent) {
165
+ const { h, c } = ctx;
166
+ return c.card(h("div.lt3-stack-3",
167
+ h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start" } },
168
+ h("div",
169
+ h("b", agent.name),
170
+ h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, agent.id),
171
+ ),
172
+ c.pill(agent.source === "builtin" ? "built-in" : "custom", agent.source === "builtin" ? "info" : "warn"),
173
+ ),
174
+ h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)", margin: 0 } }, agent.description || "No description."),
175
+ h("div.lt3-cluster", [c.statePill(agent.enabled ? "ready" : "idle"), c.pill(agent.type), c.pill(`v${agent.version || "1.0.0"}`)]),
176
+ agent.capabilities.length ? h("div.lt3-cluster", agent.capabilities.slice(0, 8).map((cap) => h("span.lt3-chip", cap))) : null,
177
+ h("div.lt3-row-2",
178
+ h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggleAgent(ctx, agent) } }, c.icon(agent.enabled ? "toggle-right" : "toggle-left"), agent.enabled ? "Disable" : "Enable"),
179
+ agent.removable ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => removeAgent(ctx, agent) } }, c.icon("trash"), "Remove") : null,
180
+ ),
181
+ ), { interactive: false });
182
+ }
183
+
184
+ async function toggleAgent(ctx, agent) {
185
+ const res = await ctx.api.updateAgent(agent.id, { config: agent.config || {}, enabled: !agent.enabled });
186
+ ctx.toast(res && res.ok ? `${agent.name}: ${agent.enabled ? "disabled" : "enabled"}` : "Agent update unavailable", res && res.ok ? "ok" : "err");
187
+ if (res && res.ok) ctx.navigate("agents");
188
+ }
189
+
190
+ async function removeAgent(ctx, agent) {
191
+ const res = await ctx.api.removeAgent(agent.id);
192
+ ctx.toast(res && res.ok ? `Removed ${agent.name}` : "Agent remove unavailable", res && res.ok ? "ok" : "err");
193
+ if (res && res.ok) ctx.navigate("agents");
194
+ }
195
+
112
196
  /* ── Agent card ──────────────────────────────────────────────────────────── */
113
197
  function agentCard(ctx, agent, byId) {
114
198
  const { h, icon, c } = ctx;
@@ -158,6 +242,22 @@ function normalize(data) {
158
242
  }));
159
243
  }
160
244
 
245
+ function normalizeRegistry(data) {
246
+ const list = Array.isArray(data) ? data : (data && Array.isArray(data.agents) ? data.agents : []);
247
+ return list.map((agent, i) => ({
248
+ id: agent.id || `agent:${i}`,
249
+ name: agent.name || agent.id || `Agent ${i + 1}`,
250
+ type: agent.type || "custom",
251
+ version: agent.version || "1.0.0",
252
+ description: agent.description || "",
253
+ capabilities: Array.isArray(agent.capabilities) ? agent.capabilities : [],
254
+ source: agent.source || "user",
255
+ enabled: agent.enabled !== false,
256
+ removable: !!agent.removable,
257
+ config: agent.config || {},
258
+ }));
259
+ }
260
+
161
261
  const AVAILABLE_STATES = new Set(["available", "ready", "active", "ok", "idle"]);
162
262
  function isAvailable(state) {
163
263
  return AVAILABLE_STATES.has(String(state).toLowerCase());