@soleri/core 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/extensions/index.d.ts +3 -0
  2. package/dist/extensions/index.d.ts.map +1 -0
  3. package/dist/extensions/index.js +2 -0
  4. package/dist/extensions/index.js.map +1 -0
  5. package/dist/extensions/middleware.d.ts +13 -0
  6. package/dist/extensions/middleware.d.ts.map +1 -0
  7. package/dist/extensions/middleware.js +47 -0
  8. package/dist/extensions/middleware.js.map +1 -0
  9. package/dist/extensions/types.d.ts +64 -0
  10. package/dist/extensions/types.d.ts.map +1 -0
  11. package/dist/extensions/types.js +2 -0
  12. package/dist/extensions/types.js.map +1 -0
  13. package/dist/index.d.ts +8 -16
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +7 -16
  16. package/dist/index.js.map +1 -1
  17. package/dist/planning/gap-analysis.d.ts.map +1 -1
  18. package/dist/planning/gap-analysis.js +3 -1
  19. package/dist/planning/gap-analysis.js.map +1 -1
  20. package/dist/runtime/core-ops.d.ts +1 -1
  21. package/dist/runtime/core-ops.js +1 -1
  22. package/dist/runtime/facades/admin-facade.d.ts +8 -0
  23. package/dist/runtime/facades/admin-facade.d.ts.map +1 -0
  24. package/dist/runtime/facades/admin-facade.js +90 -0
  25. package/dist/runtime/facades/admin-facade.js.map +1 -0
  26. package/dist/runtime/facades/brain-facade.d.ts +8 -0
  27. package/dist/runtime/facades/brain-facade.d.ts.map +1 -0
  28. package/dist/runtime/facades/brain-facade.js +294 -0
  29. package/dist/runtime/facades/brain-facade.js.map +1 -0
  30. package/dist/runtime/facades/cognee-facade.d.ts +8 -0
  31. package/dist/runtime/facades/cognee-facade.d.ts.map +1 -0
  32. package/dist/runtime/facades/cognee-facade.js +154 -0
  33. package/dist/runtime/facades/cognee-facade.js.map +1 -0
  34. package/dist/runtime/facades/control-facade.d.ts +8 -0
  35. package/dist/runtime/facades/control-facade.d.ts.map +1 -0
  36. package/dist/runtime/facades/control-facade.js +244 -0
  37. package/dist/runtime/facades/control-facade.js.map +1 -0
  38. package/dist/runtime/facades/curator-facade.d.ts +8 -0
  39. package/dist/runtime/facades/curator-facade.d.ts.map +1 -0
  40. package/dist/runtime/facades/curator-facade.js +117 -0
  41. package/dist/runtime/facades/curator-facade.js.map +1 -0
  42. package/dist/runtime/facades/index.d.ts +10 -0
  43. package/dist/runtime/facades/index.d.ts.map +1 -0
  44. package/dist/runtime/facades/index.js +71 -0
  45. package/dist/runtime/facades/index.js.map +1 -0
  46. package/dist/runtime/facades/loop-facade.d.ts +8 -0
  47. package/dist/runtime/facades/loop-facade.d.ts.map +1 -0
  48. package/dist/runtime/facades/loop-facade.js +9 -0
  49. package/dist/runtime/facades/loop-facade.js.map +1 -0
  50. package/dist/runtime/facades/memory-facade.d.ts +8 -0
  51. package/dist/runtime/facades/memory-facade.d.ts.map +1 -0
  52. package/dist/runtime/facades/memory-facade.js +108 -0
  53. package/dist/runtime/facades/memory-facade.js.map +1 -0
  54. package/dist/runtime/facades/orchestrate-facade.d.ts +8 -0
  55. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -0
  56. package/dist/runtime/facades/orchestrate-facade.js +58 -0
  57. package/dist/runtime/facades/orchestrate-facade.js.map +1 -0
  58. package/dist/runtime/facades/plan-facade.d.ts +8 -0
  59. package/dist/runtime/facades/plan-facade.d.ts.map +1 -0
  60. package/dist/runtime/facades/plan-facade.js +110 -0
  61. package/dist/runtime/facades/plan-facade.js.map +1 -0
  62. package/dist/runtime/facades/vault-facade.d.ts +8 -0
  63. package/dist/runtime/facades/vault-facade.d.ts.map +1 -0
  64. package/dist/runtime/facades/vault-facade.js +194 -0
  65. package/dist/runtime/facades/vault-facade.js.map +1 -0
  66. package/dist/runtime/vault-extra-ops.d.ts +2 -2
  67. package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
  68. package/dist/runtime/vault-extra-ops.js +37 -2
  69. package/dist/runtime/vault-extra-ops.js.map +1 -1
  70. package/dist/streams/index.d.ts +4 -0
  71. package/dist/streams/index.d.ts.map +1 -0
  72. package/dist/streams/index.js +3 -0
  73. package/dist/streams/index.js.map +1 -0
  74. package/dist/streams/normalize.d.ts +14 -0
  75. package/dist/streams/normalize.d.ts.map +1 -0
  76. package/dist/streams/normalize.js +43 -0
  77. package/dist/streams/normalize.js.map +1 -0
  78. package/dist/streams/replayable-stream.d.ts +19 -0
  79. package/dist/streams/replayable-stream.d.ts.map +1 -0
  80. package/dist/streams/replayable-stream.js +90 -0
  81. package/dist/streams/replayable-stream.js.map +1 -0
  82. package/dist/vault/content-hash.d.ts +16 -0
  83. package/dist/vault/content-hash.d.ts.map +1 -0
  84. package/dist/vault/content-hash.js +21 -0
  85. package/dist/vault/content-hash.js.map +1 -0
  86. package/dist/vault/vault.d.ts +9 -0
  87. package/dist/vault/vault.d.ts.map +1 -1
  88. package/dist/vault/vault.js +49 -3
  89. package/dist/vault/vault.js.map +1 -1
  90. package/package.json +1 -1
  91. package/src/__tests__/content-hash.test.ts +60 -0
  92. package/src/__tests__/core-ops.test.ts +10 -7
  93. package/src/__tests__/extensions.test.ts +233 -0
  94. package/src/__tests__/grading-ops.test.ts +2 -2
  95. package/src/__tests__/memory-cross-project-ops.test.ts +2 -2
  96. package/src/__tests__/normalize.test.ts +75 -0
  97. package/src/__tests__/playbook.test.ts +4 -4
  98. package/src/__tests__/replayable-stream.test.ts +66 -0
  99. package/src/__tests__/vault-extra-ops.test.ts +1 -1
  100. package/src/__tests__/vault.test.ts +72 -0
  101. package/src/extensions/index.ts +2 -0
  102. package/src/extensions/middleware.ts +53 -0
  103. package/src/extensions/types.ts +64 -0
  104. package/src/index.ts +14 -17
  105. package/src/planning/gap-analysis.ts +52 -7
  106. package/src/runtime/facades/admin-facade.ts +101 -0
  107. package/src/runtime/facades/brain-facade.ts +331 -0
  108. package/src/runtime/facades/cognee-facade.ts +162 -0
  109. package/src/runtime/facades/control-facade.ts +279 -0
  110. package/src/runtime/facades/curator-facade.ts +132 -0
  111. package/src/runtime/facades/index.ts +74 -0
  112. package/src/runtime/facades/loop-facade.ts +12 -0
  113. package/src/runtime/facades/memory-facade.ts +114 -0
  114. package/src/runtime/facades/orchestrate-facade.ts +68 -0
  115. package/src/runtime/facades/plan-facade.ts +119 -0
  116. package/src/runtime/facades/vault-facade.ts +223 -0
  117. package/src/runtime/vault-extra-ops.ts +38 -2
  118. package/src/streams/index.ts +3 -0
  119. package/src/streams/normalize.ts +56 -0
  120. package/src/streams/replayable-stream.ts +92 -0
  121. package/src/vault/content-hash.ts +31 -0
  122. package/src/vault/vault.ts +73 -3
  123. package/src/runtime/core-ops.ts +0 -1443
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Memory facade — session & cross-project memory ops.
3
+ * capture, search, dedup, promote.
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import type { OpDefinition } from '../../facades/types.js';
8
+ import type { AgentRuntime } from '../types.js';
9
+ import { createMemoryExtraOps } from '../memory-extra-ops.js';
10
+ import { createMemoryCrossProjectOps } from '../memory-cross-project-ops.js';
11
+
12
+ export function createMemoryFacadeOps(runtime: AgentRuntime): OpDefinition[] {
13
+ const { vault } = runtime;
14
+
15
+ return [
16
+ // ─── Memory (inline from core-ops.ts) ───────────────────────
17
+ {
18
+ name: 'memory_search',
19
+ description: 'Search memories using full-text search.',
20
+ auth: 'read',
21
+ schema: z.object({
22
+ query: z.string(),
23
+ type: z.enum(['session', 'lesson', 'preference']).optional(),
24
+ projectPath: z.string().optional(),
25
+ limit: z.number().optional(),
26
+ }),
27
+ handler: async (params) => {
28
+ return vault.searchMemories(params.query as string, {
29
+ type: params.type as string | undefined,
30
+ projectPath: params.projectPath as string | undefined,
31
+ limit: (params.limit as number) ?? 10,
32
+ });
33
+ },
34
+ },
35
+ {
36
+ name: 'memory_capture',
37
+ description: 'Capture a memory — session summary, lesson learned, or preference.',
38
+ auth: 'write',
39
+ schema: z.object({
40
+ projectPath: z.string(),
41
+ type: z.enum(['session', 'lesson', 'preference']),
42
+ context: z.string(),
43
+ summary: z.string(),
44
+ topics: z.array(z.string()).optional().default([]),
45
+ filesModified: z.array(z.string()).optional().default([]),
46
+ toolsUsed: z.array(z.string()).optional().default([]),
47
+ }),
48
+ handler: async (params) => {
49
+ const memory = vault.captureMemory({
50
+ projectPath: params.projectPath as string,
51
+ type: params.type as 'session' | 'lesson' | 'preference',
52
+ context: params.context as string,
53
+ summary: params.summary as string,
54
+ topics: (params.topics as string[]) ?? [],
55
+ filesModified: (params.filesModified as string[]) ?? [],
56
+ toolsUsed: (params.toolsUsed as string[]) ?? [],
57
+ });
58
+ return { captured: true, memory };
59
+ },
60
+ },
61
+ {
62
+ name: 'memory_list',
63
+ description: 'List memories with optional filters.',
64
+ auth: 'read',
65
+ schema: z.object({
66
+ type: z.enum(['session', 'lesson', 'preference']).optional(),
67
+ projectPath: z.string().optional(),
68
+ limit: z.number().optional(),
69
+ offset: z.number().optional(),
70
+ }),
71
+ handler: async (params) => {
72
+ const memories = vault.listMemories({
73
+ type: params.type as string | undefined,
74
+ projectPath: params.projectPath as string | undefined,
75
+ limit: (params.limit as number) ?? 50,
76
+ offset: (params.offset as number) ?? 0,
77
+ });
78
+ const stats = vault.memoryStats();
79
+ return { memories, stats };
80
+ },
81
+ },
82
+ {
83
+ name: 'session_capture',
84
+ description:
85
+ 'Capture a session summary before context compaction. Called automatically by PreCompact hook.',
86
+ auth: 'write',
87
+ schema: z.object({
88
+ projectPath: z.string().optional().default('.'),
89
+ summary: z.string().describe('Brief summary of what was accomplished in this session'),
90
+ topics: z.array(z.string()).optional().default([]),
91
+ filesModified: z.array(z.string()).optional().default([]),
92
+ toolsUsed: z.array(z.string()).optional().default([]),
93
+ }),
94
+ handler: async (params) => {
95
+ const { resolve } = await import('node:path');
96
+ const projectPath = resolve((params.projectPath as string) ?? '.');
97
+ const memory = vault.captureMemory({
98
+ projectPath,
99
+ type: 'session',
100
+ context: 'Auto-captured before context compaction',
101
+ summary: params.summary as string,
102
+ topics: (params.topics as string[]) ?? [],
103
+ filesModified: (params.filesModified as string[]) ?? [],
104
+ toolsUsed: (params.toolsUsed as string[]) ?? [],
105
+ });
106
+ return { captured: true, memory, message: 'Session summary saved to memory.' };
107
+ },
108
+ },
109
+
110
+ // ─── Satellite ops ───────────────────────────────────────────
111
+ ...createMemoryExtraOps(runtime),
112
+ ...createMemoryCrossProjectOps(runtime),
113
+ ];
114
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Orchestrate facade — execution orchestration ops.
3
+ * project registration, playbooks, plan/execute/complete.
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import type { OpDefinition } from '../../facades/types.js';
8
+ import type { AgentRuntime } from '../types.js';
9
+ import { createOrchestrateOps } from '../orchestrate-ops.js';
10
+ import { createProjectOps } from '../project-ops.js';
11
+ import { createPlaybookOps } from '../playbook-ops.js';
12
+
13
+ export function createOrchestrateFacadeOps(runtime: AgentRuntime): OpDefinition[] {
14
+ const { vault, governance, projectRegistry } = runtime;
15
+
16
+ return [
17
+ // ─── Register (inline from core-ops.ts) ─────────────────────
18
+ {
19
+ name: 'register',
20
+ description:
21
+ 'Register a project for this session. Call on every new session to track usage and get context.',
22
+ auth: 'write',
23
+ schema: z.object({
24
+ projectPath: z.string().optional().default('.'),
25
+ name: z.string().optional().describe('Project display name (derived from path if omitted)'),
26
+ }),
27
+ handler: async (params) => {
28
+ const { resolve } = await import('node:path');
29
+ const projectPath = resolve((params.projectPath as string) ?? '.');
30
+ const project = vault.registerProject(projectPath, params.name as string | undefined);
31
+ // Also track in project registry for cross-project features
32
+ projectRegistry.register(projectPath, params.name as string | undefined);
33
+ const stats = vault.stats();
34
+ const isNew = project.sessionCount === 1;
35
+
36
+ // Expire stale proposals on session start (fire-and-forget)
37
+ const policy = governance.getPolicy(projectPath);
38
+ const expired = governance.expireStaleProposals(policy.autoCapture.autoExpireDays);
39
+
40
+ const proposalStats = governance.getProposalStats(projectPath);
41
+ const quotaStatus = governance.getQuotaStatus(projectPath);
42
+
43
+ return {
44
+ project,
45
+ is_new: isNew,
46
+ message: isNew
47
+ ? 'Welcome! New project registered.'
48
+ : 'Welcome back! Session #' + project.sessionCount + ' for ' + project.name + '.',
49
+ vault: { entries: stats.totalEntries, domains: Object.keys(stats.byDomain) },
50
+ governance: {
51
+ pendingProposals: proposalStats.pending,
52
+ quotaPercent:
53
+ quotaStatus.maxTotal > 0
54
+ ? Math.round((quotaStatus.total / quotaStatus.maxTotal) * 100)
55
+ : 0,
56
+ isQuotaWarning: quotaStatus.isWarning,
57
+ expiredThisSession: expired,
58
+ },
59
+ };
60
+ },
61
+ },
62
+
63
+ // ─── Satellite ops ───────────────────────────────────────────
64
+ ...createOrchestrateOps(runtime),
65
+ ...createProjectOps(runtime),
66
+ ...createPlaybookOps(runtime),
67
+ ];
68
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Plan facade — plan lifecycle ops.
3
+ * create, approve, execute, reconcile, complete, grading.
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import type { OpDefinition } from '../../facades/types.js';
8
+ import type { AgentRuntime } from '../types.js';
9
+ import { createPlanningExtraOps } from '../planning-extra-ops.js';
10
+ import { createGradingOps } from '../grading-ops.js';
11
+
12
+ export function createPlanFacadeOps(runtime: AgentRuntime): OpDefinition[] {
13
+ const { planner } = runtime;
14
+
15
+ return [
16
+ // ─── Planning (inline from core-ops.ts) ─────────────────────
17
+ {
18
+ name: 'create_plan',
19
+ description:
20
+ 'Create a new plan in draft status. Plans track multi-step tasks with decisions and sub-tasks.',
21
+ auth: 'write',
22
+ schema: z.object({
23
+ objective: z.string().describe('What the plan aims to achieve'),
24
+ scope: z.string().describe('Which parts of the codebase are affected'),
25
+ decisions: z.array(z.string()).optional().default([]),
26
+ tasks: z
27
+ .array(z.object({ title: z.string(), description: z.string() }))
28
+ .optional()
29
+ .default([]),
30
+ }),
31
+ handler: async (params) => {
32
+ const plan = planner.create({
33
+ objective: params.objective as string,
34
+ scope: params.scope as string,
35
+ decisions: (params.decisions as string[]) ?? [],
36
+ tasks: (params.tasks as Array<{ title: string; description: string }>) ?? [],
37
+ });
38
+ return { created: true, plan };
39
+ },
40
+ },
41
+ {
42
+ name: 'get_plan',
43
+ description: 'Get a plan by ID, or list all active plans if no ID provided.',
44
+ auth: 'read',
45
+ schema: z.object({
46
+ planId: z.string().optional().describe('Plan ID. Omit to list all active plans.'),
47
+ }),
48
+ handler: async (params) => {
49
+ if (params.planId) {
50
+ const plan = planner.get(params.planId as string);
51
+ if (!plan) return { error: 'Plan not found: ' + params.planId };
52
+ return plan;
53
+ }
54
+ return { active: planner.getActive(), executing: planner.getExecuting() };
55
+ },
56
+ },
57
+ {
58
+ name: 'approve_plan',
59
+ description: 'Approve a draft plan and optionally start execution.',
60
+ auth: 'write',
61
+ schema: z.object({
62
+ planId: z.string(),
63
+ startExecution: z
64
+ .boolean()
65
+ .optional()
66
+ .default(false)
67
+ .describe('If true, immediately start execution after approval'),
68
+ }),
69
+ handler: async (params) => {
70
+ let plan = planner.approve(params.planId as string);
71
+ if (params.startExecution) {
72
+ plan = planner.startExecution(plan.id);
73
+ }
74
+ return { approved: true, executing: plan.status === 'executing', plan };
75
+ },
76
+ },
77
+ {
78
+ name: 'update_task',
79
+ description: 'Update a task status within an executing plan.',
80
+ auth: 'write',
81
+ schema: z.object({
82
+ planId: z.string(),
83
+ taskId: z.string(),
84
+ status: z.enum(['pending', 'in_progress', 'completed', 'skipped', 'failed']),
85
+ }),
86
+ handler: async (params) => {
87
+ const plan = planner.updateTask(
88
+ params.planId as string,
89
+ params.taskId as string,
90
+ params.status as 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed',
91
+ );
92
+ const task = plan.tasks.find((t) => t.id === params.taskId);
93
+ return { updated: true, task, plan: { id: plan.id, status: plan.status } };
94
+ },
95
+ },
96
+ {
97
+ name: 'complete_plan',
98
+ description: 'Mark an executing plan as completed.',
99
+ auth: 'write',
100
+ schema: z.object({
101
+ planId: z.string(),
102
+ }),
103
+ handler: async (params) => {
104
+ const plan = planner.complete(params.planId as string);
105
+ const taskSummary = {
106
+ completed: plan.tasks.filter((t) => t.status === 'completed').length,
107
+ skipped: plan.tasks.filter((t) => t.status === 'skipped').length,
108
+ failed: plan.tasks.filter((t) => t.status === 'failed').length,
109
+ total: plan.tasks.length,
110
+ };
111
+ return { completed: true, plan, taskSummary };
112
+ },
113
+ },
114
+
115
+ // ─── Satellite ops ───────────────────────────────────────────
116
+ ...createPlanningExtraOps(runtime),
117
+ ...createGradingOps(runtime),
118
+ ];
119
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Vault facade — knowledge management ops.
3
+ * search, CRUD, import/export, intake, archival.
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import type { OpDefinition } from '../../facades/types.js';
8
+ import type { IntelligenceEntry } from '../../intelligence/types.js';
9
+ import type { AgentRuntime } from '../types.js';
10
+ import { createVaultExtraOps } from '../vault-extra-ops.js';
11
+ import { createCaptureOps } from '../capture-ops.js';
12
+ import { createIntakeOps } from '../intake-ops.js';
13
+
14
+ export function createVaultFacadeOps(runtime: AgentRuntime): OpDefinition[] {
15
+ const { vault, brain, cognee, llmClient, syncManager, intakePipeline } = runtime;
16
+
17
+ return [
18
+ // ─── Search / Vault (inline from core-ops.ts) ───────────────
19
+ {
20
+ name: 'search',
21
+ description:
22
+ 'Search across all knowledge domains. Results ranked by TF-IDF + severity + recency + tag overlap + domain match.',
23
+ auth: 'read',
24
+ schema: z.object({
25
+ query: z.string(),
26
+ domain: z.string().optional(),
27
+ type: z.enum(['pattern', 'anti-pattern', 'rule', 'playbook']).optional(),
28
+ severity: z.enum(['critical', 'warning', 'suggestion']).optional(),
29
+ tags: z.array(z.string()).optional(),
30
+ limit: z.number().optional(),
31
+ }),
32
+ handler: async (params) => {
33
+ return brain.intelligentSearch(params.query as string, {
34
+ domain: params.domain as string | undefined,
35
+ type: params.type as string | undefined,
36
+ severity: params.severity as string | undefined,
37
+ tags: params.tags as string[] | undefined,
38
+ limit: (params.limit as number) ?? 10,
39
+ });
40
+ },
41
+ },
42
+ {
43
+ name: 'vault_stats',
44
+ description: 'Get vault statistics — entry counts by type, domain, severity.',
45
+ auth: 'read',
46
+ handler: async () => vault.stats(),
47
+ },
48
+ {
49
+ name: 'list_all',
50
+ description: 'List all knowledge entries with optional filters.',
51
+ auth: 'read',
52
+ schema: z.object({
53
+ domain: z.string().optional(),
54
+ type: z.enum(['pattern', 'anti-pattern', 'rule', 'playbook']).optional(),
55
+ severity: z.enum(['critical', 'warning', 'suggestion']).optional(),
56
+ tags: z.array(z.string()).optional(),
57
+ limit: z.number().optional(),
58
+ offset: z.number().optional(),
59
+ }),
60
+ handler: async (params) => {
61
+ return vault.list({
62
+ domain: params.domain as string | undefined,
63
+ type: params.type as string | undefined,
64
+ severity: params.severity as string | undefined,
65
+ tags: params.tags as string[] | undefined,
66
+ limit: (params.limit as number) ?? 50,
67
+ offset: (params.offset as number) ?? 0,
68
+ });
69
+ },
70
+ },
71
+ {
72
+ name: 'export',
73
+ description:
74
+ 'Export vault entries as JSON intelligence bundles — one per domain. Enables version control and sharing.',
75
+ auth: 'read',
76
+ schema: z.object({
77
+ domain: z.string().optional().describe('Export only this domain. Omit to export all.'),
78
+ }),
79
+ handler: async (params) => {
80
+ const stats = vault.stats();
81
+ const domains = params.domain ? [params.domain as string] : Object.keys(stats.byDomain);
82
+ const bundles: Array<{ domain: string; version: string; entries: IntelligenceEntry[] }> =
83
+ [];
84
+ for (const d of domains) {
85
+ const entries = vault.list({ domain: d, limit: 10000 });
86
+ bundles.push({ domain: d, version: '1.0.0', entries });
87
+ }
88
+ return {
89
+ exported: true,
90
+ bundles,
91
+ totalEntries: bundles.reduce((sum, b) => sum + b.entries.length, 0),
92
+ domains: bundles.map((b) => b.domain),
93
+ };
94
+ },
95
+ },
96
+
97
+ // ─── Enriched Capture ────────────────────────────────────────
98
+ {
99
+ name: 'capture_enriched',
100
+ description:
101
+ 'Unified LLM-enriched capture — accepts minimal fields (title, description, type), uses LLM to auto-infer tags, category, and severity.',
102
+ auth: 'write',
103
+ schema: z.object({
104
+ title: z.string().describe('Knowledge title'),
105
+ description: z.string().describe('Knowledge description'),
106
+ type: z
107
+ .enum(['pattern', 'anti-pattern', 'rule', 'playbook'])
108
+ .optional()
109
+ .describe('Entry type. If omitted, LLM infers from content.'),
110
+ domain: z.string().optional().describe('Domain. If omitted, LLM infers.'),
111
+ tags: z.array(z.string()).optional().describe('Tags. LLM adds more if needed.'),
112
+ }),
113
+ handler: async (params) => {
114
+ try {
115
+ const title = params.title as string;
116
+ const description = params.description as string;
117
+
118
+ // Try LLM enrichment for auto-tagging
119
+ let inferredTags: string[] = (params.tags as string[] | undefined) ?? [];
120
+ let inferredType = (params.type as IntelligenceEntry['type'] | undefined) ?? 'pattern';
121
+ const inferredDomain = (params.domain as string | undefined) ?? 'general';
122
+ let inferredSeverity: IntelligenceEntry['severity'] = 'suggestion';
123
+ const enriched = false;
124
+
125
+ try {
126
+ const captureId = `enriched-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
127
+ const enrichResult = brain.enrichAndCapture({
128
+ id: captureId,
129
+ title,
130
+ description,
131
+ type: inferredType,
132
+ domain: inferredDomain,
133
+ severity: inferredSeverity,
134
+ tags: inferredTags,
135
+ });
136
+
137
+ if (enrichResult.captured) {
138
+ return {
139
+ captured: true,
140
+ enriched: true,
141
+ entryId: enrichResult.id,
142
+ autoTags: enrichResult.autoTags,
143
+ duplicate: enrichResult.duplicate ?? null,
144
+ };
145
+ }
146
+ } catch {
147
+ // LLM enrichment failed — fall back to basic capture
148
+ }
149
+
150
+ // Fallback: basic capture without LLM enrichment
151
+ // Infer type from keywords
152
+ const lowerDesc = description.toLowerCase();
153
+ if (!params.type) {
154
+ if (
155
+ lowerDesc.includes('avoid') ||
156
+ lowerDesc.includes("don't") ||
157
+ lowerDesc.includes('anti-pattern')
158
+ )
159
+ inferredType = 'anti-pattern';
160
+ else if (
161
+ lowerDesc.includes('rule') ||
162
+ lowerDesc.includes('must') ||
163
+ lowerDesc.includes('always')
164
+ )
165
+ inferredType = 'rule';
166
+ }
167
+
168
+ // Infer severity from keywords
169
+ if (
170
+ lowerDesc.includes('critical') ||
171
+ lowerDesc.includes('security') ||
172
+ lowerDesc.includes('breaking')
173
+ )
174
+ inferredSeverity = 'critical';
175
+ else if (
176
+ lowerDesc.includes('warning') ||
177
+ lowerDesc.includes('careful') ||
178
+ lowerDesc.includes('avoid')
179
+ )
180
+ inferredSeverity = 'warning';
181
+
182
+ // Auto-generate tags from title words
183
+ if (inferredTags.length === 0) {
184
+ inferredTags = title
185
+ .toLowerCase()
186
+ .split(/\s+/)
187
+ .filter(
188
+ (w) =>
189
+ w.length > 3 && !['with', 'from', 'that', 'this', 'have', 'been'].includes(w),
190
+ )
191
+ .slice(0, 5);
192
+ }
193
+
194
+ const entry: IntelligenceEntry = {
195
+ id: `enriched-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
196
+ type: inferredType,
197
+ domain: inferredDomain,
198
+ title,
199
+ severity: inferredSeverity,
200
+ description,
201
+ tags: inferredTags,
202
+ };
203
+
204
+ vault.add(entry);
205
+
206
+ return {
207
+ captured: true,
208
+ enriched,
209
+ entry,
210
+ autoTags: inferredTags,
211
+ };
212
+ } catch (err) {
213
+ return { error: (err as Error).message };
214
+ }
215
+ },
216
+ },
217
+
218
+ // ─── Satellite ops ───────────────────────────────────────────
219
+ ...createVaultExtraOps(runtime),
220
+ ...createCaptureOps(runtime),
221
+ ...createIntakeOps(intakePipeline),
222
+ ];
223
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Extra vault operations — 23 ops that extend the 4 base vault ops in core-ops.ts.
2
+ * Extra vault operations — 25 ops that extend the 4 base vault ops in core-ops.ts.
3
3
  *
4
4
  * Groups: single-entry CRUD (3), bulk (2), discovery (3), import/export (3),
5
5
  * analytics (1), seed canonical (1), knowledge lifecycle (4), temporal (3),
6
- * archival (3).
6
+ * archival (3), content hashing (2).
7
7
  */
8
8
 
9
9
  import { z } from 'zod';
@@ -586,6 +586,42 @@ export function createVaultExtraOps(runtime: AgentRuntime): OpDefinition[] {
586
586
  return vault.optimize();
587
587
  },
588
588
  },
589
+
590
+ // ── Content hashing (#166) ────────────────────────────────────────
591
+ {
592
+ name: 'vault_content_hash',
593
+ description: 'Compute content hash for an entry without inserting',
594
+ auth: 'read' as const,
595
+ schema: z.object({
596
+ type: z.string(),
597
+ domain: z.string(),
598
+ title: z.string(),
599
+ description: z.string(),
600
+ tags: z.array(z.string()).optional(),
601
+ example: z.string().optional(),
602
+ counterExample: z.string().optional(),
603
+ }),
604
+ handler: async (params) => {
605
+ const { computeContentHash: hashFn } = await import('../vault/content-hash.js');
606
+ const hash = hashFn(params as unknown as Parameters<typeof hashFn>[0]);
607
+ const existingId = vault.findByContentHash(hash);
608
+ return { hash, duplicate: existingId !== null, existingId };
609
+ },
610
+ },
611
+ {
612
+ name: 'vault_dedup_status',
613
+ description: 'Report content hash coverage and duplicate statistics',
614
+ auth: 'read' as const,
615
+ handler: async () => {
616
+ const stats = vault.contentHashStats();
617
+ const duplicates = stats.total - stats.uniqueHashes;
618
+ return {
619
+ ...stats,
620
+ duplicates,
621
+ coverage: stats.total > 0 ? Math.round((stats.hashed / stats.total) * 100) : 100,
622
+ };
623
+ },
624
+ },
589
625
  ];
590
626
  }
591
627
 
@@ -0,0 +1,3 @@
1
+ export { ReplayableStream } from './replayable-stream.js';
2
+ export { normalize, collect } from './normalize.js';
3
+ export type { NestableInput } from './normalize.js';
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Input type that supports arbitrary nesting of sync/async values.
3
+ */
4
+ export type NestableInput<T> =
5
+ | T
6
+ | T[]
7
+ | Promise<T | T[]>
8
+ | AsyncIterable<T>
9
+ | Iterable<NestableInput<T>>;
10
+
11
+ function isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> {
12
+ return (
13
+ value !== null &&
14
+ value !== undefined &&
15
+ typeof value === 'object' &&
16
+ Symbol.asyncIterator in value
17
+ );
18
+ }
19
+
20
+ function isSyncIterable<T>(value: unknown): value is Iterable<T> {
21
+ return (
22
+ value !== null && value !== undefined && typeof value === 'object' && Symbol.iterator in value
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Recursively flatten nested async/sync inputs into a flat AsyncIterable<T>.
28
+ * Discrimination: Promise → AsyncIterable → Iterable (excluding strings) → leaf T.
29
+ */
30
+ export async function* normalize<T>(input: NestableInput<T>): AsyncIterable<T> {
31
+ if (input instanceof Promise) {
32
+ const resolved = await input;
33
+ yield* normalize<T>(resolved as NestableInput<T>);
34
+ } else if (isAsyncIterable<T>(input)) {
35
+ for await (const item of input) {
36
+ yield item;
37
+ }
38
+ } else if (typeof input !== 'string' && isSyncIterable<NestableInput<T>>(input)) {
39
+ for (const item of input) {
40
+ yield* normalize<T>(item);
41
+ }
42
+ } else {
43
+ yield input as T;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Collect an AsyncIterable<T> into a Promise<T[]>.
49
+ */
50
+ export async function collect<T>(source: AsyncIterable<T>): Promise<T[]> {
51
+ const items: T[] = [];
52
+ for await (const item of source) {
53
+ items.push(item);
54
+ }
55
+ return items;
56
+ }