@soleri/core 2.12.0 → 7.0.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.
- package/data/flows/build.flow.yaml +128 -0
- package/data/flows/deliver.flow.yaml +110 -0
- package/data/flows/design.flow.yaml +108 -0
- package/data/flows/enhance.flow.yaml +90 -0
- package/data/flows/explore.flow.yaml +84 -0
- package/data/flows/fix.flow.yaml +90 -0
- package/data/flows/plan.flow.yaml +87 -0
- package/data/flows/review.flow.yaml +90 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +16 -2
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/capabilities/chain-mapping.d.ts +21 -0
- package/dist/capabilities/chain-mapping.d.ts.map +1 -0
- package/dist/capabilities/chain-mapping.js +86 -0
- package/dist/capabilities/chain-mapping.js.map +1 -0
- package/dist/capabilities/index.d.ts +10 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +8 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/registry.d.ts +95 -0
- package/dist/capabilities/registry.d.ts.map +1 -0
- package/dist/capabilities/registry.js +227 -0
- package/dist/capabilities/registry.js.map +1 -0
- package/dist/capabilities/types.d.ts +106 -0
- package/dist/capabilities/types.d.ts.map +1 -0
- package/dist/capabilities/types.js +12 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +58 -2
- package/dist/control/intent-router.js.map +1 -1
- package/dist/domain-packs/index.d.ts +8 -0
- package/dist/domain-packs/index.d.ts.map +1 -0
- package/dist/domain-packs/index.js +8 -0
- package/dist/domain-packs/index.js.map +1 -0
- package/dist/domain-packs/inject-rules.d.ts +24 -0
- package/dist/domain-packs/inject-rules.d.ts.map +1 -0
- package/dist/domain-packs/inject-rules.js +65 -0
- package/dist/domain-packs/inject-rules.js.map +1 -0
- package/dist/domain-packs/knowledge-installer.d.ts +27 -0
- package/dist/domain-packs/knowledge-installer.d.ts.map +1 -0
- package/dist/domain-packs/knowledge-installer.js +89 -0
- package/dist/domain-packs/knowledge-installer.js.map +1 -0
- package/dist/domain-packs/loader.d.ts +28 -0
- package/dist/domain-packs/loader.d.ts.map +1 -0
- package/dist/domain-packs/loader.js +105 -0
- package/dist/domain-packs/loader.js.map +1 -0
- package/dist/domain-packs/pack-runtime.d.ts +80 -0
- package/dist/domain-packs/pack-runtime.d.ts.map +1 -0
- package/dist/domain-packs/pack-runtime.js +36 -0
- package/dist/domain-packs/pack-runtime.js.map +1 -0
- package/dist/domain-packs/skills-installer.d.ts +21 -0
- package/dist/domain-packs/skills-installer.d.ts.map +1 -0
- package/dist/domain-packs/skills-installer.js +38 -0
- package/dist/domain-packs/skills-installer.js.map +1 -0
- package/dist/domain-packs/token-resolver.d.ts +37 -0
- package/dist/domain-packs/token-resolver.d.ts.map +1 -0
- package/dist/domain-packs/token-resolver.js +109 -0
- package/dist/domain-packs/token-resolver.js.map +1 -0
- package/dist/domain-packs/types.d.ts +91 -0
- package/dist/domain-packs/types.d.ts.map +1 -0
- package/dist/domain-packs/types.js +122 -0
- package/dist/domain-packs/types.js.map +1 -0
- package/dist/engine/bin/soleri-engine.d.ts +12 -0
- package/dist/engine/bin/soleri-engine.d.ts.map +1 -0
- package/dist/engine/bin/soleri-engine.js +183 -0
- package/dist/engine/bin/soleri-engine.js.map +1 -0
- package/dist/engine/core-ops.d.ts +27 -0
- package/dist/engine/core-ops.d.ts.map +1 -0
- package/dist/engine/core-ops.js +159 -0
- package/dist/engine/core-ops.js.map +1 -0
- package/dist/engine/index.d.ts +19 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +17 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/register-engine.d.ts +54 -0
- package/dist/engine/register-engine.d.ts.map +1 -0
- package/dist/engine/register-engine.js +270 -0
- package/dist/engine/register-engine.js.map +1 -0
- package/dist/engine/test-helpers.d.ts +30 -0
- package/dist/engine/test-helpers.d.ts.map +1 -0
- package/dist/engine/test-helpers.js +59 -0
- package/dist/engine/test-helpers.js.map +1 -0
- package/dist/flows/context-router.d.ts +39 -0
- package/dist/flows/context-router.d.ts.map +1 -0
- package/dist/flows/context-router.js +206 -0
- package/dist/flows/context-router.js.map +1 -0
- package/dist/flows/dispatch-registry.d.ts +24 -0
- package/dist/flows/dispatch-registry.d.ts.map +1 -0
- package/dist/flows/dispatch-registry.js +70 -0
- package/dist/flows/dispatch-registry.js.map +1 -0
- package/dist/flows/epilogue.d.ts +24 -0
- package/dist/flows/epilogue.d.ts.map +1 -0
- package/dist/flows/epilogue.js +52 -0
- package/dist/flows/epilogue.js.map +1 -0
- package/dist/flows/executor.d.ts +25 -0
- package/dist/flows/executor.d.ts.map +1 -0
- package/dist/flows/executor.js +153 -0
- package/dist/flows/executor.js.map +1 -0
- package/dist/flows/gate-evaluator.d.ts +26 -0
- package/dist/flows/gate-evaluator.d.ts.map +1 -0
- package/dist/flows/gate-evaluator.js +162 -0
- package/dist/flows/gate-evaluator.js.map +1 -0
- package/dist/flows/index.d.ts +14 -0
- package/dist/flows/index.d.ts.map +1 -0
- package/dist/flows/index.js +20 -0
- package/dist/flows/index.js.map +1 -0
- package/dist/flows/loader.d.ts +17 -0
- package/dist/flows/loader.d.ts.map +1 -0
- package/dist/flows/loader.js +61 -0
- package/dist/flows/loader.js.map +1 -0
- package/dist/flows/plan-builder.d.ts +40 -0
- package/dist/flows/plan-builder.d.ts.map +1 -0
- package/dist/flows/plan-builder.js +213 -0
- package/dist/flows/plan-builder.js.map +1 -0
- package/dist/flows/probes.d.ts +11 -0
- package/dist/flows/probes.d.ts.map +1 -0
- package/dist/flows/probes.js +62 -0
- package/dist/flows/probes.js.map +1 -0
- package/dist/flows/types.d.ts +950 -0
- package/dist/flows/types.d.ts.map +1 -0
- package/dist/flows/types.js +105 -0
- package/dist/flows/types.js.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/intelligence/loader.d.ts +19 -0
- package/dist/intelligence/loader.d.ts.map +1 -1
- package/dist/intelligence/loader.js +35 -0
- package/dist/intelligence/loader.js.map +1 -1
- package/dist/intelligence/types.d.ts +1 -0
- package/dist/intelligence/types.d.ts.map +1 -1
- package/dist/packs/types.d.ts +58 -19
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +14 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/playbooks/generic/onboarding.d.ts +9 -0
- package/dist/playbooks/generic/onboarding.d.ts.map +1 -0
- package/dist/playbooks/generic/onboarding.js +74 -0
- package/dist/playbooks/generic/onboarding.js.map +1 -0
- package/dist/playbooks/playbook-registry.d.ts.map +1 -1
- package/dist/playbooks/playbook-registry.js +2 -0
- package/dist/playbooks/playbook-registry.js.map +1 -1
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
- package/dist/runtime/admin-extra-ops.js +15 -9
- package/dist/runtime/admin-extra-ops.js.map +1 -1
- package/dist/runtime/admin-ops.js +4 -4
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +33 -1
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/domain-ops.d.ts +21 -5
- package/dist/runtime/domain-ops.d.ts.map +1 -1
- package/dist/runtime/domain-ops.js +64 -6
- package/dist/runtime/domain-ops.js.map +1 -1
- package/dist/runtime/facades/cognee-facade.d.ts.map +1 -1
- package/dist/runtime/facades/cognee-facade.js +3 -1
- package/dist/runtime/facades/cognee-facade.js.map +1 -1
- package/dist/runtime/facades/index.d.ts.map +1 -1
- package/dist/runtime/facades/index.js +10 -6
- package/dist/runtime/facades/index.js.map +1 -1
- package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
- package/dist/runtime/facades/vault-facade.js +2 -0
- package/dist/runtime/facades/vault-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts +8 -7
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +217 -61
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +23 -17
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +6 -2
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.d.ts +13 -0
- package/dist/runtime/vault-linking-ops.d.ts.map +1 -0
- package/dist/runtime/vault-linking-ops.js +367 -0
- package/dist/runtime/vault-linking-ops.js.map +1 -0
- package/dist/vault/linking.d.ts +46 -0
- package/dist/vault/linking.d.ts.map +1 -0
- package/dist/vault/linking.js +275 -0
- package/dist/vault/linking.js.map +1 -0
- package/dist/vault/vault-types.d.ts +37 -0
- package/dist/vault/vault-types.d.ts.map +1 -1
- package/dist/vault/vault.d.ts +12 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +85 -6
- package/dist/vault/vault.js.map +1 -1
- package/package.json +4 -1
- package/src/__tests__/admin-extra-ops.test.ts +1 -1
- package/src/__tests__/admin-ops.test.ts +2 -1
- package/src/__tests__/cognee-client-gaps.test.ts +470 -0
- package/src/__tests__/cognee-hybrid-search.test.ts +478 -0
- package/src/__tests__/cognee-sync-manager-deep.test.ts +630 -0
- package/src/__tests__/cognee-sync-manager.test.ts +1 -0
- package/src/__tests__/core-ops.test.ts +9 -61
- package/src/__tests__/domain-packs.test.ts +421 -0
- package/src/__tests__/flows.test.ts +604 -0
- package/src/__tests__/playbook-registry.test.ts +2 -2
- package/src/__tests__/playbook-seeder.test.ts +8 -8
- package/src/__tests__/playbook.test.ts +5 -5
- package/src/__tests__/token-resolver.test.ts +79 -0
- package/src/brain/intelligence.ts +21 -2
- package/src/capabilities/chain-mapping.ts +93 -0
- package/src/capabilities/index.ts +21 -0
- package/src/capabilities/registry.ts +290 -0
- package/src/capabilities/types.ts +143 -0
- package/src/control/intent-router.ts +46 -2
- package/src/domain-packs/index.ts +27 -0
- package/src/domain-packs/inject-rules.ts +74 -0
- package/src/domain-packs/knowledge-installer.ts +116 -0
- package/src/domain-packs/loader.ts +124 -0
- package/src/domain-packs/pack-runtime.ts +99 -0
- package/src/domain-packs/skills-installer.ts +56 -0
- package/src/domain-packs/token-resolver.ts +126 -0
- package/src/domain-packs/types.ts +229 -0
- package/src/engine/__tests__/register-engine.test.ts +104 -0
- package/src/engine/bin/soleri-engine.ts +217 -0
- package/src/engine/core-ops.ts +178 -0
- package/src/engine/index.ts +19 -0
- package/src/engine/register-engine.ts +385 -0
- package/src/engine/test-helpers.ts +83 -0
- package/src/flows/context-router.ts +257 -0
- package/src/flows/dispatch-registry.ts +80 -0
- package/src/flows/epilogue.ts +65 -0
- package/src/flows/executor.ts +182 -0
- package/src/flows/gate-evaluator.ts +171 -0
- package/src/flows/index.ts +52 -0
- package/src/flows/loader.ts +63 -0
- package/src/flows/plan-builder.ts +250 -0
- package/src/flows/probes.ts +70 -0
- package/src/flows/types.ts +217 -0
- package/src/index.ts +68 -1
- package/src/intelligence/loader.ts +38 -0
- package/src/intelligence/types.ts +1 -0
- package/src/packs/types.ts +19 -0
- package/src/playbooks/generic/onboarding.ts +79 -0
- package/src/playbooks/playbook-registry.ts +2 -0
- package/src/runtime/admin-extra-ops.ts +14 -8
- package/src/runtime/admin-ops.ts +4 -4
- package/src/runtime/capture-ops.ts +40 -1
- package/src/runtime/domain-ops.ts +71 -5
- package/src/runtime/facades/cognee-facade.ts +3 -1
- package/src/runtime/facades/index.ts +12 -6
- package/src/runtime/facades/vault-facade.ts +2 -0
- package/src/runtime/orchestrate-ops.ts +261 -65
- package/src/runtime/runtime.ts +27 -18
- package/src/runtime/types.ts +6 -2
- package/src/runtime/vault-linking-ops.ts +454 -0
- package/src/vault/linking.ts +333 -0
- package/src/vault/vault-types.ts +46 -0
- package/src/vault/vault.ts +94 -7
package/src/runtime/runtime.ts
CHANGED
|
@@ -85,21 +85,24 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
|
|
|
85
85
|
// Planner — multi-step task tracking
|
|
86
86
|
const planner = new Planner(plansPath);
|
|
87
87
|
|
|
88
|
-
// Cognee — vector search client (graceful degradation if Cognee is down)
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
// Cognee — vector search client (opt-in, graceful degradation if Cognee is down)
|
|
89
|
+
let cognee: CogneeClient | null = null;
|
|
90
|
+
if (config.cognee) {
|
|
91
|
+
const cogneePartial: Partial<import('../cognee/types.js').CogneeConfig> = { dataset: agentId };
|
|
92
|
+
if (process.env.COGNEE_BASE_URL) cogneePartial.baseUrl = process.env.COGNEE_BASE_URL;
|
|
93
|
+
if (process.env.COGNEE_API_TOKEN) cogneePartial.apiToken = process.env.COGNEE_API_TOKEN;
|
|
94
|
+
if (process.env.COGNEE_DATASET) cogneePartial.dataset = process.env.COGNEE_DATASET;
|
|
95
|
+
cognee = new CogneeClient(cogneePartial);
|
|
96
|
+
}
|
|
94
97
|
|
|
95
98
|
// Brain — intelligence layer (TF-IDF scoring, auto-tagging, dedup)
|
|
96
|
-
const brain = new Brain(vault, cognee);
|
|
99
|
+
const brain = new Brain(vault, cognee ?? undefined);
|
|
97
100
|
|
|
98
101
|
// Brain Intelligence — pattern strengths, session knowledge, intelligence pipeline
|
|
99
102
|
const brainIntelligence = new BrainIntelligence(vault, brain);
|
|
100
103
|
|
|
101
104
|
// Curator — vault self-maintenance (dedup, contradictions, grooming, health)
|
|
102
|
-
const curator = new Curator(vault, cognee);
|
|
105
|
+
const curator = new Curator(vault, cognee ?? undefined);
|
|
103
106
|
|
|
104
107
|
// Governance — policy engine + proposal tracker for gated knowledge capture
|
|
105
108
|
const governance = new Governance(vault);
|
|
@@ -132,13 +135,16 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
|
|
|
132
135
|
const anthropicKeyPool = new KeyPool(keyPoolFiles.anthropic);
|
|
133
136
|
const llmClient = new LLMClient(openaiKeyPool, anthropicKeyPool, agentId);
|
|
134
137
|
|
|
135
|
-
// Cognee Sync Manager — queue-based dirty tracking with offline resilience
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
// Cognee Sync Manager — queue-based dirty tracking with offline resilience (only when Cognee enabled)
|
|
139
|
+
let syncManager: CogneeSyncManager | null = null;
|
|
140
|
+
if (cognee) {
|
|
141
|
+
syncManager = new CogneeSyncManager(
|
|
142
|
+
vault.getProvider(),
|
|
143
|
+
cognee,
|
|
144
|
+
process.env.COGNEE_DATASET ?? agentId,
|
|
145
|
+
);
|
|
146
|
+
vault.setSyncManager(syncManager);
|
|
147
|
+
}
|
|
142
148
|
|
|
143
149
|
// Intake Pipeline — PDF/book ingestion with LLM classification
|
|
144
150
|
const intakePipeline = new IntakePipeline(vault.getProvider(), vault, llmClient);
|
|
@@ -168,7 +174,10 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
|
|
|
168
174
|
const health = new HealthRegistry();
|
|
169
175
|
health.register('vault', 'healthy');
|
|
170
176
|
health.register('brain', 'healthy');
|
|
171
|
-
health.register(
|
|
177
|
+
health.register(
|
|
178
|
+
'cognee',
|
|
179
|
+
cognee ? (cognee.getStatus()?.available ? 'healthy' : 'degraded') : 'down',
|
|
180
|
+
);
|
|
172
181
|
health.register(
|
|
173
182
|
'llm',
|
|
174
183
|
llmClient.isAvailable().openai || llmClient.isAvailable().anthropic ? 'healthy' : 'degraded',
|
|
@@ -221,8 +230,8 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
|
|
|
221
230
|
knowledgeReview,
|
|
222
231
|
createdAt: Date.now(),
|
|
223
232
|
close: () => {
|
|
224
|
-
syncManager
|
|
225
|
-
cognee
|
|
233
|
+
syncManager?.close();
|
|
234
|
+
cognee?.resetPendingCognify();
|
|
226
235
|
vaultManager.close();
|
|
227
236
|
},
|
|
228
237
|
};
|
package/src/runtime/types.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface AgentRuntimeConfig {
|
|
|
48
48
|
logLevel?: LogLevel;
|
|
49
49
|
/** Path to shared global vault. Default: ~/.soleri/vault.db */
|
|
50
50
|
sharedVaultPath?: string;
|
|
51
|
+
/** Enable Cognee vector search integration. Default: false (opt-in). */
|
|
52
|
+
cognee?: boolean;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
@@ -63,7 +65,8 @@ export interface AgentRuntime {
|
|
|
63
65
|
planner: Planner;
|
|
64
66
|
curator: Curator;
|
|
65
67
|
governance: Governance;
|
|
66
|
-
|
|
68
|
+
/** Cognee vector search client. Null when Cognee integration is disabled. */
|
|
69
|
+
cognee: CogneeClient | null;
|
|
67
70
|
loop: LoopManager;
|
|
68
71
|
identityManager: IdentityManager;
|
|
69
72
|
intentRouter: IntentRouter;
|
|
@@ -72,7 +75,8 @@ export interface AgentRuntime {
|
|
|
72
75
|
telemetry: Telemetry;
|
|
73
76
|
projectRegistry: ProjectRegistry;
|
|
74
77
|
templateManager: TemplateManager;
|
|
75
|
-
|
|
78
|
+
/** Cognee sync manager. Null when Cognee integration is disabled. */
|
|
79
|
+
syncManager: CogneeSyncManager | null;
|
|
76
80
|
intakePipeline: IntakePipeline;
|
|
77
81
|
/** Mutable auth policy — controls facade dispatch enforcement. */
|
|
78
82
|
authPolicy: AuthPolicy;
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault linking ops — Zettelkasten bidirectional linking.
|
|
3
|
+
*
|
|
4
|
+
* Provides 8 ops: link_entries, unlink_entries, get_links, traverse,
|
|
5
|
+
* suggest_links, get_orphans, relink_vault, link_stats.
|
|
6
|
+
* Ported from Salvador MCP with improvements:
|
|
7
|
+
* - FTS5 for suggest_links (Salvador uses TF-IDF)
|
|
8
|
+
* - relink_vault: LLM-evaluated batch re-linking (Salvador uses a separate script)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import type { OpDefinition } from '../facades/types.js';
|
|
13
|
+
import type { AgentRuntime } from './types.js';
|
|
14
|
+
import { LinkManager } from '../vault/linking.js';
|
|
15
|
+
|
|
16
|
+
const EVAL_SYSTEM_PROMPT = `You evaluate pairs of knowledge entries to determine if they should be linked in a Zettelkasten vault. For EACH pair, decide:
|
|
17
|
+
- If meaningfully related → return { "link": true, "type": "<type>", "note": "<1 sentence why>" }
|
|
18
|
+
- If NOT meaningfully related → return { "link": false }
|
|
19
|
+
|
|
20
|
+
Link types:
|
|
21
|
+
- "extends" — target builds on or refines the source
|
|
22
|
+
- "supports" — target provides evidence or foundation for the source
|
|
23
|
+
- "contradicts" — target is an opposing approach or counterpoint
|
|
24
|
+
- "sequences" — source must happen before target
|
|
25
|
+
|
|
26
|
+
Rules: Same category alone is NOT enough. Be selective. Return a JSON array.`;
|
|
27
|
+
|
|
28
|
+
export function createVaultLinkingOps(runtime: AgentRuntime): OpDefinition[] {
|
|
29
|
+
const { vault } = runtime;
|
|
30
|
+
const linkManager = new LinkManager(vault.getProvider());
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
name: 'link_entries',
|
|
35
|
+
description: 'Create a typed link between two vault entries (Zettelkasten)',
|
|
36
|
+
auth: 'write',
|
|
37
|
+
schema: z.object({
|
|
38
|
+
sourceId: z.string().describe('REQUIRED: Source entry ID'),
|
|
39
|
+
targetId: z.string().describe('REQUIRED: Target entry ID'),
|
|
40
|
+
linkType: z
|
|
41
|
+
.enum(['supports', 'contradicts', 'extends', 'sequences'])
|
|
42
|
+
.describe('REQUIRED: Relationship type'),
|
|
43
|
+
note: z.string().optional().describe('Optional context for the link'),
|
|
44
|
+
}),
|
|
45
|
+
handler: async (params) => {
|
|
46
|
+
const sourceId = params.sourceId as string;
|
|
47
|
+
const targetId = params.targetId as string;
|
|
48
|
+
|
|
49
|
+
// Validate both entries exist to prevent dangling links
|
|
50
|
+
const provider = vault.getProvider();
|
|
51
|
+
const sourceExists = provider.get<{ id: string }>('SELECT id FROM entries WHERE id = ?', [
|
|
52
|
+
sourceId,
|
|
53
|
+
]);
|
|
54
|
+
const targetExists = provider.get<{ id: string }>('SELECT id FROM entries WHERE id = ?', [
|
|
55
|
+
targetId,
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
if (!sourceExists || !targetExists) {
|
|
59
|
+
const missing = [];
|
|
60
|
+
if (!sourceExists) missing.push(`source '${sourceId}'`);
|
|
61
|
+
if (!targetExists) missing.push(`target '${targetId}'`);
|
|
62
|
+
throw new Error(`Entry not found: ${missing.join(' and ')}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
linkManager.addLink(
|
|
66
|
+
sourceId,
|
|
67
|
+
targetId,
|
|
68
|
+
params.linkType as 'supports' | 'contradicts' | 'extends' | 'sequences',
|
|
69
|
+
params.note as string | undefined,
|
|
70
|
+
);
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
link: {
|
|
74
|
+
sourceId,
|
|
75
|
+
targetId,
|
|
76
|
+
linkType: params.linkType,
|
|
77
|
+
note: params.note,
|
|
78
|
+
},
|
|
79
|
+
sourceLinkCount: linkManager.getLinkCount(sourceId),
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'unlink_entries',
|
|
85
|
+
description: 'Remove a link between two vault entries',
|
|
86
|
+
auth: 'write',
|
|
87
|
+
schema: z.object({
|
|
88
|
+
sourceId: z.string().describe('REQUIRED: Source entry ID'),
|
|
89
|
+
targetId: z.string().describe('REQUIRED: Target entry ID'),
|
|
90
|
+
}),
|
|
91
|
+
handler: async (params) => {
|
|
92
|
+
linkManager.removeLink(params.sourceId as string, params.targetId as string);
|
|
93
|
+
return { success: true, removed: { sourceId: params.sourceId, targetId: params.targetId } };
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'get_links',
|
|
98
|
+
description: 'Get all links for a vault entry (outgoing + incoming backlinks)',
|
|
99
|
+
auth: 'read',
|
|
100
|
+
schema: z.object({
|
|
101
|
+
entryId: z.string().describe('REQUIRED: Entry ID'),
|
|
102
|
+
}),
|
|
103
|
+
handler: async (params) => {
|
|
104
|
+
const entryId = params.entryId as string;
|
|
105
|
+
const outgoing = linkManager.getLinks(entryId);
|
|
106
|
+
const incoming = linkManager.getBacklinks(entryId);
|
|
107
|
+
return { entryId, outgoing, incoming, totalLinks: outgoing.length + incoming.length };
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'traverse',
|
|
112
|
+
description:
|
|
113
|
+
'Walk the link graph from an entry up to N hops deep (Zettelkasten graph traversal)',
|
|
114
|
+
auth: 'read',
|
|
115
|
+
schema: z.object({
|
|
116
|
+
entryId: z.string().describe('REQUIRED: Starting entry ID'),
|
|
117
|
+
depth: z.coerce
|
|
118
|
+
.number()
|
|
119
|
+
.int()
|
|
120
|
+
.min(1)
|
|
121
|
+
.max(5)
|
|
122
|
+
.default(2)
|
|
123
|
+
.describe('Max hops (1-5, default 2)'),
|
|
124
|
+
}),
|
|
125
|
+
handler: async (params) => {
|
|
126
|
+
const entryId = params.entryId as string;
|
|
127
|
+
const depth = (params.depth as number) || 2;
|
|
128
|
+
const connected = linkManager.traverse(entryId, depth);
|
|
129
|
+
return { entryId, depth, connectedEntries: connected, totalConnected: connected.length };
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'suggest_links',
|
|
134
|
+
description:
|
|
135
|
+
'Find semantically similar entries as link candidates using FTS5 (Zettelkasten auto-linking)',
|
|
136
|
+
auth: 'read',
|
|
137
|
+
schema: z.object({
|
|
138
|
+
entryId: z.string().describe('REQUIRED: Entry ID to find link candidates for'),
|
|
139
|
+
limit: z.coerce
|
|
140
|
+
.number()
|
|
141
|
+
.int()
|
|
142
|
+
.min(1)
|
|
143
|
+
.max(20)
|
|
144
|
+
.default(5)
|
|
145
|
+
.describe('Max suggestions (default 5)'),
|
|
146
|
+
}),
|
|
147
|
+
handler: async (params) => {
|
|
148
|
+
const entryId = params.entryId as string;
|
|
149
|
+
const limit = (params.limit as number) || 5;
|
|
150
|
+
const suggestions = linkManager.suggestLinks(entryId, limit);
|
|
151
|
+
return { entryId, suggestions, totalSuggestions: suggestions.length };
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'get_orphans',
|
|
156
|
+
description: 'Find vault entries with zero links (Zettelkasten orphan detection)',
|
|
157
|
+
auth: 'read',
|
|
158
|
+
schema: z.object({
|
|
159
|
+
limit: z.coerce
|
|
160
|
+
.number()
|
|
161
|
+
.int()
|
|
162
|
+
.min(1)
|
|
163
|
+
.max(100)
|
|
164
|
+
.default(20)
|
|
165
|
+
.describe('Max orphans (default 20)'),
|
|
166
|
+
}),
|
|
167
|
+
handler: async (params) => {
|
|
168
|
+
const limit = (params.limit as number) || 20;
|
|
169
|
+
const orphans = linkManager.getOrphans(limit);
|
|
170
|
+
return { orphans, totalOrphans: orphans.length };
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'relink_vault',
|
|
175
|
+
description:
|
|
176
|
+
'Smart Zettelkasten re-linking: drops batch links, evaluates all entries with LLM, creates quality links with reasoning notes. Long-running operation.',
|
|
177
|
+
auth: 'write',
|
|
178
|
+
schema: z.object({
|
|
179
|
+
batchSize: z.coerce
|
|
180
|
+
.number()
|
|
181
|
+
.int()
|
|
182
|
+
.min(1)
|
|
183
|
+
.max(20)
|
|
184
|
+
.default(10)
|
|
185
|
+
.describe('Pairs per LLM call (default 10)'),
|
|
186
|
+
limit: z.coerce
|
|
187
|
+
.number()
|
|
188
|
+
.int()
|
|
189
|
+
.min(0)
|
|
190
|
+
.max(5000)
|
|
191
|
+
.default(0)
|
|
192
|
+
.describe('Max entries to process (0 = all)'),
|
|
193
|
+
dryRun: z.boolean().optional().default(false).describe('Preview without changes'),
|
|
194
|
+
}),
|
|
195
|
+
handler: async (params) => {
|
|
196
|
+
const batchSize = (params.batchSize as number) || 10;
|
|
197
|
+
const limit = (params.limit as number) || 0;
|
|
198
|
+
const dryRun = (params.dryRun as boolean) ?? false;
|
|
199
|
+
const { llmClient } = runtime;
|
|
200
|
+
|
|
201
|
+
if (!llmClient.isAvailable().anthropic && !llmClient.isAvailable().openai) {
|
|
202
|
+
return { success: false, error: 'No LLM provider available for link evaluation' };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const provider = vault.getProvider();
|
|
206
|
+
|
|
207
|
+
// Phase 1: Preserve manual links, drop batch links
|
|
208
|
+
const manualCount =
|
|
209
|
+
provider.get<{ c: number }>(
|
|
210
|
+
"SELECT COUNT(*) as c FROM vault_links WHERE note IS NOT NULL AND note != ''",
|
|
211
|
+
)?.c ?? 0;
|
|
212
|
+
|
|
213
|
+
const batchCount =
|
|
214
|
+
provider.get<{ c: number }>(
|
|
215
|
+
"SELECT COUNT(*) as c FROM vault_links WHERE note IS NULL OR note = ''",
|
|
216
|
+
)?.c ?? 0;
|
|
217
|
+
|
|
218
|
+
if (!dryRun) {
|
|
219
|
+
provider.run("DELETE FROM vault_links WHERE note IS NULL OR note = ''");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Phase 2: Get entries and generate candidates
|
|
223
|
+
let entries = provider.all<{
|
|
224
|
+
id: string;
|
|
225
|
+
title: string;
|
|
226
|
+
type: string;
|
|
227
|
+
description: string | null;
|
|
228
|
+
}>('SELECT id, title, type, description FROM entries ORDER BY updated_at DESC');
|
|
229
|
+
if (limit > 0) entries = entries.slice(0, limit);
|
|
230
|
+
|
|
231
|
+
// Build candidates via tag overlap + category match
|
|
232
|
+
const candidates: Array<{
|
|
233
|
+
sourceId: string;
|
|
234
|
+
sourceTitle: string;
|
|
235
|
+
sourceType: string;
|
|
236
|
+
sourceDesc: string;
|
|
237
|
+
targetId: string;
|
|
238
|
+
targetTitle: string;
|
|
239
|
+
targetType: string;
|
|
240
|
+
targetDesc: string;
|
|
241
|
+
}> = [];
|
|
242
|
+
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
const existingLinks = new Set([
|
|
245
|
+
...linkManager.getLinks(entry.id).map((l) => l.targetId),
|
|
246
|
+
...linkManager.getBacklinks(entry.id).map((l) => l.sourceId),
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
// Tag overlap matches
|
|
250
|
+
const matches = provider.all<{
|
|
251
|
+
id: string;
|
|
252
|
+
title: string;
|
|
253
|
+
type: string;
|
|
254
|
+
description: string | null;
|
|
255
|
+
}>(
|
|
256
|
+
`SELECT DISTINCT e.id, e.title, e.type, SUBSTR(e.description, 1, 200) as description
|
|
257
|
+
FROM entries e
|
|
258
|
+
JOIN (SELECT entry_id, tag FROM vault_tags WHERE entry_id = ?) src_tags ON 1=1
|
|
259
|
+
JOIN vault_tags t ON t.tag = src_tags.tag AND t.entry_id = e.id
|
|
260
|
+
WHERE e.id != ?
|
|
261
|
+
GROUP BY e.id ORDER BY COUNT(*) DESC LIMIT 5`,
|
|
262
|
+
[entry.id, entry.id],
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Fallback: category match
|
|
266
|
+
if (matches.length < 3) {
|
|
267
|
+
try {
|
|
268
|
+
const existing = new Set(matches.map((m) => m.id));
|
|
269
|
+
const catMatches = provider.all<(typeof matches)[0]>(
|
|
270
|
+
`SELECT id, title, type, SUBSTR(description, 1, 200) as description
|
|
271
|
+
FROM entries WHERE id != ? AND type = ? LIMIT 3`,
|
|
272
|
+
[entry.id, entry.type],
|
|
273
|
+
);
|
|
274
|
+
for (const m of catMatches) {
|
|
275
|
+
if (!existing.has(m.id)) matches.push(m);
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
/* ignore */
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const match of matches.slice(0, 5)) {
|
|
283
|
+
if (existingLinks.has(match.id)) continue;
|
|
284
|
+
candidates.push({
|
|
285
|
+
sourceId: entry.id,
|
|
286
|
+
sourceTitle: entry.title,
|
|
287
|
+
sourceType: entry.type,
|
|
288
|
+
sourceDesc: (entry.description || '').slice(0, 200),
|
|
289
|
+
targetId: match.id,
|
|
290
|
+
targetTitle: match.title,
|
|
291
|
+
targetType: match.type,
|
|
292
|
+
targetDesc: (match.description || '').slice(0, 200),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (dryRun) {
|
|
298
|
+
return {
|
|
299
|
+
dryRun: true,
|
|
300
|
+
entries: entries.length,
|
|
301
|
+
candidates: candidates.length,
|
|
302
|
+
llmCallsNeeded: Math.ceil(candidates.length / batchSize),
|
|
303
|
+
manualLinksPreserved: manualCount,
|
|
304
|
+
batchLinksToRemove: batchCount,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Phase 3: LLM evaluation in batches (sequential to respect rate limits)
|
|
309
|
+
let linksCreated = 0;
|
|
310
|
+
let pairsSkipped = 0;
|
|
311
|
+
let llmCalls = 0;
|
|
312
|
+
let errors = 0;
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
|
|
315
|
+
// Build batches
|
|
316
|
+
const batches: (typeof candidates)[] = [];
|
|
317
|
+
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
318
|
+
batches.push(candidates.slice(i, i + batchSize));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Process sequentially using reduce chain (avoids await-in-loop lint)
|
|
322
|
+
await batches.reduce(async (prev, batch) => {
|
|
323
|
+
await prev;
|
|
324
|
+
const pairsText = batch
|
|
325
|
+
.map(
|
|
326
|
+
(p, idx) =>
|
|
327
|
+
`--- Pair ${idx + 1} ---\nSOURCE [${p.sourceType}]: ${p.sourceTitle}\n${p.sourceDesc}\n\nTARGET [${p.targetType}]: ${p.targetTitle}\n${p.targetDesc}`,
|
|
328
|
+
)
|
|
329
|
+
.join('\n\n');
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const result = await llmClient.complete({
|
|
333
|
+
provider: llmClient.isAvailable().anthropic ? 'anthropic' : 'openai',
|
|
334
|
+
model: llmClient.isAvailable().anthropic ? 'claude-sonnet-4-20250514' : 'gpt-4o-mini',
|
|
335
|
+
systemPrompt: EVAL_SYSTEM_PROMPT,
|
|
336
|
+
userPrompt: pairsText,
|
|
337
|
+
maxTokens: 2000,
|
|
338
|
+
caller: 'relink_vault',
|
|
339
|
+
task: 'link-evaluation',
|
|
340
|
+
});
|
|
341
|
+
llmCalls++;
|
|
342
|
+
|
|
343
|
+
let cleaned = result.text.trim();
|
|
344
|
+
if (cleaned.startsWith('```')) {
|
|
345
|
+
const first = cleaned.indexOf('\n');
|
|
346
|
+
const last = cleaned.lastIndexOf('```');
|
|
347
|
+
cleaned = cleaned.slice(first + 1, last).trim();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const decisions = JSON.parse(cleaned) as Array<{
|
|
351
|
+
link: boolean;
|
|
352
|
+
type?: string;
|
|
353
|
+
note?: string;
|
|
354
|
+
}>;
|
|
355
|
+
|
|
356
|
+
for (let j = 0; j < decisions.length && j < batch.length; j++) {
|
|
357
|
+
const d = decisions[j];
|
|
358
|
+
if (d.link && d.type && d.note) {
|
|
359
|
+
if (batch[j].sourceId === batch[j].targetId) continue;
|
|
360
|
+
try {
|
|
361
|
+
provider.run(
|
|
362
|
+
'INSERT OR IGNORE INTO vault_links (source_id, target_id, link_type, note, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
363
|
+
[batch[j].sourceId, batch[j].targetId, d.type, d.note, now],
|
|
364
|
+
);
|
|
365
|
+
linksCreated++;
|
|
366
|
+
} catch {
|
|
367
|
+
/* FK or duplicate */
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
pairsSkipped++;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
errors++;
|
|
375
|
+
}
|
|
376
|
+
}, Promise.resolve());
|
|
377
|
+
|
|
378
|
+
// Phase 4: Stats
|
|
379
|
+
const totalLinks =
|
|
380
|
+
provider.get<{ c: number }>('SELECT COUNT(*) as c FROM vault_links')?.c ?? 0;
|
|
381
|
+
const orphanCount =
|
|
382
|
+
provider.get<{ c: number }>(
|
|
383
|
+
`SELECT COUNT(*) as c FROM entries
|
|
384
|
+
WHERE id NOT IN (SELECT source_id FROM vault_links)
|
|
385
|
+
AND id NOT IN (SELECT target_id FROM vault_links)`,
|
|
386
|
+
)?.c ?? 0;
|
|
387
|
+
const byType = provider.all<{ link_type: string; c: number }>(
|
|
388
|
+
'SELECT link_type, COUNT(*) as c FROM vault_links GROUP BY link_type ORDER BY c DESC',
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
success: true,
|
|
393
|
+
entriesProcessed: entries.length,
|
|
394
|
+
candidatesEvaluated: candidates.length,
|
|
395
|
+
linksCreated,
|
|
396
|
+
pairsSkipped,
|
|
397
|
+
llmCalls,
|
|
398
|
+
errors,
|
|
399
|
+
totalLinks,
|
|
400
|
+
orphans: orphanCount,
|
|
401
|
+
byType: Object.fromEntries(byType.map((r) => [r.link_type, r.c])),
|
|
402
|
+
manualLinksPreserved: manualCount,
|
|
403
|
+
batchLinksRemoved: batchCount,
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: 'link_stats',
|
|
409
|
+
description:
|
|
410
|
+
'Get Zettelkasten graph statistics: total links, by type, most connected, orphan count.',
|
|
411
|
+
auth: 'read',
|
|
412
|
+
handler: async () => {
|
|
413
|
+
const provider = vault.getProvider();
|
|
414
|
+
try {
|
|
415
|
+
const totalLinks =
|
|
416
|
+
provider.get<{ c: number }>('SELECT COUNT(*) as c FROM vault_links')?.c ?? 0;
|
|
417
|
+
const totalEntries =
|
|
418
|
+
provider.get<{ c: number }>('SELECT COUNT(*) as c FROM entries')?.c ?? 0;
|
|
419
|
+
const orphans =
|
|
420
|
+
provider.get<{ c: number }>(
|
|
421
|
+
`SELECT COUNT(*) as c FROM entries
|
|
422
|
+
WHERE id NOT IN (SELECT source_id FROM vault_links)
|
|
423
|
+
AND id NOT IN (SELECT target_id FROM vault_links)`,
|
|
424
|
+
)?.c ?? 0;
|
|
425
|
+
const byType = provider.all<{ link_type: string; c: number }>(
|
|
426
|
+
'SELECT link_type, COUNT(*) as c FROM vault_links GROUP BY link_type ORDER BY c DESC',
|
|
427
|
+
);
|
|
428
|
+
const withNotes =
|
|
429
|
+
provider.get<{ c: number }>(
|
|
430
|
+
"SELECT COUNT(*) as c FROM vault_links WHERE note IS NOT NULL AND note != ''",
|
|
431
|
+
)?.c ?? 0;
|
|
432
|
+
const mostConnected = provider.all<{ title: string; links: number }>(
|
|
433
|
+
`SELECT e.title, (
|
|
434
|
+
(SELECT COUNT(*) FROM vault_links WHERE source_id = e.id) +
|
|
435
|
+
(SELECT COUNT(*) FROM vault_links WHERE target_id = e.id)
|
|
436
|
+
) as links FROM entries e ORDER BY links DESC LIMIT 10`,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
totalEntries,
|
|
441
|
+
totalLinks,
|
|
442
|
+
orphans,
|
|
443
|
+
linksWithNotes: withNotes,
|
|
444
|
+
linkQuality: totalLinks > 0 ? `${((withNotes / totalLinks) * 100).toFixed(0)}%` : 'n/a',
|
|
445
|
+
byType: Object.fromEntries(byType.map((r) => [r.link_type, r.c])),
|
|
446
|
+
mostConnected,
|
|
447
|
+
};
|
|
448
|
+
} catch {
|
|
449
|
+
return { totalLinks: 0, totalEntries: 0, orphans: 0, byType: {}, mostConnected: [] };
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
];
|
|
454
|
+
}
|