@dot-ai/core 0.5.2

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 (96) hide show
  1. package/.ai/memory/2026-03-04.md +2 -0
  2. package/.ai/tasks.json +7 -0
  3. package/LICENSE +21 -0
  4. package/dist/__tests__/config.test.d.ts +2 -0
  5. package/dist/__tests__/config.test.d.ts.map +1 -0
  6. package/dist/__tests__/config.test.js +128 -0
  7. package/dist/__tests__/config.test.js.map +1 -0
  8. package/dist/__tests__/e2e.test.d.ts +2 -0
  9. package/dist/__tests__/e2e.test.d.ts.map +1 -0
  10. package/dist/__tests__/e2e.test.js +211 -0
  11. package/dist/__tests__/e2e.test.js.map +1 -0
  12. package/dist/__tests__/engine.test.d.ts +2 -0
  13. package/dist/__tests__/engine.test.d.ts.map +1 -0
  14. package/dist/__tests__/engine.test.js +271 -0
  15. package/dist/__tests__/engine.test.js.map +1 -0
  16. package/dist/__tests__/format.test.d.ts +2 -0
  17. package/dist/__tests__/format.test.d.ts.map +1 -0
  18. package/dist/__tests__/format.test.js +200 -0
  19. package/dist/__tests__/format.test.js.map +1 -0
  20. package/dist/__tests__/labels.test.d.ts +2 -0
  21. package/dist/__tests__/labels.test.d.ts.map +1 -0
  22. package/dist/__tests__/labels.test.js +82 -0
  23. package/dist/__tests__/labels.test.js.map +1 -0
  24. package/dist/__tests__/loader.test.d.ts +2 -0
  25. package/dist/__tests__/loader.test.d.ts.map +1 -0
  26. package/dist/__tests__/loader.test.js +161 -0
  27. package/dist/__tests__/loader.test.js.map +1 -0
  28. package/dist/__tests__/logger.test.d.ts +2 -0
  29. package/dist/__tests__/logger.test.d.ts.map +1 -0
  30. package/dist/__tests__/logger.test.js +95 -0
  31. package/dist/__tests__/logger.test.js.map +1 -0
  32. package/dist/__tests__/nodes.test.d.ts +2 -0
  33. package/dist/__tests__/nodes.test.d.ts.map +1 -0
  34. package/dist/__tests__/nodes.test.js +83 -0
  35. package/dist/__tests__/nodes.test.js.map +1 -0
  36. package/dist/config.d.ts +29 -0
  37. package/dist/config.d.ts.map +1 -0
  38. package/dist/config.js +141 -0
  39. package/dist/config.js.map +1 -0
  40. package/dist/contracts.d.ts +56 -0
  41. package/dist/contracts.d.ts.map +1 -0
  42. package/dist/contracts.js +2 -0
  43. package/dist/contracts.js.map +1 -0
  44. package/dist/engine.d.ts +38 -0
  45. package/dist/engine.d.ts.map +1 -0
  46. package/dist/engine.js +88 -0
  47. package/dist/engine.js.map +1 -0
  48. package/dist/format.d.ts +18 -0
  49. package/dist/format.d.ts.map +1 -0
  50. package/dist/format.js +89 -0
  51. package/dist/format.js.map +1 -0
  52. package/dist/index.d.ts +21 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +22 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/labels.d.ts +13 -0
  57. package/dist/labels.d.ts.map +1 -0
  58. package/dist/labels.js +36 -0
  59. package/dist/labels.js.map +1 -0
  60. package/dist/loader.d.ts +26 -0
  61. package/dist/loader.d.ts.map +1 -0
  62. package/dist/loader.js +120 -0
  63. package/dist/loader.js.map +1 -0
  64. package/dist/logger.d.ts +29 -0
  65. package/dist/logger.d.ts.map +1 -0
  66. package/dist/logger.js +29 -0
  67. package/dist/logger.js.map +1 -0
  68. package/dist/nodes.d.ts +15 -0
  69. package/dist/nodes.d.ts.map +1 -0
  70. package/dist/nodes.js +46 -0
  71. package/dist/nodes.js.map +1 -0
  72. package/dist/types.d.ts +111 -0
  73. package/dist/types.d.ts.map +1 -0
  74. package/dist/types.js +2 -0
  75. package/dist/types.js.map +1 -0
  76. package/package.json +23 -0
  77. package/src/__tests__/config.test.ts +166 -0
  78. package/src/__tests__/e2e.test.ts +257 -0
  79. package/src/__tests__/engine.test.ts +305 -0
  80. package/src/__tests__/format.test.ts +247 -0
  81. package/src/__tests__/labels.test.ts +96 -0
  82. package/src/__tests__/loader.test.ts +191 -0
  83. package/src/__tests__/logger.test.ts +113 -0
  84. package/src/__tests__/nodes.test.ts +103 -0
  85. package/src/config.ts +178 -0
  86. package/src/contracts.ts +71 -0
  87. package/src/engine.ts +145 -0
  88. package/src/format.ts +113 -0
  89. package/src/index.ts +63 -0
  90. package/src/labels.ts +40 -0
  91. package/src/loader.ts +152 -0
  92. package/src/logger.ts +49 -0
  93. package/src/nodes.ts +46 -0
  94. package/src/types.ts +123 -0
  95. package/tsconfig.json +23 -0
  96. package/tsconfig.tsbuildinfo +1 -0
package/src/engine.ts ADDED
@@ -0,0 +1,145 @@
1
+ import type {
2
+ MemoryProvider,
3
+ SkillProvider,
4
+ IdentityProvider,
5
+ RoutingProvider,
6
+ TaskProvider,
7
+ ToolProvider,
8
+ } from './contracts.js';
9
+ import type { EnrichedContext, Identity, Skill } from './types.js';
10
+ import type { Logger } from './logger.js';
11
+ import { extractLabels, buildVocabulary } from './labels.js';
12
+
13
+ /**
14
+ * All providers needed by the engine.
15
+ */
16
+ export interface Providers {
17
+ memory: MemoryProvider;
18
+ skills: SkillProvider;
19
+ identity: IdentityProvider;
20
+ routing: RoutingProvider;
21
+ tasks: TaskProvider;
22
+ tools: ToolProvider;
23
+ }
24
+
25
+ /**
26
+ * Cached data from boot phase. Reused across prompts.
27
+ */
28
+ export interface BootCache {
29
+ identities: Identity[];
30
+ vocabulary: string[];
31
+ skills: Skill[];
32
+ }
33
+
34
+ /**
35
+ * Boot phase — run once per session.
36
+ * Loads identities, indexes skills/tools, builds label vocabulary.
37
+ */
38
+ export async function boot(providers: Providers, logger?: Logger): Promise<BootCache> {
39
+ const start = performance.now();
40
+
41
+ const [identities, skills, tools] = await Promise.all([
42
+ providers.identity.load(),
43
+ providers.skills.list(),
44
+ providers.tools.list(),
45
+ ]);
46
+
47
+ // Build vocabulary from skill labels, skill triggers (excluding meta-triggers), and tool labels
48
+ const META_TRIGGERS = new Set(['always', 'auto', 'manual', 'boot', 'heartbeat', 'pipeline', 'audit']);
49
+ const skillTriggers = skills.map((s) =>
50
+ (s.triggers ?? []).filter((t) => !META_TRIGGERS.has(t)),
51
+ );
52
+
53
+ const vocabulary = buildVocabulary(
54
+ [...skills.map((s) => s.labels), ...skillTriggers],
55
+ tools.map((t) => t.labels),
56
+ );
57
+
58
+ logger?.log({
59
+ timestamp: new Date().toISOString(),
60
+ level: 'info',
61
+ phase: 'boot',
62
+ event: 'boot_complete',
63
+ data: { identityCount: identities.length, skillCount: skills.length, vocabularySize: vocabulary.length },
64
+ durationMs: Math.round(performance.now() - start),
65
+ });
66
+
67
+ return { identities, vocabulary, skills };
68
+ }
69
+
70
+ /**
71
+ * Enrich a prompt — run per prompt.
72
+ * Calls all providers to build an EnrichedContext.
73
+ */
74
+ export async function enrich(
75
+ prompt: string,
76
+ providers: Providers,
77
+ cache: BootCache,
78
+ logger?: Logger,
79
+ ): Promise<EnrichedContext> {
80
+ const start = performance.now();
81
+
82
+ // 1. Extract labels from prompt against known vocabulary
83
+ const labels = extractLabels(prompt, cache.vocabulary);
84
+
85
+ logger?.log({
86
+ timestamp: new Date().toISOString(),
87
+ level: 'info',
88
+ phase: 'enrich',
89
+ event: 'labels_extracted',
90
+ data: { labels: labels.map(l => l.name), vocabularySize: cache.vocabulary.length },
91
+ durationMs: Math.round(performance.now() - start),
92
+ });
93
+
94
+ // 2. Search memory + match skills + match tools + route — all in parallel
95
+ const [memories, matchedSkills, matchedTools, routing] = await Promise.all([
96
+ providers.memory.search(prompt, labels.map((l) => l.name)),
97
+ providers.skills.match(labels),
98
+ providers.tools.match(labels),
99
+ providers.routing.route(labels),
100
+ ]);
101
+
102
+ logger?.log({
103
+ timestamp: new Date().toISOString(),
104
+ level: 'info',
105
+ phase: 'enrich',
106
+ event: 'enrich_complete',
107
+ data: {
108
+ labelCount: labels.length,
109
+ memoryCount: memories.length,
110
+ skillCount: matchedSkills.length,
111
+ routing: routing.model,
112
+ },
113
+ durationMs: Math.round(performance.now() - start),
114
+ });
115
+
116
+ return {
117
+ prompt,
118
+ labels,
119
+ identities: cache.identities,
120
+ memories,
121
+ skills: matchedSkills,
122
+ tools: matchedTools,
123
+ routing,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Learn phase — run after agent response.
129
+ * Stores learnings in memory.
130
+ */
131
+ export async function learn(
132
+ response: string,
133
+ providers: Providers,
134
+ ): Promise<void> {
135
+ const MAX_LEARN_LENGTH = 500;
136
+ const truncated = response.length > MAX_LEARN_LENGTH
137
+ ? response.slice(0, MAX_LEARN_LENGTH) + '…'
138
+ : response;
139
+
140
+ await providers.memory.store({
141
+ content: truncated,
142
+ type: 'log',
143
+ date: new Date().toISOString().slice(0, 10),
144
+ });
145
+ }
package/src/format.ts ADDED
@@ -0,0 +1,113 @@
1
+ import type { EnrichedContext, MemoryEntry, Skill, Tool, RoutingResult } from './types.js';
2
+ import type { Logger } from './logger.js';
3
+
4
+ export interface FormatOptions {
5
+ /** Skip identity sections (useful when already injected at session start) */
6
+ skipIdentities?: boolean;
7
+ /** Max chars per skill content. Truncated skills get a [...truncated] marker. Default: unlimited */
8
+ maxSkillLength?: number;
9
+ /** Max number of skills to include (already sorted by match relevance). Default: unlimited */
10
+ maxSkills?: number;
11
+ /** Optional logger for tracing */
12
+ logger?: Logger;
13
+ }
14
+
15
+ /**
16
+ * Format an EnrichedContext into markdown sections for injection into agent context.
17
+ * Sections are ordered by priority: identity > memory > skills > tools > routing.
18
+ */
19
+ export function formatContext(ctx: EnrichedContext, options?: FormatOptions): string {
20
+ const start = performance.now();
21
+ const sections: string[] = [];
22
+
23
+ // Identity sections (sorted by priority, highest first)
24
+ if (!options?.skipIdentities) {
25
+ const sortedIdentities = [...ctx.identities].sort((a, b) => b.priority - a.priority);
26
+ for (const identity of sortedIdentities) {
27
+ if (identity.content) {
28
+ sections.push(identity.content);
29
+ }
30
+ }
31
+ }
32
+
33
+ // Memory section
34
+ if (ctx.memories.length > 0) {
35
+ sections.push(formatMemory(ctx.memories));
36
+ }
37
+
38
+ // Skills section
39
+ let loadedSkills = ctx.skills.filter(s => s.content);
40
+ if (options?.maxSkills != null) {
41
+ loadedSkills = loadedSkills.slice(0, options.maxSkills);
42
+ }
43
+ if (loadedSkills.length > 0) {
44
+ sections.push(formatSkills(loadedSkills, options?.maxSkillLength));
45
+ }
46
+
47
+ // Tools section
48
+ if (ctx.tools.length > 0) {
49
+ sections.push(formatTools(ctx.tools));
50
+ }
51
+
52
+ // Routing hint
53
+ if (ctx.routing.model !== 'default') {
54
+ sections.push(formatRouting(ctx.routing));
55
+ }
56
+
57
+ const result = sections.join('\n\n---\n\n');
58
+
59
+ options?.logger?.log({
60
+ timestamp: new Date().toISOString(),
61
+ level: 'info',
62
+ phase: 'format',
63
+ event: 'format_complete',
64
+ data: {
65
+ outputChars: result.length,
66
+ estimatedTokens: Math.round(result.length / 4),
67
+ skillsIncluded: loadedSkills.map(s => s.name),
68
+ truncatedSkills: loadedSkills
69
+ .filter(s => options?.maxSkillLength != null && (s.content?.length ?? 0) > options.maxSkillLength)
70
+ .map(s => s.name),
71
+ },
72
+ durationMs: Math.round(performance.now() - start),
73
+ });
74
+
75
+ return result;
76
+ }
77
+
78
+ function formatMemory(memories: MemoryEntry[]): string {
79
+ const lines = ['## Relevant Memory\n'];
80
+ for (const m of memories.slice(0, 10)) { // Limit to 10 most relevant
81
+ const date = m.date ? ` (${m.date})` : '';
82
+ lines.push(`- ${m.content}${date}`);
83
+ }
84
+ return lines.join('\n');
85
+ }
86
+
87
+ function formatSkills(skills: Skill[], maxLength?: number): string {
88
+ const lines = ['## Active Skills\n'];
89
+ for (const s of skills) {
90
+ lines.push(`### ${s.name}`);
91
+ if (s.content) {
92
+ if (maxLength != null && s.content.length > maxLength) {
93
+ lines.push(s.content.slice(0, maxLength) + '\n\n[...truncated]');
94
+ } else {
95
+ lines.push(s.content);
96
+ }
97
+ }
98
+ lines.push('');
99
+ }
100
+ return lines.join('\n');
101
+ }
102
+
103
+ function formatTools(tools: Tool[]): string {
104
+ const lines = ['## Available Tools\n'];
105
+ for (const t of tools) {
106
+ lines.push(`- **${t.name}**: ${t.description}`);
107
+ }
108
+ return lines.join('\n');
109
+ }
110
+
111
+ function formatRouting(routing: RoutingResult): string {
112
+ return `## Model Routing\n\nRecommended model: **${routing.model}** (${routing.reason})`;
113
+ }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @dot-ai/core v4 — Contracts and types for the dot-ai convention.
3
+ *
4
+ * dot-ai = contracts (interfaces) + providers (pluggable implementations) + adapters (agent integration).
5
+ * Core defines WHAT, providers define HOW, adapters define WHERE.
6
+ */
7
+
8
+ // ── Types ──
9
+ export type {
10
+ Label,
11
+ Node,
12
+ MemoryEntry,
13
+ Skill,
14
+ Identity,
15
+ Task,
16
+ Tool,
17
+ RoutingResult,
18
+ EnrichedContext,
19
+ TaskFilter,
20
+ DotAiConfig,
21
+ DebugConfig,
22
+ ProviderConfig,
23
+ WorkspaceConfig,
24
+ } from './types.js';
25
+
26
+ // ── Contracts ──
27
+ export type {
28
+ MemoryProvider,
29
+ SkillProvider,
30
+ IdentityProvider,
31
+ RoutingProvider,
32
+ TaskProvider,
33
+ ToolProvider,
34
+ ProviderFactory,
35
+ } from './contracts.js';
36
+
37
+ // ── Engine ──
38
+ export { boot, enrich, learn } from './engine.js';
39
+ export type { Providers, BootCache } from './engine.js';
40
+
41
+ // ── Config ──
42
+ export { loadConfig, resolveConfig, injectRoot } from './config.js';
43
+ export type { ResolvedConfig } from './config.js';
44
+
45
+ // ── Format ──
46
+ export { formatContext } from './format.js';
47
+ export type { FormatOptions } from './format.js';
48
+
49
+ // ── Logger ──
50
+ export type { LogLevel, LogEntry, Logger } from './logger.js';
51
+ export { NoopLogger, JsonFileLogger, StderrLogger } from './logger.js';
52
+
53
+ // ── Loader ──
54
+ export { registerProvider, clearProviders, createProviders } from './loader.js';
55
+
56
+ // ── Labels ──
57
+ export { extractLabels, buildVocabulary } from './labels.js';
58
+
59
+ // ── Nodes ──
60
+ export { discoverNodes, parseScanDirs } from './nodes.js';
61
+
62
+ // ── registerDefaults ──
63
+ export { registerDefaults } from './loader.js';
package/src/labels.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { Label } from './types.js';
2
+
3
+ function escapeRegex(s: string): string {
4
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5
+ }
6
+
7
+ /**
8
+ * Extract labels from a prompt using word-boundary keyword matching.
9
+ * No LLM — pure deterministic pattern matching.
10
+ * Returns matched Label[] from a known vocabulary.
11
+ */
12
+ export function extractLabels(prompt: string, vocabulary: string[]): Label[] {
13
+ const labels: Label[] = [];
14
+ const seen = new Set<string>();
15
+
16
+ for (const word of vocabulary) {
17
+ if (seen.has(word)) continue;
18
+ const regex = new RegExp(`\\b${escapeRegex(word)}\\b`, 'i');
19
+ if (regex.test(prompt)) {
20
+ seen.add(word);
21
+ labels.push({ name: word, source: 'extract' });
22
+ }
23
+ }
24
+
25
+ return labels;
26
+ }
27
+
28
+ /**
29
+ * Build a vocabulary from skill labels and any other known labels.
30
+ * This is called once at boot to build the label dictionary.
31
+ */
32
+ export function buildVocabulary(skillLabels: string[][], toolLabels: string[][]): string[] {
33
+ const set = new Set<string>();
34
+ for (const labels of [...skillLabels, ...toolLabels]) {
35
+ for (const label of labels) {
36
+ set.add(label);
37
+ }
38
+ }
39
+ return Array.from(set);
40
+ }
package/src/loader.ts ADDED
@@ -0,0 +1,152 @@
1
+ import type { DotAiConfig, Task } from './types.js';
2
+ import type {
3
+ MemoryProvider,
4
+ SkillProvider,
5
+ IdentityProvider,
6
+ RoutingProvider,
7
+ TaskProvider,
8
+ ToolProvider,
9
+ } from './contracts.js';
10
+ import type { Providers } from './engine.js';
11
+ import { resolveConfig } from './config.js';
12
+ /**
13
+ * Registry of provider factories.
14
+ * Adapters register their providers here before boot.
15
+ */
16
+ const registry = new Map<string, (options: Record<string, unknown>) => unknown>();
17
+
18
+ /**
19
+ * Register a provider factory.
20
+ * Call this before createProviders().
21
+ *
22
+ * @example
23
+ * registerProvider('@dot-ai/cockpit-memory', (opts) => new CockpitMemory(opts.url));
24
+ */
25
+ export function registerProvider(
26
+ name: string,
27
+ factory: (options: Record<string, unknown>) => unknown,
28
+ ): void {
29
+ registry.set(name, factory);
30
+ }
31
+
32
+ /**
33
+ * Clear all registered providers.
34
+ * Useful for testing.
35
+ */
36
+ export function clearProviders(): void {
37
+ registry.clear();
38
+ }
39
+
40
+ /**
41
+ * Register all default file-based providers.
42
+ * Call this at startup if you want file-based defaults available.
43
+ */
44
+ export function registerDefaults(): void {
45
+ // Default providers are now separate packages (@dot-ai/provider-file-memory, etc.)
46
+ // They are resolved via auto-discovery in resolve() → tryImportProvider()
47
+ // No explicit registration needed — the package names match the config defaults.
48
+ }
49
+
50
+ /**
51
+ * Create all providers from config.
52
+ * Falls back to no-op providers for any missing registration.
53
+ */
54
+ export async function createProviders(config: DotAiConfig): Promise<Providers> {
55
+ const resolved = resolveConfig(config);
56
+
57
+ return {
58
+ memory: await resolve<MemoryProvider>(resolved.memory.use, resolved.memory.with ?? {}, noopMemory),
59
+ skills: await resolve<SkillProvider>(resolved.skills.use, resolved.skills.with ?? {}, noopSkills),
60
+ identity: await resolve<IdentityProvider>(resolved.identity.use, resolved.identity.with ?? {}, noopIdentity),
61
+ routing: await resolve<RoutingProvider>(resolved.routing.use, resolved.routing.with ?? {}, noopRouting),
62
+ tasks: await resolve<TaskProvider>(resolved.tasks.use, resolved.tasks.with ?? {}, noopTasks),
63
+ tools: await resolve<ToolProvider>(resolved.tools.use, resolved.tools.with ?? {}, noopTools),
64
+ };
65
+ }
66
+
67
+ async function resolve<T>(name: string, options: Record<string, unknown>, fallback: T): Promise<T> {
68
+ let factory = registry.get(name);
69
+
70
+ if (!factory) {
71
+ // Auto-discovery: try dynamic import
72
+ factory = await tryImportProvider(name);
73
+ if (factory) {
74
+ registry.set(name, factory); // Cache for next time
75
+ }
76
+ }
77
+
78
+ if (!factory) return fallback;
79
+ return factory(options) as T;
80
+ }
81
+
82
+ /**
83
+ * Try to import a provider package dynamically.
84
+ * Looks for: default export factory, createXxxProvider function, or XxxProvider class.
85
+ */
86
+ async function tryImportProvider(
87
+ name: string,
88
+ ): Promise<((options: Record<string, unknown>) => unknown) | undefined> {
89
+ try {
90
+ const mod = await import(name);
91
+
92
+ // 1. Check for default export (function)
93
+ if (typeof mod.default === 'function') {
94
+ return mod.default as (options: Record<string, unknown>) => unknown;
95
+ }
96
+
97
+ // 2. Check for createXxxProvider factory function
98
+ for (const [key, value] of Object.entries(mod)) {
99
+ if (key.startsWith('create') && key.endsWith('Provider') && typeof value === 'function') {
100
+ return value as (options: Record<string, unknown>) => unknown;
101
+ }
102
+ }
103
+
104
+ // 3. Check for XxxProvider class (constructor)
105
+ for (const [key, value] of Object.entries(mod)) {
106
+ if (key.endsWith('Provider') && typeof value === 'function') {
107
+ return (opts: Record<string, unknown>) => new (value as new (opts: Record<string, unknown>) => unknown)(opts);
108
+ }
109
+ }
110
+
111
+ return undefined;
112
+ } catch {
113
+ // Package not found or import error — not a problem, fall back to noop
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ // ── No-op providers (safe fallbacks) ────────────────────────────────────────
119
+
120
+ const noopMemory: MemoryProvider = {
121
+ async search(_query: string, _labels?: string[]) { return []; },
122
+ async store(_entry) {},
123
+ };
124
+
125
+ const noopSkills: SkillProvider = {
126
+ async list() { return []; },
127
+ async match(_labels) { return []; },
128
+ async load(_name: string) { return null; },
129
+ };
130
+
131
+ const noopIdentity: IdentityProvider = {
132
+ async load() { return []; },
133
+ };
134
+
135
+ const noopRouting: RoutingProvider = {
136
+ async route(_labels) { return { model: 'default', reason: 'no routing provider' }; },
137
+ };
138
+
139
+ const noopTasks: TaskProvider = {
140
+ async list(_filter?) { return []; },
141
+ async get(_id: string) { return null; },
142
+ async create(task): Promise<Task> { return { id: crypto.randomUUID(), ...task }; },
143
+ async update(id: string, patch: Partial<Task>): Promise<Task> {
144
+ return { id, text: '', status: '', ...patch };
145
+ },
146
+ };
147
+
148
+ const noopTools: ToolProvider = {
149
+ async list() { return []; },
150
+ async match(_labels) { return []; },
151
+ async load(_name: string) { return null; },
152
+ };
package/src/logger.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { appendFile } from 'node:fs/promises';
2
+
3
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
4
+
5
+ export interface LogEntry {
6
+ timestamp: string;
7
+ level: LogLevel;
8
+ phase: 'boot' | 'enrich' | 'learn' | 'format';
9
+ event: string;
10
+ data?: Record<string, unknown>;
11
+ durationMs?: number;
12
+ }
13
+
14
+ export interface Logger {
15
+ log(entry: LogEntry): void;
16
+ flush(): Promise<void>;
17
+ }
18
+
19
+ export class NoopLogger implements Logger {
20
+ log(_entry: LogEntry): void {}
21
+ async flush(): Promise<void> {}
22
+ }
23
+
24
+ export class JsonFileLogger implements Logger {
25
+ private buffer: string[] = [];
26
+ private filePath: string;
27
+
28
+ constructor(filePath: string) {
29
+ this.filePath = filePath;
30
+ }
31
+
32
+ log(entry: LogEntry): void {
33
+ this.buffer.push(JSON.stringify(entry));
34
+ }
35
+
36
+ async flush(): Promise<void> {
37
+ if (this.buffer.length === 0) return;
38
+ const data = this.buffer.join('\n') + '\n';
39
+ this.buffer = [];
40
+ await appendFile(this.filePath, data, 'utf-8');
41
+ }
42
+ }
43
+
44
+ export class StderrLogger implements Logger {
45
+ log(entry: LogEntry): void {
46
+ process.stderr.write(JSON.stringify(entry) + '\n');
47
+ }
48
+ async flush(): Promise<void> {}
49
+ }
package/src/nodes.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { Node } from './types.js';
4
+
5
+ /**
6
+ * Discover all .ai/ directories in a workspace.
7
+ * Always includes root. Scans configurable directories for sub-nodes.
8
+ *
9
+ * @param root - workspace root (absolute path)
10
+ * @param scanDirs - directories to scan for sub-nodes (default: ["projects"])
11
+ */
12
+ export function discoverNodes(root: string, scanDirs: string[] = ['projects']): Node[] {
13
+ const nodes: Node[] = [
14
+ { name: 'root', path: join(root, '.ai'), root: true },
15
+ ];
16
+
17
+ for (const dir of scanDirs) {
18
+ const scanPath = join(root, dir);
19
+ try {
20
+ const entries = readdirSync(scanPath, { withFileTypes: true });
21
+ for (const entry of entries) {
22
+ if (!entry.isDirectory()) continue;
23
+ const aiPath = join(scanPath, entry.name, '.ai');
24
+ try {
25
+ readdirSync(aiPath); // existence check
26
+ nodes.push({ name: entry.name, path: aiPath, root: false });
27
+ } catch {
28
+ // No .ai/ in this directory
29
+ }
30
+ }
31
+ } catch {
32
+ // Scan directory doesn't exist
33
+ }
34
+ }
35
+
36
+ return nodes;
37
+ }
38
+
39
+ /**
40
+ * Parse scanDirs from a config string value.
41
+ * Returns empty array if not configured.
42
+ */
43
+ export function parseScanDirs(value: unknown): string[] {
44
+ if (!value || typeof value !== 'string') return [];
45
+ return value.split(',').map(s => s.trim()).filter(Boolean);
46
+ }