zouroboros-core 2.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.
@@ -0,0 +1,307 @@
1
+ /**
2
+ * ECC-002: Slash Commands Hub
3
+ *
4
+ * First-class CLI-style commands callable from any persona conversation.
5
+ * Zero-dependency command parser with unified help, registration, and routing.
6
+ * Supports subcommands: /memory search "query" → routes to memory.search handler.
7
+ */
8
+ export class CommandHub {
9
+ commands = new Map();
10
+ aliases = new Map();
11
+ hooks = null;
12
+ /** Wire to hook system for command.execute events */
13
+ wireHooks(hooks) {
14
+ this.hooks = hooks;
15
+ }
16
+ register(definition) {
17
+ if (!definition.name || !definition.name.startsWith('/')) {
18
+ throw new Error(`Command name must start with '/': ${definition.name}`);
19
+ }
20
+ // Check for alias collisions
21
+ for (const alias of definition.aliases) {
22
+ const existing = this.aliases.get(alias);
23
+ if (existing && existing !== definition.name) {
24
+ throw new Error(`Alias '${alias}' already registered to '${existing}', cannot register for '${definition.name}'`);
25
+ }
26
+ }
27
+ this.commands.set(definition.name, definition);
28
+ for (const alias of definition.aliases) {
29
+ this.aliases.set(alias, definition.name);
30
+ }
31
+ }
32
+ /** Register a subcommand under an existing command */
33
+ registerSubcommand(commandName, sub) {
34
+ const cmd = this.commands.get(commandName);
35
+ if (!cmd)
36
+ throw new Error(`Command '${commandName}' not found`);
37
+ if (!cmd.subcommands)
38
+ cmd.subcommands = new Map();
39
+ cmd.subcommands.set(sub.name, sub);
40
+ }
41
+ unregister(name) {
42
+ const cmd = this.commands.get(name);
43
+ if (!cmd)
44
+ return false;
45
+ for (const alias of cmd.aliases) {
46
+ this.aliases.delete(alias);
47
+ }
48
+ this.commands.delete(name);
49
+ return true;
50
+ }
51
+ resolve(nameOrAlias) {
52
+ const cmd = this.commands.get(nameOrAlias);
53
+ if (cmd)
54
+ return cmd;
55
+ const canonical = this.aliases.get(nameOrAlias);
56
+ if (canonical)
57
+ return this.commands.get(canonical) || null;
58
+ return null;
59
+ }
60
+ parse(input) {
61
+ const trimmed = input.trim();
62
+ if (!trimmed.startsWith('/'))
63
+ return null;
64
+ const parts = this.tokenize(trimmed);
65
+ if (parts.length === 0)
66
+ return null;
67
+ const name = parts[0];
68
+ const args = {};
69
+ const flags = new Set();
70
+ let subcommand;
71
+ const def = this.resolve(name);
72
+ // Check for subcommand: if part[1] exists and matches a registered subcommand
73
+ let argDefs = def?.args || [];
74
+ let partStart = 1;
75
+ if (def?.subcommands && parts.length > 1 && def.subcommands.has(parts[1])) {
76
+ subcommand = parts[1];
77
+ argDefs = def.subcommands.get(parts[1]).args;
78
+ partStart = 2;
79
+ }
80
+ let positionalIndex = 0;
81
+ for (let i = partStart; i < parts.length; i++) {
82
+ const part = parts[i];
83
+ if (part.startsWith('--')) {
84
+ const key = part.slice(2);
85
+ const eqIdx = key.indexOf('=');
86
+ if (eqIdx >= 0) {
87
+ args[key.slice(0, eqIdx)] = key.slice(eqIdx + 1);
88
+ }
89
+ else {
90
+ const argDef = argDefs.find(a => a.name === key);
91
+ if (argDef?.type === 'flag' || argDef?.type === 'boolean') {
92
+ flags.add(key);
93
+ args[key] = true;
94
+ }
95
+ else if (i + 1 < parts.length && !parts[i + 1].startsWith('--')) {
96
+ args[key] = this.coerceArg(parts[++i], argDef?.type);
97
+ }
98
+ else {
99
+ flags.add(key);
100
+ args[key] = true;
101
+ }
102
+ }
103
+ }
104
+ else {
105
+ const positionalDefs = argDefs.filter(a => a.type !== 'flag');
106
+ if (positionalIndex < positionalDefs.length) {
107
+ const argDef = positionalDefs[positionalIndex];
108
+ args[argDef.name] = this.coerceArg(part, argDef.type);
109
+ }
110
+ else {
111
+ args[`_${positionalIndex}`] = part;
112
+ }
113
+ positionalIndex++;
114
+ }
115
+ }
116
+ // Apply defaults
117
+ for (const argDef of argDefs) {
118
+ if (argDef.default !== undefined && args[argDef.name] === undefined) {
119
+ args[argDef.name] = argDef.default;
120
+ }
121
+ }
122
+ return { name, subcommand, args, raw: trimmed, flags };
123
+ }
124
+ async execute(input) {
125
+ const parsed = this.parse(input);
126
+ if (!parsed) {
127
+ return { success: false, output: '', error: 'Invalid command format. Commands must start with /' };
128
+ }
129
+ const cmd = this.resolve(parsed.name);
130
+ if (!cmd) {
131
+ const suggestions = this.suggest(parsed.name);
132
+ const suggestionText = suggestions.length > 0
133
+ ? ` Did you mean: ${suggestions.join(', ')}?`
134
+ : '';
135
+ return { success: false, output: '', error: `Unknown command: ${parsed.name}.${suggestionText}` };
136
+ }
137
+ // Route to subcommand handler if applicable
138
+ let handler = cmd.handler;
139
+ let requiredArgs = cmd.args;
140
+ if (parsed.subcommand && cmd.subcommands?.has(parsed.subcommand)) {
141
+ const sub = cmd.subcommands.get(parsed.subcommand);
142
+ handler = sub.handler;
143
+ requiredArgs = sub.args;
144
+ }
145
+ // Validate required args
146
+ for (const argDef of requiredArgs) {
147
+ if (argDef.required && parsed.args[argDef.name] === undefined) {
148
+ return {
149
+ success: false,
150
+ output: '',
151
+ error: `Missing required argument: ${argDef.name}\nUsage: ${cmd.usage}`,
152
+ };
153
+ }
154
+ }
155
+ try {
156
+ const result = await handler(parsed);
157
+ // Emit command.execute hook event
158
+ if (this.hooks) {
159
+ this.hooks.emit('command.execute', {
160
+ command: parsed.name,
161
+ subcommand: parsed.subcommand,
162
+ success: result.success,
163
+ }, 'command-hub').catch(() => { });
164
+ }
165
+ return result;
166
+ }
167
+ catch (err) {
168
+ return {
169
+ success: false,
170
+ output: '',
171
+ error: `Command failed: ${err instanceof Error ? err.message : String(err)}`,
172
+ };
173
+ }
174
+ }
175
+ help(commandName) {
176
+ if (commandName) {
177
+ const cmd = this.resolve(commandName);
178
+ if (!cmd)
179
+ return `Unknown command: ${commandName}`;
180
+ const lines = [
181
+ `${cmd.name} — ${cmd.description}`,
182
+ `Usage: ${cmd.usage}`,
183
+ ];
184
+ if (cmd.aliases.length > 0) {
185
+ lines.push(`Aliases: ${cmd.aliases.join(', ')}`);
186
+ }
187
+ if (cmd.subcommands && cmd.subcommands.size > 0) {
188
+ lines.push('Subcommands:');
189
+ for (const [subName, sub] of cmd.subcommands) {
190
+ lines.push(` ${subName} — ${sub.description}`);
191
+ }
192
+ }
193
+ if (cmd.args.length > 0) {
194
+ lines.push('Arguments:');
195
+ for (const arg of cmd.args) {
196
+ const req = arg.required ? '(required)' : `(default: ${arg.default ?? 'none'})`;
197
+ lines.push(` --${arg.name} ${arg.description} ${req}`);
198
+ }
199
+ }
200
+ return lines.join('\n');
201
+ }
202
+ // List all commands by category
203
+ const byCategory = new Map();
204
+ for (const cmd of this.commands.values()) {
205
+ if (cmd.hidden)
206
+ continue;
207
+ const list = byCategory.get(cmd.category) || [];
208
+ list.push(cmd);
209
+ byCategory.set(cmd.category, list);
210
+ }
211
+ const lines = ['Available Commands:', ''];
212
+ for (const [category, cmds] of byCategory) {
213
+ lines.push(`[${category}]`);
214
+ for (const cmd of cmds.sort((a, b) => a.name.localeCompare(b.name))) {
215
+ const aliasStr = cmd.aliases.length > 0 ? ` (${cmd.aliases.join(', ')})` : '';
216
+ lines.push(` ${cmd.name}${aliasStr} — ${cmd.description}`);
217
+ }
218
+ lines.push('');
219
+ }
220
+ return lines.join('\n');
221
+ }
222
+ suggest(partial) {
223
+ const lower = partial.toLowerCase();
224
+ const matches = [];
225
+ for (const name of this.commands.keys()) {
226
+ if (name.toLowerCase().includes(lower) || this.levenshtein(name.toLowerCase(), lower) <= 2) {
227
+ matches.push(name);
228
+ }
229
+ }
230
+ for (const [alias, canonical] of this.aliases) {
231
+ if (alias.toLowerCase().includes(lower)) {
232
+ matches.push(`${alias} → ${canonical}`);
233
+ }
234
+ }
235
+ return matches.slice(0, 5);
236
+ }
237
+ list(category) {
238
+ const all = [...this.commands.values()];
239
+ if (category)
240
+ return all.filter(c => c.category === category);
241
+ return all;
242
+ }
243
+ getCategories() {
244
+ const counts = {};
245
+ for (const cmd of this.commands.values()) {
246
+ counts[cmd.category] = (counts[cmd.category] || 0) + 1;
247
+ }
248
+ return counts;
249
+ }
250
+ tokenize(input) {
251
+ const tokens = [];
252
+ let current = '';
253
+ let inQuote = false;
254
+ let quoteChar = '';
255
+ for (const ch of input) {
256
+ if (inQuote) {
257
+ if (ch === quoteChar) {
258
+ inQuote = false;
259
+ }
260
+ else {
261
+ current += ch;
262
+ }
263
+ }
264
+ else if (ch === '"' || ch === "'") {
265
+ inQuote = true;
266
+ quoteChar = ch;
267
+ }
268
+ else if (ch === ' ' || ch === '\t') {
269
+ if (current.length > 0) {
270
+ tokens.push(current);
271
+ current = '';
272
+ }
273
+ }
274
+ else {
275
+ current += ch;
276
+ }
277
+ }
278
+ if (current.length > 0)
279
+ tokens.push(current);
280
+ return tokens;
281
+ }
282
+ coerceArg(value, type) {
283
+ if (type === 'number') {
284
+ const n = Number(value);
285
+ return isNaN(n) ? value : n;
286
+ }
287
+ if (type === 'boolean') {
288
+ return value === 'true' || value === '1' || value === 'yes';
289
+ }
290
+ return value;
291
+ }
292
+ levenshtein(a, b) {
293
+ const m = a.length, n = b.length;
294
+ const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
295
+ for (let i = 1; i <= m; i++) {
296
+ for (let j = 1; j <= n; j++) {
297
+ dp[i][j] = a[i - 1] === b[j - 1]
298
+ ? dp[i - 1][j - 1]
299
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
300
+ }
301
+ }
302
+ return dp[m][n];
303
+ }
304
+ }
305
+ export function createCommandHub() {
306
+ return new CommandHub();
307
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Configuration management for Zouroboros
3
+ */
4
+ import type { ZouroborosConfig } from '../types.js';
5
+ import { DEFAULT_CONFIG, DEFAULT_CONFIG_PATH } from '../constants.js';
6
+ export { DEFAULT_CONFIG, DEFAULT_CONFIG_PATH };
7
+ export { validateConfigSchema, formatValidationErrors } from './schema.js';
8
+ export type { ConfigValidationIssue } from './schema.js';
9
+ /**
10
+ * Configuration validation error
11
+ */
12
+ export declare class ConfigValidationError extends Error {
13
+ path: string;
14
+ constructor(message: string, path: string);
15
+ }
16
+ /**
17
+ * Load configuration from file or return defaults
18
+ */
19
+ export declare function loadConfig(configPath?: string): ZouroborosConfig;
20
+ /**
21
+ * Save configuration to file
22
+ */
23
+ export declare function saveConfig(config: ZouroborosConfig, configPath?: string): void;
24
+ /**
25
+ * Merge partial config with defaults
26
+ */
27
+ export declare function mergeConfig(partial: Partial<ZouroborosConfig>): ZouroborosConfig;
28
+ /**
29
+ * Validate configuration structure using Zod schemas.
30
+ * Throws ConfigValidationError with actionable messages on failure.
31
+ */
32
+ export declare function validateConfig(config: unknown): ZouroborosConfig;
33
+ /**
34
+ * Get a nested config value by path
35
+ */
36
+ export declare function getConfigValue<T>(config: ZouroborosConfig, path: string): T | undefined;
37
+ /**
38
+ * Set a nested config value by path
39
+ */
40
+ export declare function setConfigValue<T>(config: ZouroborosConfig, path: string, value: T): ZouroborosConfig;
41
+ /**
42
+ * Initialize configuration with interactive prompts
43
+ */
44
+ export declare function initConfig(options?: {
45
+ force?: boolean;
46
+ workspaceRoot?: string;
47
+ dataDir?: string;
48
+ }): Promise<ZouroborosConfig>;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Configuration management for Zouroboros
3
+ */
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
5
+ import { dirname, join } from 'path';
6
+ import { DEFAULT_CONFIG, DEFAULT_CONFIG_PATH } from '../constants.js';
7
+ import { validateConfigSchema, formatValidationErrors } from './schema.js';
8
+ export { DEFAULT_CONFIG, DEFAULT_CONFIG_PATH };
9
+ export { validateConfigSchema, formatValidationErrors } from './schema.js';
10
+ /**
11
+ * Configuration validation error
12
+ */
13
+ export class ConfigValidationError extends Error {
14
+ path;
15
+ constructor(message, path) {
16
+ super(message);
17
+ this.path = path;
18
+ this.name = 'ConfigValidationError';
19
+ }
20
+ }
21
+ /**
22
+ * Load configuration from file or return defaults
23
+ */
24
+ export function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
25
+ if (!existsSync(configPath)) {
26
+ return { ...DEFAULT_CONFIG };
27
+ }
28
+ try {
29
+ const content = readFileSync(configPath, 'utf-8');
30
+ const parsed = JSON.parse(content);
31
+ return mergeConfig(parsed);
32
+ }
33
+ catch (error) {
34
+ throw new ConfigValidationError(`Failed to parse config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`, configPath);
35
+ }
36
+ }
37
+ /**
38
+ * Save configuration to file
39
+ */
40
+ export function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
41
+ const validated = validateConfig(config);
42
+ validated.updatedAt = new Date().toISOString();
43
+ const dir = dirname(configPath);
44
+ if (!existsSync(dir)) {
45
+ mkdirSync(dir, { recursive: true });
46
+ }
47
+ writeFileSync(configPath, JSON.stringify(validated, null, 2), 'utf-8');
48
+ }
49
+ /**
50
+ * Merge partial config with defaults
51
+ */
52
+ export function mergeConfig(partial) {
53
+ return {
54
+ ...DEFAULT_CONFIG,
55
+ ...partial,
56
+ core: { ...DEFAULT_CONFIG.core, ...partial.core },
57
+ memory: { ...DEFAULT_CONFIG.memory, ...partial.memory },
58
+ swarm: {
59
+ ...DEFAULT_CONFIG.swarm,
60
+ ...partial.swarm,
61
+ circuitBreaker: { ...DEFAULT_CONFIG.swarm.circuitBreaker, ...partial.swarm?.circuitBreaker },
62
+ retryConfig: { ...DEFAULT_CONFIG.swarm.retryConfig, ...partial.swarm?.retryConfig },
63
+ },
64
+ personas: { ...DEFAULT_CONFIG.personas, ...partial.personas },
65
+ selfheal: {
66
+ ...DEFAULT_CONFIG.selfheal,
67
+ ...partial.selfheal,
68
+ metrics: { ...DEFAULT_CONFIG.selfheal.metrics, ...partial.selfheal?.metrics },
69
+ },
70
+ };
71
+ }
72
+ /**
73
+ * Validate configuration structure using Zod schemas.
74
+ * Throws ConfigValidationError with actionable messages on failure.
75
+ */
76
+ export function validateConfig(config) {
77
+ if (typeof config !== 'object' || config === null) {
78
+ throw new ConfigValidationError('Config must be an object', '');
79
+ }
80
+ const issues = validateConfigSchema(config);
81
+ if (issues) {
82
+ throw new ConfigValidationError(formatValidationErrors(issues), issues[0]?.path ?? '');
83
+ }
84
+ return config;
85
+ }
86
+ /**
87
+ * Get a nested config value by path
88
+ */
89
+ export function getConfigValue(config, path) {
90
+ const parts = path.split('.');
91
+ let current = config;
92
+ for (const part of parts) {
93
+ if (typeof current !== 'object' || current === null) {
94
+ return undefined;
95
+ }
96
+ current = current[part];
97
+ }
98
+ return current;
99
+ }
100
+ /**
101
+ * Set a nested config value by path
102
+ */
103
+ export function setConfigValue(config, path, value) {
104
+ const parts = path.split('.');
105
+ const newConfig = structuredClone(config);
106
+ let current = newConfig;
107
+ for (let i = 0; i < parts.length - 1; i++) {
108
+ const part = parts[i];
109
+ if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
110
+ current[part] = {};
111
+ }
112
+ current = current[part];
113
+ }
114
+ current[parts[parts.length - 1]] = value;
115
+ newConfig.updatedAt = new Date().toISOString();
116
+ return validateConfig(newConfig);
117
+ }
118
+ /**
119
+ * Initialize configuration with interactive prompts
120
+ */
121
+ export async function initConfig(options = {}) {
122
+ const configPath = DEFAULT_CONFIG_PATH;
123
+ if (existsSync(configPath) && !options.force) {
124
+ throw new Error(`Config already exists at ${configPath}. Use --force to overwrite.`);
125
+ }
126
+ const config = {
127
+ ...DEFAULT_CONFIG,
128
+ createdAt: new Date().toISOString(),
129
+ updatedAt: new Date().toISOString(),
130
+ };
131
+ if (options.workspaceRoot) {
132
+ config.core.workspaceRoot = options.workspaceRoot;
133
+ }
134
+ if (options.dataDir) {
135
+ config.core.dataDir = options.dataDir;
136
+ config.memory.dbPath = join(options.dataDir, 'memory.db');
137
+ config.swarm.registryPath = join(options.dataDir, 'executor-registry.json');
138
+ }
139
+ // Ensure data directory exists
140
+ if (!existsSync(config.core.dataDir)) {
141
+ mkdirSync(config.core.dataDir, { recursive: true });
142
+ }
143
+ saveConfig(config, configPath);
144
+ return config;
145
+ }