morpheus-cli 0.9.5 → 0.9.7

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 (75) hide show
  1. package/README.md +63 -43
  2. package/dist/channels/discord.js +71 -21
  3. package/dist/channels/telegram.js +73 -19
  4. package/dist/cli/commands/restart.js +15 -0
  5. package/dist/cli/commands/start.js +18 -0
  6. package/dist/config/manager.js +61 -0
  7. package/dist/config/paths.js +1 -0
  8. package/dist/config/schemas.js +11 -3
  9. package/dist/http/api.js +3 -0
  10. package/dist/http/routers/link.js +239 -0
  11. package/dist/http/routers/skills.js +1 -8
  12. package/dist/runtime/apoc.js +1 -1
  13. package/dist/runtime/audit/repository.js +1 -1
  14. package/dist/runtime/link-chunker.js +214 -0
  15. package/dist/runtime/link-repository.js +301 -0
  16. package/dist/runtime/link-search.js +298 -0
  17. package/dist/runtime/link-worker.js +284 -0
  18. package/dist/runtime/link.js +295 -0
  19. package/dist/runtime/memory/sati/service.js +1 -1
  20. package/dist/runtime/memory/sqlite.js +52 -0
  21. package/dist/runtime/neo.js +1 -1
  22. package/dist/runtime/oracle.js +81 -44
  23. package/dist/runtime/scaffold.js +4 -17
  24. package/dist/runtime/skills/__tests__/loader.test.js +7 -10
  25. package/dist/runtime/skills/__tests__/registry.test.js +2 -18
  26. package/dist/runtime/skills/__tests__/tool.test.js +55 -224
  27. package/dist/runtime/skills/index.js +1 -2
  28. package/dist/runtime/skills/loader.js +0 -2
  29. package/dist/runtime/skills/registry.js +8 -20
  30. package/dist/runtime/skills/schema.js +0 -4
  31. package/dist/runtime/skills/tool.js +42 -209
  32. package/dist/runtime/smiths/delegator.js +1 -1
  33. package/dist/runtime/smiths/registry.js +1 -1
  34. package/dist/runtime/tasks/worker.js +12 -44
  35. package/dist/runtime/trinity.js +1 -1
  36. package/dist/types/config.js +14 -0
  37. package/dist/ui/assets/AuditDashboard-93LCGHG1.js +1 -0
  38. package/dist/ui/assets/{Chat-BNtutgja.js → Chat-CK5sNcQ1.js} +8 -8
  39. package/dist/ui/assets/{Chronos-3C8RPZcl.js → Chronos-m2h--GEe.js} +1 -1
  40. package/dist/ui/assets/{ConfirmationModal-ZQPBeJ2Z.js → ConfirmationModal-Dd5pUJme.js} +1 -1
  41. package/dist/ui/assets/{Dashboard-CqkHzr2F.js → Dashboard-ODwl7d-a.js} +1 -1
  42. package/dist/ui/assets/{DeleteConfirmationModal-CioxFWn_.js → DeleteConfirmationModal-CCcojDmr.js} +1 -1
  43. package/dist/ui/assets/Documents-dWnSoxFO.js +7 -0
  44. package/dist/ui/assets/{Logs-DBVanS0O.js → Logs-Dc9Z2LBj.js} +1 -1
  45. package/dist/ui/assets/{MCPManager-vXfL3P2U.js → MCPManager-CMkb8vMn.js} +1 -1
  46. package/dist/ui/assets/{ModelPricing-DyfdunLT.js → ModelPricing-DtHPPbEQ.js} +1 -1
  47. package/dist/ui/assets/{Notifications-VL-vep6d.js → Notifications-BPvo-DWP.js} +1 -1
  48. package/dist/ui/assets/{Pagination-oTGieBLM.js → Pagination-BHZKk42X.js} +1 -1
  49. package/dist/ui/assets/{SatiMemories-jaadkW0U.js → SatiMemories-BUPu1Lxr.js} +1 -1
  50. package/dist/ui/assets/SessionAudit-CFKF4DA8.js +9 -0
  51. package/dist/ui/assets/Settings-C4JrXfsR.js +47 -0
  52. package/dist/ui/assets/{Skills-DE3zziXL.js → Skills-BUlvJgJ4.js} +1 -1
  53. package/dist/ui/assets/{Smiths-pmogN1mU.js → Smiths-CDtJdY0I.js} +1 -1
  54. package/dist/ui/assets/{Tasks-Bs8s34Jc.js → Tasks-DK_cOsNK.js} +1 -1
  55. package/dist/ui/assets/{TrinityDatabases-D7uihcdp.js → TrinityDatabases-X07by-19.js} +1 -1
  56. package/dist/ui/assets/{UsageStats-B9gePLZ0.js → UsageStats-dYcgckLq.js} +1 -1
  57. package/dist/ui/assets/{WebhookManager-B2L3rCLM.js → WebhookManager-DDw5eX2R.js} +1 -1
  58. package/dist/ui/assets/{audit-Cggeu9mM.js → audit-DZ5WLUEm.js} +1 -1
  59. package/dist/ui/assets/{chronos-D3-sWhfU.js → chronos-B_HI4mlq.js} +1 -1
  60. package/dist/ui/assets/{config-CBqRUPgn.js → config-B-YxlVrc.js} +1 -1
  61. package/dist/ui/assets/index-DVjwJ8jT.css +1 -0
  62. package/dist/ui/assets/{index-zKplfrXZ.js → index-DfJwcKqG.js} +5 -5
  63. package/dist/ui/assets/{mcp-uL1R9hyA.js → mcp-k-_pwbqA.js} +1 -1
  64. package/dist/ui/assets/{skills-jmw8yTJs.js → skills-xMXangks.js} +1 -1
  65. package/dist/ui/assets/{stats-HOms6GnM.js → stats-C4QZIv5O.js} +1 -1
  66. package/dist/ui/assets/{vendor-icons-DMd9RGvJ.js → vendor-icons-NHF9HNeN.js} +1 -1
  67. package/dist/ui/index.html +3 -3
  68. package/dist/ui/sw.js +1 -1
  69. package/package.json +3 -1
  70. package/dist/runtime/__tests__/keymaker.test.js +0 -148
  71. package/dist/runtime/keymaker.js +0 -157
  72. package/dist/ui/assets/AuditDashboard-DliJ1CX0.js +0 -1
  73. package/dist/ui/assets/SessionAudit-BsXrWlwz.js +0 -9
  74. package/dist/ui/assets/Settings-B4eezRcg.js +0 -47
  75. package/dist/ui/assets/index-D4fzIKy1.css +0 -1
@@ -26,6 +26,8 @@ import { ChronosRepository } from '../../runtime/chronos/repository.js';
26
26
  import { SkillRegistry } from '../../runtime/skills/index.js';
27
27
  import { MCPToolCache } from '../../runtime/tools/cache.js';
28
28
  import { SmithRegistry } from '../../runtime/smiths/registry.js';
29
+ import { Link } from '../../runtime/link.js';
30
+ import { LinkWorker } from '../../runtime/link-worker.js';
29
31
  // Load .env file explicitly in start command
30
32
  const envPath = path.join(process.cwd(), '.env');
31
33
  if (fs.existsSync(envPath)) {
@@ -173,6 +175,16 @@ export const startCommand = new Command('start')
173
175
  catch (err) {
174
176
  display.log(chalk.yellow(`Smiths initialization warning: ${err.message}`), { source: 'Smiths' });
175
177
  }
178
+ // Initialize Link (Documentation Specialist) before Oracle
179
+ try {
180
+ const linkConfig = ConfigManager.getInstance().getLinkConfig();
181
+ const link = Link.getInstance(config);
182
+ await link.initialize();
183
+ display.log(chalk.green('✓ Link initialized'), { source: 'Link' });
184
+ }
185
+ catch (err) {
186
+ display.log(chalk.yellow(`Link initialization warning: ${err.message}`), { source: 'Link' });
187
+ }
176
188
  // Initialize Oracle
177
189
  const oracle = new Oracle(config);
178
190
  try {
@@ -228,6 +240,7 @@ export const startCommand = new Command('start')
228
240
  const telegram = new TelegramAdapter(oracle);
229
241
  try {
230
242
  await telegram.connect(config.channels.telegram.token, config.channels.telegram.allowedUsers || []);
243
+ await telegram.restoreUserSessions();
231
244
  ChannelRegistry.register(telegram);
232
245
  adapters.push(telegram);
233
246
  }
@@ -245,6 +258,7 @@ export const startCommand = new Command('start')
245
258
  const discord = new DiscordAdapter(oracle);
246
259
  try {
247
260
  await discord.connect(config.channels.discord.token, config.channels.discord.allowedUsers || []);
261
+ await discord.restoreUserSessions();
248
262
  ChannelRegistry.register(discord);
249
263
  adapters.push(discord);
250
264
  }
@@ -259,6 +273,9 @@ export const startCommand = new Command('start')
259
273
  // Start Background Services
260
274
  startSessionEmbeddingScheduler();
261
275
  chronosWorker.start();
276
+ // Start LinkWorker for document indexing
277
+ const linkWorker = LinkWorker.getInstance();
278
+ linkWorker.start();
262
279
  if (asyncTasksEnabled) {
263
280
  taskWorker.start();
264
281
  taskNotifier.start();
@@ -278,6 +295,7 @@ export const startCommand = new Command('start')
278
295
  await adapter.disconnect();
279
296
  }
280
297
  chronosWorker.stop();
298
+ linkWorker.stop();
281
299
  if (asyncTasksEnabled) {
282
300
  taskWorker.stop();
283
301
  taskNotifier.stop();
@@ -53,6 +53,10 @@ export class ConfigManager {
53
53
  if (decrypted.trinity?.api_key) {
54
54
  decrypted.trinity = { ...decrypted.trinity, api_key: tryDecrypt(decrypted.trinity.api_key) };
55
55
  }
56
+ // Decrypt Link
57
+ if (decrypted.link?.api_key) {
58
+ decrypted.link = { ...decrypted.link, api_key: tryDecrypt(decrypted.link.api_key) };
59
+ }
56
60
  // Decrypt Audio (Telephonist)
57
61
  if (decrypted.audio?.apiKey) {
58
62
  decrypted.audio = { ...decrypted.audio, apiKey: tryDecrypt(decrypted.audio.apiKey) };
@@ -95,6 +99,10 @@ export class ConfigManager {
95
99
  if (encrypted.trinity?.api_key) {
96
100
  encrypted.trinity = { ...encrypted.trinity, api_key: tryEncrypt(encrypted.trinity.api_key) };
97
101
  }
102
+ // Encrypt Link
103
+ if (encrypted.link?.api_key) {
104
+ encrypted.link = { ...encrypted.link, api_key: tryEncrypt(encrypted.link.api_key) };
105
+ }
98
106
  // Encrypt Audio (Telephonist)
99
107
  if (encrypted.audio?.apiKey) {
100
108
  encrypted.audio = { ...encrypted.audio, apiKey: tryEncrypt(encrypted.audio.apiKey) };
@@ -240,6 +248,38 @@ export class ConfigManager {
240
248
  execution_mode: resolveString('MORPHEUS_TRINITY_EXECUTION_MODE', config.trinity?.execution_mode, 'async'),
241
249
  };
242
250
  }
251
+ // Apply precedence to Link config
252
+ const linkEnvVars = [
253
+ 'MORPHEUS_LINK_PROVIDER',
254
+ 'MORPHEUS_LINK_MODEL',
255
+ 'MORPHEUS_LINK_TEMPERATURE',
256
+ 'MORPHEUS_LINK_API_KEY',
257
+ ];
258
+ const hasLinkEnvOverrides = linkEnvVars.some((envVar) => process.env[envVar] !== undefined);
259
+ let linkConfig;
260
+ if (config.link || hasLinkEnvOverrides) {
261
+ const linkProvider = resolveProvider('MORPHEUS_LINK_PROVIDER', config.link?.provider, llmConfig.provider);
262
+ const linkMaxTokensFallback = config.link?.max_tokens ?? llmConfig.max_tokens;
263
+ const linkContextWindowFallback = config.link?.context_window ?? llmConfig.context_window;
264
+ linkConfig = {
265
+ provider: linkProvider,
266
+ model: resolveModel(linkProvider, 'MORPHEUS_LINK_MODEL', config.link?.model || llmConfig.model),
267
+ temperature: resolveNumeric('MORPHEUS_LINK_TEMPERATURE', config.link?.temperature, llmConfig.temperature),
268
+ max_tokens: resolveOptionalNumeric('MORPHEUS_LINK_MAX_TOKENS', config.link?.max_tokens, linkMaxTokensFallback),
269
+ api_key: resolveApiKey(linkProvider, 'MORPHEUS_LINK_API_KEY', config.link?.api_key || llmConfig.api_key),
270
+ base_url: config.link?.base_url || config.llm.base_url,
271
+ context_window: resolveOptionalNumeric('MORPHEUS_LINK_CONTEXT_WINDOW', config.link?.context_window, linkContextWindowFallback),
272
+ chunk_size: resolveNumeric('MORPHEUS_LINK_CHUNK_SIZE', config.link?.chunk_size, 500),
273
+ score_threshold: resolveNumeric('MORPHEUS_LINK_SCORE_THRESHOLD', config.link?.score_threshold, 0.5),
274
+ max_results: resolveNumeric('MORPHEUS_LINK_MAX_RESULTS', config.link?.max_results, 10),
275
+ execution_mode: resolveString('MORPHEUS_LINK_EXECUTION_MODE', config.link?.execution_mode, 'async'),
276
+ scan_interval_ms: resolveNumeric('MORPHEUS_LINK_SCAN_INTERVAL_MS', config.link?.scan_interval_ms, 30000),
277
+ max_file_size_mb: resolveNumeric('MORPHEUS_LINK_MAX_FILE_SIZE_MB', config.link?.max_file_size_mb, 50),
278
+ vector_weight: resolveNumeric('MORPHEUS_LINK_VECTOR_WEIGHT', config.link?.vector_weight, 0.8),
279
+ bm25_weight: resolveNumeric('MORPHEUS_LINK_BM25_WEIGHT', config.link?.bm25_weight, 0.2),
280
+ personality: resolveString('MORPHEUS_LINK_PERSONALITY', config.link?.personality, 'documentation_specialist'),
281
+ };
282
+ }
243
283
  // Apply precedence to audio config
244
284
  const audioProvider = resolveString('MORPHEUS_AUDIO_PROVIDER', config.audio.provider, DEFAULT_CONFIG.audio.provider);
245
285
  // AudioProvider uses 'google' but resolveApiKey expects LLMProvider which uses 'gemini'
@@ -312,6 +352,7 @@ export class ConfigManager {
312
352
  neo: neoConfig,
313
353
  apoc: apocConfig,
314
354
  trinity: trinityConfig,
355
+ link: linkConfig,
315
356
  audio: audioConfig,
316
357
  channels: channelsConfig,
317
358
  ui: uiConfig,
@@ -436,6 +477,26 @@ export class ConfigManager {
436
477
  }
437
478
  return defaults;
438
479
  }
480
+ getLinkConfig() {
481
+ const defaults = {
482
+ provider: this.config.llm.provider,
483
+ model: this.config.llm.model,
484
+ temperature: this.config.llm.temperature,
485
+ personality: 'documentation_specialist',
486
+ chunk_size: 500,
487
+ score_threshold: 0.5,
488
+ max_results: 10,
489
+ execution_mode: 'async',
490
+ scan_interval_ms: 30000,
491
+ max_file_size_mb: 50,
492
+ vector_weight: 0.8,
493
+ bm25_weight: 0.2,
494
+ };
495
+ if (this.config.link) {
496
+ return { ...defaults, ...this.config.link };
497
+ }
498
+ return defaults;
499
+ }
439
500
  getDevKitConfig() {
440
501
  const defaults = {
441
502
  sandbox_dir: process.cwd(),
@@ -15,4 +15,5 @@ export const PATHS = {
15
15
  commands: path.join(MORPHEUS_ROOT, 'commands'),
16
16
  mcps: path.join(MORPHEUS_ROOT, 'mcps.json'),
17
17
  skills: path.join(MORPHEUS_ROOT, 'skills'),
18
+ docs: path.join(MORPHEUS_ROOT, 'docs'),
18
19
  };
@@ -35,8 +35,16 @@ export const NeoConfigSchema = LLMConfigSchema.extend({
35
35
  export const TrinityConfigSchema = LLMConfigSchema.extend({
36
36
  execution_mode: z.enum(['sync', 'async']).default('async'),
37
37
  });
38
- export const KeymakerConfigSchema = LLMConfigSchema.extend({
39
- skills_dir: z.string().optional(),
38
+ export const LinkConfigSchema = LLMConfigSchema.extend({
39
+ personality: z.string().optional(),
40
+ chunk_size: z.number().int().positive().default(500),
41
+ score_threshold: z.number().min(0).max(1).default(0.5),
42
+ max_results: z.number().int().positive().default(10),
43
+ execution_mode: z.enum(['sync', 'async']).default('async'),
44
+ scan_interval_ms: z.number().int().min(5000).default(30000),
45
+ max_file_size_mb: z.number().int().positive().default(50),
46
+ vector_weight: z.number().min(0).max(1).default(0.8),
47
+ bm25_weight: z.number().min(0).max(1).default(0.2),
40
48
  });
41
49
  export const WebhookConfigSchema = z.object({
42
50
  telegram_notify_all: z.boolean().optional(),
@@ -86,7 +94,7 @@ export const ConfigSchema = z.object({
86
94
  neo: NeoConfigSchema.optional(),
87
95
  apoc: ApocConfigSchema.optional(),
88
96
  trinity: TrinityConfigSchema.optional(),
89
- keymaker: KeymakerConfigSchema.optional(),
97
+ link: LinkConfigSchema.optional(),
90
98
  webhooks: WebhookConfigSchema,
91
99
  audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
92
100
  memory: z.object({
package/dist/http/api.js CHANGED
@@ -21,6 +21,7 @@ import { createChronosJobRouter, createChronosConfigRouter } from './routers/chr
21
21
  import { createSkillsRouter } from './routers/skills.js';
22
22
  import { createSmithsRouter } from './routers/smiths.js';
23
23
  import { createDangerRouter } from './routers/danger.js';
24
+ import { createLinkRouter } from './routers/link.js';
24
25
  import { getActiveEnvOverrides } from '../config/precedence.js';
25
26
  import { hotReloadConfig, getRestartRequiredChanges } from '../runtime/hot-reload.js';
26
27
  import { AuditRepository } from '../runtime/audit/repository.js';
@@ -52,6 +53,8 @@ export function createApiRouter(oracle, chronosWorker) {
52
53
  router.use('/smiths', createSmithsRouter());
53
54
  // Mount Danger Zone router
54
55
  router.use('/danger', createDangerRouter());
56
+ // Mount Link router (Documentation management)
57
+ router.use('/link', createLinkRouter());
55
58
  // --- Session Management ---
56
59
  router.get('/sessions', async (req, res) => {
57
60
  try {
@@ -0,0 +1,239 @@
1
+ import { Router } from 'express';
2
+ import multer from 'multer';
3
+ import path from 'path';
4
+ import fs from 'fs-extra';
5
+ import { homedir } from 'os';
6
+ import { LinkRepository } from '../../runtime/link-repository.js';
7
+ import { LinkWorker } from '../../runtime/link-worker.js';
8
+ import { ConfigManager } from '../../config/manager.js';
9
+ const DOCS_PATH = path.join(homedir(), '.morpheus', 'docs');
10
+ // Configure multer for file uploads
11
+ const storage = multer.diskStorage({
12
+ destination: async (req, file, cb) => {
13
+ await fs.ensureDir(DOCS_PATH);
14
+ cb(null, DOCS_PATH);
15
+ },
16
+ filename: (req, file, cb) => {
17
+ // Multer decodes originalname as Latin1 per HTTP spec.
18
+ // Re-encode to get the raw bytes and decode as UTF-8.
19
+ const fixedName = Buffer.from(file.originalname, 'latin1').toString('utf-8');
20
+ cb(null, fixedName);
21
+ },
22
+ });
23
+ const upload = multer({
24
+ storage,
25
+ limits: {
26
+ fileSize: 50 * 1024 * 1024, // 50MB default, will check config
27
+ },
28
+ fileFilter: (req, file, cb) => {
29
+ const name = Buffer.from(file.originalname, 'latin1').toString('utf-8');
30
+ const ext = path.extname(name).toLowerCase();
31
+ const allowed = ['.pdf', '.txt', '.md', '.docx'];
32
+ if (allowed.includes(ext)) {
33
+ cb(null, true);
34
+ }
35
+ else {
36
+ cb(new Error(`Unsupported file type: ${ext}. Allowed: ${allowed.join(', ')}`));
37
+ }
38
+ },
39
+ });
40
+ /**
41
+ * Create the Link router for document management.
42
+ */
43
+ export function createLinkRouter() {
44
+ const router = Router();
45
+ const repository = LinkRepository.getInstance();
46
+ const worker = LinkWorker.getInstance();
47
+ // GET /api/link/documents - List all documents
48
+ router.get('/documents', (req, res) => {
49
+ try {
50
+ const status = req.query.status;
51
+ const documents = repository.listDocuments(status);
52
+ const stats = repository.getStats();
53
+ res.json({
54
+ documents,
55
+ stats,
56
+ });
57
+ }
58
+ catch (err) {
59
+ res.status(500).json({ error: err.message });
60
+ }
61
+ });
62
+ // GET /api/link/documents/:id - Get single document
63
+ router.get('/documents/:id', (req, res) => {
64
+ try {
65
+ const document = repository.getDocument(req.params.id);
66
+ if (!document) {
67
+ return res.status(404).json({ error: 'Document not found' });
68
+ }
69
+ // Also fetch chunks
70
+ const chunks = repository.getChunksByDocument(req.params.id);
71
+ res.json({ document, chunks });
72
+ }
73
+ catch (err) {
74
+ res.status(500).json({ error: err.message });
75
+ }
76
+ });
77
+ // POST /api/link/documents/upload - Upload a new document
78
+ router.post('/documents/upload', async (req, res) => {
79
+ try {
80
+ const config = ConfigManager.getInstance().getLinkConfig();
81
+ const maxSizeMB = config.max_file_size_mb;
82
+ // Configure multer with config max size
83
+ const uploadWithConfig = multer({
84
+ storage,
85
+ limits: { fileSize: maxSizeMB * 1024 * 1024 },
86
+ fileFilter: (req, file, cb) => {
87
+ const name = Buffer.from(file.originalname, 'latin1').toString('utf-8');
88
+ const ext = path.extname(name).toLowerCase();
89
+ const allowed = ['.pdf', '.txt', '.md', '.docx'];
90
+ if (allowed.includes(ext)) {
91
+ cb(null, true);
92
+ }
93
+ else {
94
+ cb(new Error(`Unsupported file type: ${ext}`));
95
+ }
96
+ },
97
+ });
98
+ // Handle upload
99
+ await new Promise((resolve, reject) => {
100
+ uploadWithConfig.single('file')(req, res, (err) => {
101
+ if (err)
102
+ reject(err);
103
+ else
104
+ resolve();
105
+ });
106
+ });
107
+ if (!req.file) {
108
+ return res.status(400).json({ error: 'No file uploaded' });
109
+ }
110
+ // Trigger immediate scan
111
+ const result = await worker.tick();
112
+ res.json({
113
+ message: 'File uploaded successfully',
114
+ filename: Buffer.from(req.file.originalname, 'latin1').toString('utf-8'),
115
+ path: req.file.path,
116
+ indexed: result.indexed,
117
+ });
118
+ }
119
+ catch (err) {
120
+ res.status(500).json({ error: err.message });
121
+ }
122
+ });
123
+ // DELETE /api/link/documents/:id - Delete a document
124
+ router.delete('/documents/:id', async (req, res) => {
125
+ try {
126
+ const document = repository.getDocument(req.params.id);
127
+ if (!document) {
128
+ return res.status(404).json({ error: 'Document not found' });
129
+ }
130
+ // Delete from repository (CASCADE removes chunks and embeddings)
131
+ const deleted = repository.deleteDocument(req.params.id);
132
+ // Also delete file from disk
133
+ try {
134
+ await fs.unlink(document.file_path);
135
+ }
136
+ catch {
137
+ // File may not exist, ignore
138
+ }
139
+ res.json({ message: 'Document deleted', deleted });
140
+ }
141
+ catch (err) {
142
+ res.status(500).json({ error: err.message });
143
+ }
144
+ });
145
+ // POST /api/link/documents/:id/reindex - Force reindex a document
146
+ router.post('/documents/:id/reindex', async (req, res) => {
147
+ try {
148
+ const document = repository.getDocument(req.params.id);
149
+ if (!document) {
150
+ return res.status(404).json({ error: 'Document not found' });
151
+ }
152
+ // Check if file still exists
153
+ const exists = await fs.pathExists(document.file_path);
154
+ if (!exists) {
155
+ return res.status(400).json({ error: 'Document file no longer exists' });
156
+ }
157
+ // Reset status to pending and trigger processing
158
+ repository.updateDocumentStatus(req.params.id, 'pending');
159
+ // Process the document
160
+ const result = await worker.processDocument(document.file_path);
161
+ res.json({
162
+ message: 'Document reindexed',
163
+ result,
164
+ });
165
+ }
166
+ catch (err) {
167
+ res.status(500).json({ error: err.message });
168
+ }
169
+ });
170
+ // GET /api/link/config - Get Link configuration
171
+ router.get('/config', (req, res) => {
172
+ try {
173
+ const config = ConfigManager.getInstance().getLinkConfig();
174
+ res.json(config);
175
+ }
176
+ catch (err) {
177
+ res.status(500).json({ error: err.message });
178
+ }
179
+ });
180
+ // POST /api/link/config - Update Link configuration (partial update)
181
+ router.post('/config', async (req, res) => {
182
+ try {
183
+ const configManager = ConfigManager.getInstance();
184
+ const currentConfig = configManager.get();
185
+ const currentLinkConfig = configManager.getLinkConfig();
186
+ const updates = req.body;
187
+ // Merge updates with current config (ensuring all required fields are present)
188
+ const newLinkConfig = {
189
+ ...currentLinkConfig,
190
+ ...updates,
191
+ };
192
+ // Save to zaion.yaml
193
+ await configManager.save({
194
+ ...currentConfig,
195
+ link: newLinkConfig,
196
+ });
197
+ // Update worker interval if changed
198
+ if (updates.scan_interval_ms) {
199
+ worker.updateInterval(updates.scan_interval_ms);
200
+ }
201
+ res.json({
202
+ message: 'Configuration updated',
203
+ config: configManager.getLinkConfig(),
204
+ });
205
+ }
206
+ catch (err) {
207
+ res.status(500).json({ error: err.message });
208
+ }
209
+ });
210
+ // POST /api/link/worker/scan - Trigger manual scan
211
+ router.post('/worker/scan', async (req, res) => {
212
+ try {
213
+ const result = await worker.tick();
214
+ res.json({
215
+ message: 'Scan completed',
216
+ ...result,
217
+ });
218
+ }
219
+ catch (err) {
220
+ res.status(500).json({ error: err.message });
221
+ }
222
+ });
223
+ // GET /api/link/worker/status - Get worker status
224
+ router.get('/worker/status', (req, res) => {
225
+ try {
226
+ const config = ConfigManager.getInstance().getLinkConfig();
227
+ const stats = repository.getStats();
228
+ res.json({
229
+ running: true, // Worker is always running when daemon is up
230
+ scan_interval_ms: config.scan_interval_ms,
231
+ ...stats,
232
+ });
233
+ }
234
+ catch (err) {
235
+ res.status(500).json({ error: err.message });
236
+ }
237
+ });
238
+ return router;
239
+ }
@@ -4,7 +4,7 @@ import extract from 'extract-zip';
4
4
  import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
- import { SkillRegistry, updateSkillDelegateDescription } from '../../runtime/skills/index.js';
7
+ import { SkillRegistry } from '../../runtime/skills/index.js';
8
8
  import { DisplayManager } from '../../runtime/display.js';
9
9
  import { PATHS } from '../../config/paths.js';
10
10
  import { SkillMetadataSchema } from '../../runtime/skills/schema.js';
@@ -119,8 +119,6 @@ export function createSkillsRouter() {
119
119
  try {
120
120
  const registry = SkillRegistry.getInstance();
121
121
  const result = await registry.reload();
122
- // Update skill_delegate tool description with new skills
123
- updateSkillDelegateDescription();
124
122
  display.log(`Skills reloaded: ${result.skills.length} loaded, ${result.errors.length} errors`, {
125
123
  source: 'SkillsAPI',
126
124
  });
@@ -207,7 +205,6 @@ export function createSkillsRouter() {
207
205
  // Reload skills
208
206
  const registry = SkillRegistry.getInstance();
209
207
  await registry.reload();
210
- updateSkillDelegateDescription();
211
208
  display.log(`Skill "${metadata.name}" uploaded successfully`, { source: 'SkillsAPI' });
212
209
  res.json({
213
210
  success: true,
@@ -258,8 +255,6 @@ export function createSkillsRouter() {
258
255
  if (!success) {
259
256
  return res.status(404).json({ error: `Skill "${name}" not found` });
260
257
  }
261
- // Update skill_delegate tool description
262
- updateSkillDelegateDescription();
263
258
  display.log(`Skill "${name}" enabled`, { source: 'SkillsAPI' });
264
259
  res.json({ success: true, name, enabled: true });
265
260
  }
@@ -277,8 +272,6 @@ export function createSkillsRouter() {
277
272
  if (!success) {
278
273
  return res.status(404).json({ error: `Skill "${name}" not found` });
279
274
  }
280
- // Update skill_delegate tool description
281
- updateSkillDelegateDescription();
282
275
  display.log(`Skill "${name}" disabled`, { source: 'SkillsAPI' });
283
276
  res.json({ success: true, name, enabled: false });
284
277
  }
@@ -259,7 +259,7 @@ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
259
259
  try {
260
260
  const inputCount = messages.length;
261
261
  const startMs = Date.now();
262
- const response = await this.agent.invoke({ messages }, { recursionLimit: 50 });
262
+ const response = await this.agent.invoke({ messages }, { recursionLimit: 10 });
263
263
  const durationMs = Date.now() - startMs;
264
264
  const apocConfig = this.config.apoc || this.config.llm;
265
265
  const lastMessage = response.messages[response.messages.length - 1];
@@ -188,7 +188,7 @@ export class AuditRepository {
188
188
  SUM(CASE WHEN ae.event_type = 'llm_call' THEN 1 ELSE 0 END) as llmCallCount,
189
189
  SUM(CASE WHEN ae.event_type = 'tool_call' THEN 1 ELSE 0 END) as toolCallCount,
190
190
  SUM(CASE WHEN ae.event_type = 'mcp_tool' THEN 1 ELSE 0 END) as mcpToolCount,
191
- SUM(CASE WHEN ae.event_type = 'skill_executed' THEN 1 ELSE 0 END) as skillCount,
191
+ SUM(CASE WHEN ae.event_type = 'skill_loaded' THEN 1 ELSE 0 END) as skillCount,
192
192
  SUM(CASE WHEN ae.event_type = 'memory_recovery' THEN 1 ELSE 0 END) as memoryRecoveryCount,
193
193
  SUM(CASE WHEN ae.event_type = 'memory_persist' THEN 1 ELSE 0 END) as memoryPersistCount,
194
194
  SUM(CASE WHEN ae.event_type = 'chronos_job' THEN 1 ELSE 0 END) as chronosJobCount,