@swarmclawai/swarmclaw 1.3.4 → 1.3.6

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 (73) hide show
  1. package/README.md +20 -76
  2. package/package.json +3 -2
  3. package/skills/swarmclaw.md +17 -0
  4. package/src/app/api/agents/[id]/dream/route.ts +45 -0
  5. package/src/app/api/knowledge/[id]/route.ts +48 -49
  6. package/src/app/api/knowledge/hygiene/route.ts +13 -0
  7. package/src/app/api/knowledge/route.ts +70 -42
  8. package/src/app/api/knowledge/sources/[id]/archive/route.ts +15 -0
  9. package/src/app/api/knowledge/sources/[id]/restore/route.ts +10 -0
  10. package/src/app/api/knowledge/sources/[id]/route.ts +1 -0
  11. package/src/app/api/knowledge/sources/[id]/supersede/route.ts +26 -0
  12. package/src/app/api/knowledge/sources/[id]/sync/route.ts +17 -0
  13. package/src/app/api/knowledge/sources/route.ts +1 -0
  14. package/src/app/api/knowledge/upload/route.ts +3 -51
  15. package/src/app/api/memory/dream/[id]/route.ts +19 -0
  16. package/src/app/api/memory/dream/route.ts +34 -0
  17. package/src/app/knowledge/layout.tsx +1 -1
  18. package/src/app/knowledge/page.tsx +2 -22
  19. package/src/app/protocols/page.tsx +21 -2
  20. package/src/cli/index.js +16 -0
  21. package/src/cli/spec.js +5 -0
  22. package/src/components/agents/agent-sheet.tsx +65 -0
  23. package/src/components/chat/message-bubble.tsx +10 -0
  24. package/src/components/knowledge/grounding-panel.tsx +99 -0
  25. package/src/components/knowledge/knowledge-detail.tsx +402 -0
  26. package/src/components/knowledge/knowledge-list.tsx +351 -126
  27. package/src/components/knowledge/knowledge-sheet.tsx +208 -119
  28. package/src/components/memory/dream-history.tsx +155 -0
  29. package/src/components/memory/memory-card.tsx +7 -0
  30. package/src/components/memory/memory-detail.tsx +46 -0
  31. package/src/components/runs/run-list.tsx +23 -0
  32. package/src/lib/server/api-routes.test.ts +43 -2
  33. package/src/lib/server/chat-execution/chat-execution-disabled.test.ts +14 -31
  34. package/src/lib/server/chat-execution/chat-execution-eval-history.test.ts +11 -34
  35. package/src/lib/server/chat-execution/chat-execution-grounding.test.ts +108 -0
  36. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +35 -36
  37. package/src/lib/server/chat-execution/chat-execution-types.ts +8 -1
  38. package/src/lib/server/chat-execution/chat-execution.ts +1 -0
  39. package/src/lib/server/chat-execution/chat-turn-finalization.ts +21 -1
  40. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +6 -1
  41. package/src/lib/server/chat-execution/post-stream-finalization.ts +15 -3
  42. package/src/lib/server/chat-execution/prompt-sections.ts +29 -3
  43. package/src/lib/server/chat-execution/stream-agent-chat.ts +6 -1
  44. package/src/lib/server/execution-engine/task-attempt.ts +8 -2
  45. package/src/lib/server/knowledge-import.ts +159 -0
  46. package/src/lib/server/knowledge-sources.test.ts +261 -0
  47. package/src/lib/server/knowledge-sources.ts +1284 -0
  48. package/src/lib/server/memory/dream-cycles.ts +49 -0
  49. package/src/lib/server/memory/dream-idle-callback.ts +38 -0
  50. package/src/lib/server/memory/dream-service.ts +315 -0
  51. package/src/lib/server/memory/memory-db.ts +37 -2
  52. package/src/lib/server/protocols/protocol-agent-turn.ts +7 -0
  53. package/src/lib/server/protocols/protocol-run-lifecycle.ts +19 -6
  54. package/src/lib/server/protocols/protocol-service.test.ts +99 -0
  55. package/src/lib/server/protocols/protocol-step-helpers.ts +7 -1
  56. package/src/lib/server/protocols/protocol-step-processors.ts +16 -3
  57. package/src/lib/server/protocols/protocol-types.ts +4 -0
  58. package/src/lib/server/runtime/daemon-state/core.ts +6 -1
  59. package/src/lib/server/runtime/run-ledger.test.ts +120 -0
  60. package/src/lib/server/runtime/run-ledger.ts +27 -1
  61. package/src/lib/server/runtime/session-run-manager/drain.ts +5 -0
  62. package/src/lib/server/runtime/session-run-manager/state.ts +19 -2
  63. package/src/lib/server/storage-normalization.ts +5 -0
  64. package/src/lib/server/storage.ts +15 -0
  65. package/src/lib/server/test-utils/run-with-temp-data-dir.ts +15 -2
  66. package/src/stores/slices/ui-slice.ts +4 -0
  67. package/src/types/agent.ts +7 -0
  68. package/src/types/dream.ts +45 -0
  69. package/src/types/index.ts +1 -0
  70. package/src/types/message.ts +3 -0
  71. package/src/types/misc.ts +131 -0
  72. package/src/types/protocol.ts +4 -0
  73. package/src/types/run.ts +4 -1
package/README.md CHANGED
@@ -204,6 +204,25 @@ Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public
204
204
 
205
205
  ## Release Notes
206
206
 
207
+ ### v1.3.6 Highlights
208
+
209
+ - **Knowledge hygiene visibility fix**: exact-duplicate archival now only applies when sources share the same visibility and origin fingerprint. Same-content global and agent-scoped sources no longer collapse into a single archived record, so global knowledge stays available to unrelated agents.
210
+ - **Release gate hardening**: the default test matrix now includes the 1.3.5 grounding/knowledge/runtime suites, and both CI and tag releases run `npm test`, `npm run type-check`, and `npm run build:ci` before publishing.
211
+
212
+ ### v1.3.5 Highlights
213
+
214
+ - **Knowledge grounding & citations**: agent responses are now grounded against knowledge sources at retrieval time. Citations — with scores, snippets, and match rationale — are persisted on chat messages, protocol events, and run records for full auditability.
215
+ - **Knowledge source lifecycle**: new source management system with create, sync, archive, restore, supersede, and delete operations. Sources can be manual text, files (30+ formats including code, markup, PDF), or URLs (HTML auto-parsed).
216
+ - **Hygiene automation**: background scanner detects stale, duplicate, overlapping, and broken knowledge sources. Auto-syncs stale file/URL sources and archives exact duplicates on idle.
217
+ - **Redesigned Knowledge page**: detail-focused layout with sidebar list, full source inspector (metadata, chunks, sync status), and inline actions. Search/browse toggle, tag filtering, and archive visibility controls.
218
+ - **Grounding panel**: new reusable citation display component shown on chat messages, protocol artifacts, and run results — surfaces retrieval query, hit scores, snippets, and source links.
219
+ - **7 new API endpoints**: `/knowledge/hygiene` (GET/POST), `/knowledge/sources/:id/archive`, `/restore`, `/supersede`, `/sync` for full source lifecycle management via CLI and API.
220
+ - **Protocol citation propagation**: structured protocol runs now capture and persist citations on participant responses and emitted artifacts.
221
+ - **Dreaming (idle-time memory consolidation)**: agents now consolidate and optimize memories during idle periods. Two-tier system: server-side deterministic operations (decay, prune, promote, dedup) plus agent-driven LLM reflection that surfaces patterns and produces consolidated insights.
222
+ - **Per-agent dream configuration**: dreaming is opt-in per agent with configurable cooldown, decay age, prune threshold, and Tier 2 reflection controls.
223
+ - **Dream cycle audit trail**: every dream cycle is tracked with status, trigger, duration, and detailed results. Viewable in the memory UI and via CLI.
224
+ - **3 new API endpoints**: `/memory/dream` (GET/POST), `/memory/dream/:id` for dream cycle management.
225
+
207
226
  ### v1.3.4 Highlights
208
227
 
209
228
  - **Bug fix — custom provider loading under Turbopack (#32)**: converted all CommonJS `require()` calls across the codebase to ES module imports, fixing "Unknown provider: custom-\<id\>" errors and other potential Turbopack compatibility issues. Affected modules: providers, provider health, subagent swarm, prompt builder, chat finalization, CLI utils, and OpenClaw connectors. Thanks to @psywolf85 for the initial fix.
@@ -235,82 +254,7 @@ Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public
235
254
  - **Extended approval workflows**: new `agent_create`, `budget_change`, and `delegation_enable` approval categories with configurable policies in settings. When enabled, agent creation returns a pending approval instead of creating the agent directly.
236
255
  - **Shared validation schemas**: Zod schemas in `src/lib/validation/schemas.ts` are now safe for client-side import (server-only DAG validation moved to `server-schemas.ts`), enabling form-level pre-validation.
237
256
 
238
- ### v1.2.9 Highlights
239
-
240
- - **SwarmDock marketplace connector**: SwarmClaw agents can now register on SwarmDock, auto-bid on matching work, receive assignments as board tasks, and submit results back through the connector runtime.
241
- - **Secure SwarmDock credentials**: Ed25519 identity keys now live in encrypted credentials instead of connector config, legacy plaintext keys auto-migrate on load, connector API responses redact secrets, and failed result submissions now surface properly for retry/recovery.
242
- - **Portable config transfer**: new portability import/export flows move agents, skills, and schedules between installs, with manifest validation that rejects malformed imports cleanly instead of crashing.
243
- - **Wallet surface simplification**: wallets are now lightweight Base-linked agent records with stricter validation for missing agents and invalid addresses, replacing the older action-heavy wallet route surface.
244
- - **UI surface cleanup**: the app now centers work around tasks, structured sessions, autonomy, connectors, and projects, with the old Missions, Canvas, and legacy wallet action screens removed from the shipped interface.
245
-
246
- ### v1.2.8 Highlights
247
-
248
- - **Linux/WSL compatibility**: subprocess spawning now uses `$SHELL` instead of hardcoded `/bin/zsh`, fixing `ENOENT` errors on Linux and WSL systems.
249
- - **nvm compatibility**: stripped `npm_config_prefix` from subprocess environment, fixing node PATH resolution for nvm users.
250
- - **Dev-mode daemon fix**: prevented duplicate daemon spawn failure when daemon runs in-process during development.
251
- - **Gateway sheet stability**: fixed infinite render loop when opening a gateway profile with a disconnected gateway.
252
- - **Auto-provision gateway on deploy**: "Deploy on this host" now automatically creates a gateway profile and credential, so agents can connect immediately without a manual save step.
253
- - **Credential cleanup on gateway delete**: deleting a gateway profile now cleans up its associated credential when no other gateway or agent references it.
254
-
255
- ### v1.2.7 Highlights
256
-
257
- - **Tool primitives**: consolidated 50+ narrow tools into 6 action-based primitives — `execute`, `files`, `memory`, `platform`, `skills`, and `credential-env` — with skill teach files so agents learn usage patterns on demand.
258
- - **Type system decomposition**: split the monolithic 3500-line types file into 16 focused domain modules (`agent.ts`, `session.ts`, `message.ts`, `run.ts`, etc.) with full backward-compatible re-exports.
259
- - **Lightweight direct chat**: message classifier detects simple conversational turns and fast-paths them with minimal prompt assembly and reduced thinking budget.
260
- - **Prompt mode resolution**: root sessions receive full system prompts while delegated and lightweight turns get streamlined minimal prompts.
261
- - **Execute tool config UI**: inspector panel now has separate configuration sections for the execute tool (sandbox/host backend, network, timeout) and browser sandbox.
262
- - **Chat store pagination**: proper `messageStartIndex` tracking, improved queued-message deduplication, and active-turn transcript merging.
263
- - **Per-session WebSocket notifications**: message mutations now broadcast to session-specific topics for more targeted UI updates.
264
- - **Tool capability policy refactoring**: consolidated matching logic with extension ID canonicalization for reliable policy resolution.
265
- - **Validation schemas**: new `AgentExecuteConfigSchema` for Zod-based execute config validation.
266
- - **Bug fix — custom provider resolution**: fixed "Unknown provider" error when using custom providers in chat execution.
267
- - **Lint baseline maintained** at 364 violations (no regressions).
268
-
269
- ### v1.2.5 Highlights
270
-
271
- - **Working memory hierarchy**: agents maintain structured working state (facts, plans, decisions, blockers, evidence) that persists across turns and survives context compaction.
272
- - **Execution brief**: each chat turn receives a concise briefing document synthesized from working state for faster, more focused agent reasoning.
273
- - **RunContext**: persistent structured context tracks session lineage, delegation chain, and accumulated working memory across run lifecycle.
274
- - **Unified message flow**: consolidated message handling and execution routing into a single coherent pipeline.
275
- - **Delegation advisory**: new advisory layer for structured capability analysis during delegation decisions.
276
- - **Real-time session sync**: session create, update, and delete events now push over WebSocket, replacing poll-only refresh for the chat list.
277
- - **HMR resilience**: module-level state in the WebSocket client, fallback polling, and API request dedup now survives Next.js hot-module reloads.
278
- - **Type safety sweep**: eliminated 16 `any` types across 7 API routes with proper narrowing and error guards.
279
- - **Bug fix — setup wizard crash**: replaced client-side `crypto.randomUUID()` with browser-safe alternative, fixing fresh-install failures with Ollama and other providers.
280
- - **Bug fix — custom provider validation**: connection-test endpoint now recognizes custom providers from storage instead of rejecting them as "Unsupported provider."
281
- - **Bug fix — session cwd normalization**: `updateChatSession` now expands `~` paths consistently with session creation.
282
-
283
- ### v1.2.4 Highlights
284
-
285
- - **Custom providers in agent config**: agent setup and inline model switching now merge saved custom provider configs into the selectable provider list, so custom providers show up reliably even when the built-in provider feed is stale or incomplete.
286
- - **Custom provider save-only flow**: the Providers screen no longer forces connection tests or live model discovery for custom providers; operators can save the endpoint, linked key, and manual model list directly.
287
- - **Custom provider runtime routing**: saved custom-provider model lists and linked credentials now flow through the agent UI and runtime resolution paths consistently, including legacy `provider_configs` records normalized on load.
288
-
289
- ### v1.2.3 Highlights
290
-
291
- - **Standalone asset staging repair**: `swarmclaw server` now copies `.next/static` and `public/` into the Next.js standalone runtime after the first build, preventing blank UI loads and 503s for CSS, JS, and image assets.
292
- - **OpenClaw SSH port fix**: remote OpenClaw deploys now preserve well-known SSH ports like `22` instead of clamping them to `1024`.
293
- - **OpenClaw image source fix**: generated remote deploy bundles and default upgrade actions now use the official `ghcr.io/openclaw/openclaw:latest` image instead of the missing Docker Hub shorthand.
294
- - **Standalone self-healing**: server startup now repairs older incomplete standalone bundles by staging missing runtime assets before launching `server.js`.
295
-
296
- ### v1.2.2 Highlights
297
-
298
- - **Modular chat execution pipeline**: decomposed the monolithic chat-execution module into 6 focused stages (preflight, preparation, stream execution, partial persistence, finalization, types) for maintainability and testability.
299
- - **Repository pattern adoption**: extracted ~15 repository modules from `storage.ts`, giving each domain (agents, sessions, missions, credentials, tasks, etc.) its own data-access layer.
300
- - **Runtime state encapsulation**: moved process-local state (active sessions, dev servers) from storage into `runtime-state.ts` with proper HMR singleton usage.
301
- - **Streaming state improvements**: stable assistant render IDs, better live-row display logic, and smoother streaming phase transitions in the chat UI.
302
- - **8 new skills**: coding-agent, github, nano-banana-pro, nano-pdf, openai-image-gen, resourceful-problem-solving, skill-creator, summarize.
303
- - **Lint baseline improvements**: reduced lint violations from 414 to 396 (-18).
304
-
305
- ### v1.2.1 Highlights
306
-
307
- - **System health endpoint**: new `/api/system/status` route returns lightweight health summary for external monitoring and uptime checks.
308
- - **Memory abstracts**: ~100-token LLM summaries attached to memories for efficient proactive recall without loading full content.
309
- - **Structured logging**: migrated 40+ files from `console.*` to the `log` module for consistent, level-aware logging across the codebase.
310
- - **Lint baseline improvements**: reduced lint violations from 440 to 414 (-26) through targeted fixes across server and UI code.
311
- - **Daemon housekeeping**: pruning for subagent processes, orchestrator state, connector sessions, and usage records to prevent resource leaks.
312
- - **SKILL.md v2.0.0**: comprehensive CLI documentation covering 40+ command groups with examples and usage patterns.
313
- - **New dev scripts**: added `type-check`, `test`, and `format` scripts to `package.json` for streamlined development workflows.
257
+ *For older release notes (v1.2.x and earlier), see [swarmclaw.ai/docs/release-notes](https://swarmclaw.ai/docs/release-notes).*
314
258
 
315
259
 
316
260
  ## What SwarmClaw Focuses On
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -62,7 +62,7 @@
62
62
  "benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
63
63
  "benchmark:agent-regression": "node --import tsx ./scripts/run-agent-regression-suite.ts",
64
64
  "type-check": "tsc --noEmit",
65
- "test": "npm run test:cli && npm run test:setup && npm run test:openclaw",
65
+ "test": "npm run test:cli && npm run test:setup && npm run test:openclaw && npm run test:runtime",
66
66
  "format": "eslint --fix",
67
67
  "lint": "eslint",
68
68
  "lint:fix": "eslint --fix",
@@ -72,6 +72,7 @@
72
72
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs",
73
73
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts",
74
74
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
75
+ "test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts",
75
76
  "test:e2e": "tsx .workbench/browser-e2e/run.ts",
76
77
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
77
78
  "prepack": "npm run build:ci",
@@ -73,6 +73,23 @@ Agents have persistent memory across sessions:
73
73
  - Memories are automatically surfaced in context when relevant
74
74
  - Store important learnings proactively -- don't wait to be asked
75
75
 
76
+ ### Dreaming
77
+
78
+ Agents with dreaming enabled automatically consolidate memories during idle periods. You can also trigger a dream manually:
79
+
80
+ #### Check dream status
81
+ ```json
82
+ { "tool": "memory", "action": "list", "category": "dream_reflection" }
83
+ ```
84
+
85
+ #### Manual dream trigger
86
+ Use the platform API to trigger a dream cycle:
87
+ ```json
88
+ { "tool": "execute", "command": "curl -s -X POST http://localhost:3456/api/memory/dream -H 'Content-Type: application/json' -d '{\"agentId\":\"YOUR_AGENT_ID\"}'" }
89
+ ```
90
+
91
+ Dream cycles produce `dream_reflection` and `consolidated_insight` memories that help maintain a clean, coherent memory store over time.
92
+
76
93
  ### Delegation
77
94
 
78
95
  Agents can delegate work to other agents:
@@ -0,0 +1,45 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { notFound } from '@/lib/server/collection-helpers'
3
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
4
+ import { log } from '@/lib/server/logger'
5
+
6
+ const TAG = 'api-agent-dream'
7
+
8
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
9
+ try {
10
+ const { id } = await params
11
+ const { loadAgents } = await import('@/lib/server/storage')
12
+ const agents = loadAgents()
13
+ const agent = agents[id]
14
+ if (!agent) return notFound()
15
+ const { resolveDreamConfig } = await import('@/lib/server/memory/dream-service')
16
+ const { listDreamCycles } = await import('@/lib/server/memory/dream-cycles')
17
+ return NextResponse.json({
18
+ ok: true,
19
+ config: resolveDreamConfig(agent),
20
+ recentCycles: listDreamCycles(id, 10),
21
+ })
22
+ } catch (err: unknown) {
23
+ log.error(TAG, 'GET failed:', err)
24
+ return NextResponse.json({ ok: false, error: String((err as Error)?.message || err) }, { status: 500 })
25
+ }
26
+ }
27
+
28
+ export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
29
+ try {
30
+ const { id } = await params
31
+ const { data: body, error } = await safeParseBody(req)
32
+ if (error) return error
33
+ const { updateAgent } = await import('@/lib/server/agents/agent-service')
34
+ const patch: Record<string, unknown> = {}
35
+ if ('dreamEnabled' in body) patch.dreamEnabled = body.dreamEnabled
36
+ if ('dreamConfig' in body) patch.dreamConfig = body.dreamConfig
37
+ const updated = updateAgent(id, patch)
38
+ if (!updated) return notFound()
39
+ const { resolveDreamConfig } = await import('@/lib/server/memory/dream-service')
40
+ return NextResponse.json({ ok: true, config: resolveDreamConfig(updated) })
41
+ } catch (err: unknown) {
42
+ log.error(TAG, 'PATCH failed:', err)
43
+ return NextResponse.json({ ok: false, error: String((err as Error)?.message || err) }, { status: 500 })
44
+ }
45
+ }
@@ -1,73 +1,72 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { notFound } from '@/lib/server/collection-helpers'
3
- import { getMemoryDb } from '@/lib/server/memory/memory-db'
3
+ import {
4
+ deleteKnowledgeSource,
5
+ getKnowledgeSourceDetail,
6
+ updateKnowledgeSource,
7
+ } from '@/lib/server/knowledge-sources'
8
+ import type { KnowledgeSourceKind } from '@/types'
9
+
10
+ function inferKind(body: Record<string, unknown>): KnowledgeSourceKind | undefined {
11
+ if (body.kind === 'file' || body.kind === 'url' || body.kind === 'manual') return body.kind
12
+ if (typeof body.sourcePath === 'string' && body.sourcePath.trim()) return 'file'
13
+ if (typeof body.sourceUrl === 'string' && body.sourceUrl.trim() && typeof body.content !== 'string') return 'url'
14
+ return undefined
15
+ }
4
16
 
5
17
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
18
  const { id } = await params
7
- const db = getMemoryDb()
8
- const entry = db.get(id)
9
- if (!entry || entry.category !== 'knowledge') {
10
- return notFound()
11
- }
12
- return NextResponse.json(entry)
19
+ const detail = await getKnowledgeSourceDetail(id)
20
+ if (!detail) return notFound()
21
+ return NextResponse.json(detail)
13
22
  }
14
23
 
15
24
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
16
25
  const { id } = await params
17
- const db = getMemoryDb()
18
- const existing = db.get(id)
19
- if (!existing || existing.category !== 'knowledge') {
20
- return notFound()
21
- }
22
-
23
26
  const body = await req.json().catch(() => null)
24
27
  if (!body || typeof body !== 'object' || Array.isArray(body)) {
25
28
  return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
26
29
  }
27
30
 
28
- const { title, content, tags, scope, agentIds } = body as Record<string, unknown>
29
-
30
- const updates: Record<string, unknown> = {}
31
- if (typeof title === 'string' && title.trim()) {
32
- updates.title = title.trim()
33
- }
34
- if (typeof content === 'string') {
35
- updates.content = content
36
- }
31
+ const payload = body as Record<string, unknown>
37
32
 
38
- const existingMeta = (existing.metadata || {}) as Record<string, unknown>
39
- const metaUpdates: Record<string, unknown> = { ...existingMeta }
33
+ try {
34
+ const detail = await updateKnowledgeSource(id, {
35
+ kind: inferKind(payload),
36
+ title: typeof payload.title === 'string' ? payload.title : undefined,
37
+ content: typeof payload.content === 'string' ? payload.content : undefined,
38
+ tags: Array.isArray(payload.tags) ? payload.tags.filter((tag): tag is string => typeof tag === 'string') : undefined,
39
+ scope: payload.scope === 'agent' ? 'agent' : payload.scope === 'global' ? 'global' : undefined,
40
+ agentIds: Array.isArray(payload.agentIds) ? payload.agentIds.filter((agentId): agentId is string => typeof agentId === 'string') : undefined,
41
+ sourceLabel: typeof payload.sourceLabel === 'string'
42
+ ? payload.sourceLabel
43
+ : typeof payload.source === 'string'
44
+ ? payload.source
45
+ : undefined,
46
+ sourceUrl: typeof payload.sourceUrl === 'string' ? payload.sourceUrl : undefined,
47
+ sourcePath: typeof payload.sourcePath === 'string'
48
+ ? payload.sourcePath
49
+ : typeof payload.filePath === 'string'
50
+ ? payload.filePath
51
+ : undefined,
52
+ metadata: payload.metadata && typeof payload.metadata === 'object' && !Array.isArray(payload.metadata)
53
+ ? payload.metadata as Record<string, unknown>
54
+ : undefined,
55
+ })
40
56
 
41
- if (Array.isArray(tags)) {
42
- const normalizedTags = (tags as unknown[]).filter(
43
- (t): t is string => typeof t === 'string' && t.trim().length > 0,
57
+ if (!detail) return notFound()
58
+ return NextResponse.json(detail)
59
+ } catch (error) {
60
+ return NextResponse.json(
61
+ { error: error instanceof Error ? error.message : 'Failed to update knowledge source.' },
62
+ { status: 400 },
44
63
  )
45
- metaUpdates.tags = normalizedTags
46
- }
47
-
48
- if (scope === 'global' || scope === 'agent') {
49
- metaUpdates.scope = scope
50
- metaUpdates.agentIds = scope === 'agent' && Array.isArray(agentIds)
51
- ? (agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
52
- : []
53
64
  }
54
-
55
- updates.metadata = metaUpdates
56
-
57
- const updated = db.update(id, updates)
58
- if (!updated) {
59
- return notFound()
60
- }
61
- return NextResponse.json(updated)
62
65
  }
63
66
 
64
67
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
65
68
  const { id } = await params
66
- const db = getMemoryDb()
67
- const existing = db.get(id)
68
- if (!existing || existing.category !== 'knowledge') {
69
- return notFound()
70
- }
71
- db.delete(id)
69
+ const deleted = await deleteKnowledgeSource(id)
70
+ if (!deleted) return notFound()
72
71
  return NextResponse.json({ deleted: id })
73
72
  }
@@ -0,0 +1,13 @@
1
+ import { NextResponse } from 'next/server'
2
+ import {
3
+ getKnowledgeHygieneSummary,
4
+ runKnowledgeHygieneMaintenance,
5
+ } from '@/lib/server/knowledge-sources'
6
+
7
+ export async function GET() {
8
+ return NextResponse.json(await getKnowledgeHygieneSummary())
9
+ }
10
+
11
+ export async function POST() {
12
+ return NextResponse.json(await runKnowledgeHygieneMaintenance())
13
+ }
@@ -1,22 +1,49 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { addKnowledge, searchKnowledge, listKnowledge } from '@/lib/server/memory/memory-db'
2
+ import {
3
+ createKnowledgeSource,
4
+ listKnowledgeSourceSummaries,
5
+ searchKnowledgeHits,
6
+ } from '@/lib/server/knowledge-sources'
7
+ import type { KnowledgeSourceKind } from '@/types'
8
+
9
+ function parseTags(raw: string | null): string[] | undefined {
10
+ if (!raw) return undefined
11
+ const tags = raw.split(',').map((tag) => tag.trim()).filter(Boolean)
12
+ return tags.length > 0 ? tags : undefined
13
+ }
14
+
15
+ function parseLimit(raw: string | null): number | undefined {
16
+ if (!raw) return undefined
17
+ return Math.max(1, Math.min(500, Number.parseInt(raw, 10) || 50))
18
+ }
19
+
20
+ function parseBool(raw: string | null): boolean {
21
+ if (!raw) return false
22
+ const value = raw.trim().toLowerCase()
23
+ return value === '1' || value === 'true' || value === 'yes'
24
+ }
25
+
26
+ function inferKind(body: Record<string, unknown>): KnowledgeSourceKind {
27
+ if (body.kind === 'file' || body.kind === 'url' || body.kind === 'manual') return body.kind
28
+ if (typeof body.sourcePath === 'string' && body.sourcePath.trim()) return 'file'
29
+ if (typeof body.sourceUrl === 'string' && body.sourceUrl.trim() && typeof body.content !== 'string') return 'url'
30
+ return 'manual'
31
+ }
3
32
 
4
33
  export async function GET(req: Request) {
5
34
  const { searchParams } = new URL(req.url)
6
- const q = searchParams.get('q')
7
- const tagsParam = searchParams.get('tags')
8
- const limitParam = searchParams.get('limit')
9
-
10
- const tags = tagsParam ? tagsParam.split(',').map((t) => t.trim()).filter(Boolean) : undefined
11
- const limit = limitParam ? Math.max(1, Math.min(500, Number.parseInt(limitParam, 10) || 50)) : undefined
35
+ const query = searchParams.get('q')
36
+ const tags = parseTags(searchParams.get('tags'))
37
+ const limit = parseLimit(searchParams.get('limit'))
38
+ const includeArchived = parseBool(searchParams.get('includeArchived'))
12
39
 
13
- if (q) {
14
- const results = searchKnowledge(q, tags, limit)
15
- return NextResponse.json(results)
40
+ if (query && query.trim()) {
41
+ const hits = await searchKnowledgeHits({ query, tags, limit, includeArchived })
42
+ return NextResponse.json(hits)
16
43
  }
17
44
 
18
- const entries = listKnowledge(tags, limit)
19
- return NextResponse.json(entries)
45
+ const sources = await listKnowledgeSourceSummaries({ tags, limit, includeArchived })
46
+ return NextResponse.json(sources)
20
47
  }
21
48
 
22
49
  export async function POST(req: Request) {
@@ -25,36 +52,37 @@ export async function POST(req: Request) {
25
52
  return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
26
53
  }
27
54
 
28
- const { title, content, tags, scope, agentIds, source, sourceUrl } = body as Record<string, unknown>
55
+ const payload = body as Record<string, unknown>
29
56
 
30
- if (typeof title !== 'string' || !title.trim()) {
31
- return NextResponse.json({ error: 'title is required.' }, { status: 400 })
32
- }
33
- if (typeof content !== 'string') {
34
- return NextResponse.json({ error: 'content is required.' }, { status: 400 })
35
- }
57
+ try {
58
+ const detail = await createKnowledgeSource({
59
+ kind: inferKind(payload),
60
+ title: typeof payload.title === 'string' ? payload.title : undefined,
61
+ content: typeof payload.content === 'string' ? payload.content : undefined,
62
+ tags: Array.isArray(payload.tags) ? payload.tags.filter((tag): tag is string => typeof tag === 'string') : undefined,
63
+ scope: payload.scope === 'agent' ? 'agent' : 'global',
64
+ agentIds: Array.isArray(payload.agentIds) ? payload.agentIds.filter((id): id is string => typeof id === 'string') : undefined,
65
+ sourceLabel: typeof payload.sourceLabel === 'string'
66
+ ? payload.sourceLabel
67
+ : typeof payload.source === 'string'
68
+ ? payload.source
69
+ : undefined,
70
+ sourceUrl: typeof payload.sourceUrl === 'string' ? payload.sourceUrl : undefined,
71
+ sourcePath: typeof payload.sourcePath === 'string'
72
+ ? payload.sourcePath
73
+ : typeof payload.filePath === 'string'
74
+ ? payload.filePath
75
+ : undefined,
76
+ metadata: payload.metadata && typeof payload.metadata === 'object' && !Array.isArray(payload.metadata)
77
+ ? payload.metadata as Record<string, unknown>
78
+ : undefined,
79
+ })
36
80
 
37
- const normalizedTags = Array.isArray(tags)
38
- ? (tags as unknown[]).filter((t): t is string => typeof t === 'string' && t.trim().length > 0)
39
- : undefined
40
-
41
- const normalizedScope = scope === 'agent' ? 'agent' as const : 'global' as const
42
- const normalizedAgentIds = Array.isArray(agentIds)
43
- ? (agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
44
- : []
45
-
46
- const normalizedSource = typeof source === 'string' && source.trim() ? source.trim() : undefined
47
- const normalizedSourceUrl = typeof sourceUrl === 'string' && sourceUrl.trim() ? sourceUrl.trim() : undefined
48
-
49
- const entry = addKnowledge({
50
- title: title.trim(),
51
- content,
52
- tags: normalizedTags,
53
- scope: normalizedScope,
54
- agentIds: normalizedAgentIds,
55
- source: normalizedSource,
56
- sourceUrl: normalizedSourceUrl,
57
- })
58
-
59
- return NextResponse.json(entry)
81
+ return NextResponse.json(detail)
82
+ } catch (error) {
83
+ return NextResponse.json(
84
+ { error: error instanceof Error ? error.message : 'Failed to create knowledge source.' },
85
+ { status: 400 },
86
+ )
87
+ }
60
88
  }
@@ -0,0 +1,15 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { notFound } from '@/lib/server/collection-helpers'
3
+ import { archiveKnowledgeSource } from '@/lib/server/knowledge-sources'
4
+
5
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const body = await req.json().catch(() => ({}))
8
+ const detail = await archiveKnowledgeSource(id, {
9
+ reason: typeof body?.reason === 'string' ? body.reason : null,
10
+ duplicateOfSourceId: typeof body?.duplicateOfSourceId === 'string' ? body.duplicateOfSourceId : null,
11
+ supersededBySourceId: typeof body?.supersededBySourceId === 'string' ? body.supersededBySourceId : null,
12
+ })
13
+ if (!detail) return notFound()
14
+ return NextResponse.json(detail)
15
+ }
@@ -0,0 +1,10 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { notFound } from '@/lib/server/collection-helpers'
3
+ import { restoreKnowledgeSource } from '@/lib/server/knowledge-sources'
4
+
5
+ export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const detail = await restoreKnowledgeSource(id)
8
+ if (!detail) return notFound()
9
+ return NextResponse.json(detail)
10
+ }
@@ -0,0 +1 @@
1
+ export { GET, PUT, DELETE } from '@/app/api/knowledge/[id]/route'
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { notFound } from '@/lib/server/collection-helpers'
3
+ import { supersedeKnowledgeSource } from '@/lib/server/knowledge-sources'
4
+
5
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const body = await req.json().catch(() => null)
8
+ const supersededBySourceId = typeof body === 'object' && body && !Array.isArray(body) && typeof body.supersededBySourceId === 'string'
9
+ ? body.supersededBySourceId
10
+ : ''
11
+
12
+ if (!supersededBySourceId.trim()) {
13
+ return NextResponse.json({ error: 'supersededBySourceId is required.' }, { status: 400 })
14
+ }
15
+
16
+ try {
17
+ const detail = await supersedeKnowledgeSource(id, supersededBySourceId)
18
+ if (!detail) return notFound()
19
+ return NextResponse.json(detail)
20
+ } catch (error) {
21
+ return NextResponse.json(
22
+ { error: error instanceof Error ? error.message : 'Failed to supersede knowledge source.' },
23
+ { status: 400 },
24
+ )
25
+ }
26
+ }
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { notFound } from '@/lib/server/collection-helpers'
3
+ import { syncKnowledgeSource } from '@/lib/server/knowledge-sources'
4
+
5
+ export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ try {
8
+ const detail = await syncKnowledgeSource(id)
9
+ if (!detail) return notFound()
10
+ return NextResponse.json(detail)
11
+ } catch (error) {
12
+ return NextResponse.json(
13
+ { error: error instanceof Error ? error.message : 'Failed to sync knowledge source.' },
14
+ { status: 400 },
15
+ )
16
+ }
17
+ }
@@ -0,0 +1 @@
1
+ export { GET, POST } from '@/app/api/knowledge/route'
@@ -3,28 +3,7 @@ import fs from 'fs'
3
3
  import path from 'path'
4
4
  import { genId } from '@/lib/id'
5
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
6
-
7
- const TEXT_EXTS = new Set([
8
- '.txt', '.md', '.markdown', '.csv', '.tsv', '.json', '.jsonl',
9
- '.html', '.htm', '.xml', '.yaml', '.yml', '.toml', '.ini', '.cfg',
10
- '.js', '', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.c', '.cpp', '.h',
11
- '.rb', '.php', '.sh', '.bash', '.zsh', '.sql', '.r', '.swift', '.kt',
12
- '.env', '.log', '.conf', '.properties', '.gitignore', '.dockerignore',
13
- ])
14
-
15
- function isTextFile(filename: string): boolean {
16
- const ext = path.extname(filename).toLowerCase()
17
- return TEXT_EXTS.has(ext) || ext === ''
18
- }
19
-
20
- function deriveTitle(filename: string): string {
21
- const name = path.basename(filename, path.extname(filename))
22
- return name
23
- .replace(/[-_]+/g, ' ')
24
- .replace(/([a-z])([A-Z])/g, '$1 $2')
25
- .replace(/\b\w/g, (c) => c.toUpperCase())
26
- .trim() || 'Uploaded Document'
27
- }
6
+ import { deriveKnowledgeTitle, extractKnowledgeTextFromBuffer } from '@/lib/server/knowledge-import'
28
7
 
29
8
  export async function POST(req: Request) {
30
9
  const filename = req.headers.get('x-filename') || 'document.txt'
@@ -44,35 +23,8 @@ export async function POST(req: Request) {
44
23
  const filePath = path.join(UPLOAD_DIR, safeName)
45
24
  fs.writeFileSync(filePath, buf)
46
25
 
47
- // Extract text content
48
- let content = ''
49
- const ext = path.extname(filename).toLowerCase()
50
-
51
- if (ext === '.pdf') {
52
- // Try dynamic import of pdf-parse if available
53
- try {
54
- // @ts-ignore — pdf-parse is an optional dependency
55
- const pdfParse = (await import(/* webpackIgnore: true */ 'pdf-parse')).default
56
- const result = await pdfParse(buf)
57
- content = result.text || ''
58
- } catch {
59
- // pdf-parse not installed — read as raw text fallback
60
- content = '[PDF document — install pdf-parse for text extraction]\n\nFile saved at: ' + filePath
61
- }
62
- } else if (isTextFile(filename)) {
63
- content = buf.toString('utf-8')
64
- } else {
65
- // Binary file — can't extract text
66
- content = `[Binary file: ${filename}]\n\nFile saved at: ${filePath}`
67
- }
68
-
69
- // Truncate very long content to prevent memory issues
70
- const MAX_CONTENT = 500_000
71
- if (content.length > MAX_CONTENT) {
72
- content = content.slice(0, MAX_CONTENT) + '\n\n[... truncated at 500k characters]'
73
- }
74
-
75
- const title = deriveTitle(filename)
26
+ const content = await extractKnowledgeTextFromBuffer(buf, filename, filePath)
27
+ const title = deriveKnowledgeTitle(filename)
76
28
  const url = `/api/uploads/${safeName}`
77
29
 
78
30
  return NextResponse.json({