zayra-core 0.0.1

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/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # zayra-core
2
+
3
+ The core foundation package of **ZAYRA AI**. It manages the core lifecycle and architecture of ZAYRA — initialization, module loading, configuration, logging, and error handling.
4
+
5
+ This is version `0.0.1`: the first development version. It contains **no frontend/UI code** — only core architecture. Future ZAYRA packages (`zayra-config`, `zayra-memory`, `zayra-train`, `zayra-ai-router`, `zayra-provider`, `zayra-tools`, `zayra-sdk`) will depend on this package.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install zayra-core
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```js
16
+ import { Zayra } from "zayra-core";
17
+
18
+ const zayra = new Zayra();
19
+
20
+ await zayra.initialize();
21
+ await zayra.start();
22
+
23
+ console.log(zayra.status());
24
+ // { state: "running", version: "0.0.1", modules: [], uptimeMs: ... }
25
+
26
+ await zayra.stop();
27
+ ```
28
+
29
+ ### Registering modules
30
+
31
+ Future ZAYRA packages plug into core via `.use(name, moduleInstance)`. A module is any object that optionally implements `initialize()`, `start()`, and/or `stop()`:
32
+
33
+ ```js
34
+ import { Zayra } from "zayra-core";
35
+
36
+ const memoryModule = {
37
+ async initialize({ config, logger }) {
38
+ logger.info("memory module initializing");
39
+ },
40
+ async start() {
41
+ /* ... */
42
+ },
43
+ async stop() {
44
+ /* ... */
45
+ },
46
+ };
47
+
48
+ const zayra = new Zayra();
49
+ zayra.use("memory", memoryModule);
50
+
51
+ await zayra.start(); // also runs initialize() automatically if needed
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ zayra-core loads configuration from, in increasing priority:
57
+
58
+ 1. `config.json` in the working directory (optional)
59
+ 2. `.env` in the working directory (optional)
60
+ 3. real `process.env` variables
61
+
62
+ **zayra-core never hardcodes API keys, secrets, or private data.** All sensitive values must be provided via `.env` or your real environment.
63
+
64
+ ```js
65
+ import { loadConfig, requireConfigValue } from "zayra-core";
66
+
67
+ const config = await loadConfig();
68
+ const apiKey = requireConfigValue(config, "ZAYRA_API_KEY"); // throws ConfigError if missing
69
+ ```
70
+
71
+ See `.env.example` for the shape of a `.env` file.
72
+
73
+ ## Logging
74
+
75
+ ```js
76
+ import { logger, Logger } from "zayra-core";
77
+
78
+ logger.info("hello");
79
+ logger.warn("careful");
80
+ logger.error("something broke");
81
+
82
+ // Scoped logger for a specific module
83
+ const memoryLogger = new Logger({ scope: "memory" });
84
+ memoryLogger.info("memory module ready");
85
+ ```
86
+
87
+ ## Errors
88
+
89
+ zayra-core never fails silently. All internal errors are instances of one of:
90
+
91
+ - `ZayraError` — base error class
92
+ - `ConfigError` — configuration loading/validation failures
93
+ - `ModuleError` — module registration/lifecycle failures
94
+
95
+ ```js
96
+ import { ConfigError } from "zayra-core";
97
+
98
+ try {
99
+ await zayra.initialize();
100
+ } catch (err) {
101
+ if (err instanceof ConfigError) {
102
+ console.error("Bad config:", err.message);
103
+ }
104
+ }
105
+ ```
106
+
107
+ ## Project structure
108
+
109
+ ```
110
+ zayra-core/
111
+ src/
112
+ index.js # public entry point
113
+ core.js # Zayra class
114
+ config.js # config.json / .env / process.env loader
115
+ logger.js # Logger class + default logger
116
+ errors.js # ZayraError, ConfigError, ModuleError
117
+ utils.js # file, validation, formatting, path helpers
118
+ version.js # package version constant
119
+ package.json
120
+ README.md
121
+ ```
122
+
123
+ ## API reference
124
+
125
+ ### `class Zayra`
126
+
127
+ | Method | Description |
128
+ | --- | --- |
129
+ | `new Zayra(options?)` | Creates a new instance. `options.logger`, `options.cwd`, `options.autoLoadConfig`. |
130
+ | `initialize()` | Loads config and runs each module's `initialize()` hook. |
131
+ | `start()` | Runs `initialize()` if needed, then runs each module's `start()` hook. |
132
+ | `stop()` | Runs each module's `stop()` hook. |
133
+ | `status()` | Returns `{ state, version, modules, uptimeMs }`. |
134
+ | `use(name, moduleInstance)` | Registers a module. Returns `this` for chaining. |
135
+ | `getModule(name)` | Retrieves a registered module instance. |
136
+ | `listModules()` | Returns an array of registered module names. |
137
+
138
+ ### Config helpers
139
+
140
+ `loadConfig(options?)`, `requireConfigValue(config, key)`, `getConfigValue(config, key, defaultValue?)`
141
+
142
+ ### Utilities
143
+
144
+ `utils.fileExists`, `utils.readJsonFile`, `utils.writeJsonFile`, `utils.isPlainObject`, `utils.isNonEmptyString`, `utils.assert`, `utils.formatTimestamp`, `utils.safeStringify`, `utils.dirnameFromMeta`, `utils.resolvePath`
145
+
146
+ ## License
147
+
148
+ MIT
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "zayra-core",
3
+ "version": "0.0.1",
4
+ "description": "Core foundation package for ZAYRA AI — manages the core lifecycle and architecture that future zayra-* packages build on.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "scripts": {
18
+ "test": "node --test \"test/**/*.test.js\""
19
+ },
20
+ "keywords": [
21
+ "zayra",
22
+ "ai",
23
+ "core",
24
+ "foundation",
25
+ "lifecycle"
26
+ ],
27
+ "author": "",
28
+ "license": "MIT",
29
+ "dependencies": {}
30
+ }
package/src/config.js ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * config.js
3
+ *
4
+ * Configuration system for ZAYRA core.
5
+ *
6
+ * Supports two sources, merged in this order (later overrides earlier):
7
+ * 1. config.json (project defaults, optional, checked into repo)
8
+ * 2. .env (environment-specific values, NOT checked into repo)
9
+ * 3. process.env (real environment variables, e.g. from the shell / CI)
10
+ *
11
+ * This package never hardcodes API keys, secrets, or private data.
12
+ * Those must always come from the environment.
13
+ *
14
+ * Note: this is a deliberately small, dependency-free .env parser
15
+ * (no `dotenv` package) to keep zayra-core's dependency footprint at
16
+ * zero for 0.0.1. It covers the common KEY=VALUE cases; if more
17
+ * advanced .env features are needed later (multiline values, etc.),
18
+ * this is the place to extend, or swap in `dotenv` behind the same
19
+ * loadConfig() API.
20
+ */
21
+
22
+ import path from "node:path";
23
+ import { promises as fs } from "node:fs";
24
+ import { ConfigError } from "./errors.js";
25
+ import { fileExists, isPlainObject, readJsonFile } from "./utils.js";
26
+
27
+ /**
28
+ * Parses raw .env file content into a plain object.
29
+ * Supports `KEY=VALUE`, blank lines, `#` comments, and quoted values.
30
+ *
31
+ * @param {string} content
32
+ * @returns {Record<string, string>}
33
+ */
34
+ function parseEnvContent(content) {
35
+ const result = {};
36
+
37
+ const lines = content.split(/\r?\n/);
38
+
39
+ for (const rawLine of lines) {
40
+ const line = rawLine.trim();
41
+
42
+ if (!line || line.startsWith("#")) {
43
+ continue;
44
+ }
45
+
46
+ const separatorIndex = line.indexOf("=");
47
+ if (separatorIndex === -1) {
48
+ continue;
49
+ }
50
+
51
+ const key = line.slice(0, separatorIndex).trim();
52
+ let value = line.slice(separatorIndex + 1).trim();
53
+
54
+ if (!key) {
55
+ continue;
56
+ }
57
+
58
+ // Strip matching surrounding quotes, if present.
59
+ const isQuoted =
60
+ (value.startsWith('"') && value.endsWith('"')) ||
61
+ (value.startsWith("'") && value.endsWith("'"));
62
+
63
+ if (isQuoted) {
64
+ value = value.slice(1, -1);
65
+ }
66
+
67
+ result[key] = value;
68
+ }
69
+
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * Loads and parses a .env file, if it exists.
75
+ * Returns an empty object if the file is missing (this is not an error —
76
+ * .env is optional, especially in production where real env vars are used).
77
+ *
78
+ * @param {string} envPath
79
+ * @returns {Promise<Record<string, string>>}
80
+ */
81
+ async function loadDotEnv(envPath) {
82
+ const exists = await fileExists(envPath);
83
+ if (!exists) {
84
+ return {};
85
+ }
86
+
87
+ try {
88
+ const content = await fs.readFile(envPath, "utf-8");
89
+ return parseEnvContent(content);
90
+ } catch (err) {
91
+ throw new ConfigError(`Failed to read .env file at "${envPath}": ${err.message}`, {
92
+ cause: err,
93
+ });
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Loads and parses a config.json file, if it exists.
99
+ * Returns an empty object if the file is missing.
100
+ *
101
+ * @param {string} configPath
102
+ * @returns {Promise<Record<string, any>>}
103
+ */
104
+ async function loadConfigJson(configPath) {
105
+ const exists = await fileExists(configPath);
106
+ if (!exists) {
107
+ return {};
108
+ }
109
+
110
+ let parsed;
111
+ try {
112
+ parsed = await readJsonFile(configPath);
113
+ } catch (err) {
114
+ throw new ConfigError(`Failed to parse config.json at "${configPath}": ${err.message}`, {
115
+ cause: err,
116
+ });
117
+ }
118
+
119
+ if (!isPlainObject(parsed)) {
120
+ throw new ConfigError(`config.json at "${configPath}" must contain a JSON object at the top level.`);
121
+ }
122
+
123
+ return parsed;
124
+ }
125
+
126
+ /**
127
+ * Loads ZAYRA configuration by merging config.json, .env, and
128
+ * process.env, in that priority order (process.env wins).
129
+ *
130
+ * @param {object} [options]
131
+ * @param {string} [options.cwd] - Base directory to resolve config.json/.env from. Defaults to process.cwd().
132
+ * @param {string} [options.configFileName="config.json"]
133
+ * @param {string} [options.envFileName=".env"]
134
+ * @param {boolean} [options.includeProcessEnv=true] - Whether to merge in real process.env values.
135
+ * @returns {Promise<Record<string, any>>} Merged configuration object.
136
+ */
137
+ export async function loadConfig(options = {}) {
138
+ const cwd = options.cwd ?? process.cwd();
139
+ const configFileName = options.configFileName ?? "config.json";
140
+ const envFileName = options.envFileName ?? ".env";
141
+ const includeProcessEnv = options.includeProcessEnv ?? true;
142
+
143
+ const configPath = path.resolve(cwd, configFileName);
144
+ const envPath = path.resolve(cwd, envFileName);
145
+
146
+ const [fileConfig, dotEnvConfig] = await Promise.all([
147
+ loadConfigJson(configPath),
148
+ loadDotEnv(envPath),
149
+ ]);
150
+
151
+ const merged = {
152
+ ...fileConfig,
153
+ ...dotEnvConfig,
154
+ ...(includeProcessEnv ? process.env : {}),
155
+ };
156
+
157
+ return merged;
158
+ }
159
+
160
+ /**
161
+ * Retrieves a single required config value, throwing a ConfigError
162
+ * if it is missing or empty. Use this for values that ZAYRA cannot
163
+ * function without (e.g. a required API key name, not its value
164
+ * hardcoded — the value itself always comes from the environment).
165
+ *
166
+ * @param {Record<string, any>} config - A config object, typically from loadConfig().
167
+ * @param {string} key
168
+ * @returns {any}
169
+ * @throws {ConfigError}
170
+ */
171
+ export function requireConfigValue(config, key) {
172
+ const value = config?.[key];
173
+
174
+ if (value === undefined || value === null || value === "") {
175
+ throw new ConfigError(`Missing required configuration value: "${key}". Set it via .env or environment variables.`);
176
+ }
177
+
178
+ return value;
179
+ }
180
+
181
+ /**
182
+ * Retrieves a single optional config value, falling back to a default.
183
+ *
184
+ * @param {Record<string, any>} config
185
+ * @param {string} key
186
+ * @param {any} [defaultValue]
187
+ * @returns {any}
188
+ */
189
+ export function getConfigValue(config, key, defaultValue = undefined) {
190
+ const value = config?.[key];
191
+ return value === undefined ? defaultValue : value;
192
+ }
193
+
194
+ export default {
195
+ loadConfig,
196
+ requireConfigValue,
197
+ getConfigValue,
198
+ };
package/src/core.js ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * core.js
3
+ *
4
+ * Defines the main Zayra class — the central orchestrator of the
5
+ * ZAYRA AI system. This class is responsible for:
6
+ * - initializing the system
7
+ * - loading and managing modules (future packages: zayra-config,
8
+ * zayra-memory, zayra-train, zayra-ai-router, zayra-provider,
9
+ * zayra-tools, zayra-sdk)
10
+ * - managing the system lifecycle (initialize -> start -> stop)
11
+ * - providing a simple status snapshot
12
+ *
13
+ * Modules are kept independent: Zayra only knows about the generic
14
+ * "module" shape (an object with optional initialize/start/stop
15
+ * methods), never about the internals of any specific package. This
16
+ * keeps zayra-core decoupled from packages that don't exist yet.
17
+ */
18
+
19
+ import { logger as defaultLogger } from "./logger.js";
20
+ import { loadConfig } from "./config.js";
21
+ import { ZayraError, ModuleError } from "./errors.js";
22
+ import { VERSION } from "./version.js";
23
+
24
+ /**
25
+ * Lifecycle states for the Zayra core engine.
26
+ * @readonly
27
+ * @enum {string}
28
+ */
29
+ export const ZayraState = Object.freeze({
30
+ CREATED: "created",
31
+ INITIALIZING: "initializing",
32
+ INITIALIZED: "initialized",
33
+ STARTING: "starting",
34
+ RUNNING: "running",
35
+ STOPPING: "stopping",
36
+ STOPPED: "stopped",
37
+ ERRORED: "errored",
38
+ });
39
+
40
+ /**
41
+ * Main ZAYRA core engine.
42
+ *
43
+ * @example
44
+ * import { Zayra } from "zayra-core";
45
+ *
46
+ * const ai = new Zayra();
47
+ * await ai.initialize();
48
+ * await ai.start();
49
+ * console.log(ai.status());
50
+ * await ai.stop();
51
+ */
52
+ export class Zayra {
53
+ /**
54
+ * @param {object} [options]
55
+ * @param {import("./logger.js").Logger} [options.logger] - Custom logger instance. Defaults to the shared zayra-core logger.
56
+ * @param {string} [options.cwd] - Working directory used to resolve config.json/.env. Defaults to process.cwd().
57
+ * @param {boolean} [options.autoLoadConfig=true] - Whether initialize() should automatically load config.json/.env/process.env.
58
+ */
59
+ constructor(options = {}) {
60
+ this.logger = options.logger ?? defaultLogger;
61
+ this.cwd = options.cwd ?? process.cwd();
62
+ this.autoLoadConfig = options.autoLoadConfig ?? true;
63
+
64
+ this.version = VERSION;
65
+ this.state = ZayraState.CREATED;
66
+ this.config = {};
67
+
68
+ /**
69
+ * Registered modules, keyed by name.
70
+ * Each entry: { name, instance }
71
+ * @type {Map<string, { name: string, instance: any }>}
72
+ */
73
+ this._modules = new Map();
74
+ }
75
+
76
+ /* ------------------------------------------------------------------ */
77
+ /* Module architecture */
78
+ /* ------------------------------------------------------------------ */
79
+
80
+ /**
81
+ * Registers a module with the core engine. Modules are how future
82
+ * packages (zayra-config, zayra-memory, zayra-train, zayra-ai-router,
83
+ * zayra-provider, zayra-tools, zayra-sdk) plug into ZAYRA, without
84
+ * core needing to know their internals.
85
+ *
86
+ * A module is any object, optionally implementing:
87
+ * - async initialize(context) - called when core.initialize() runs
88
+ * - async start(context) - called when core.start() runs
89
+ * - async stop(context) - called when core.stop() runs
90
+ *
91
+ * @param {string} name - Unique module name (e.g. "memory", "router").
92
+ * @param {object} moduleInstance - The module implementation.
93
+ * @returns {this} For chaining, e.g. zayra.use("memory", memoryModule).use(...)
94
+ * @throws {ModuleError} If a module with this name is already registered.
95
+ */
96
+ use(name, moduleInstance) {
97
+ if (this._modules.has(name)) {
98
+ throw new ModuleError(`Module "${name}" is already registered.`, { moduleName: name });
99
+ }
100
+
101
+ this._modules.set(name, { name, instance: moduleInstance });
102
+ this.logger.info(`Module registered: ${name}`);
103
+
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * Retrieves a previously registered module by name.
109
+ *
110
+ * @param {string} name
111
+ * @returns {any} The module instance.
112
+ * @throws {ModuleError} If no module with this name is registered.
113
+ */
114
+ getModule(name) {
115
+ const entry = this._modules.get(name);
116
+ if (!entry) {
117
+ throw new ModuleError(`Module "${name}" is not registered.`, { moduleName: name });
118
+ }
119
+ return entry.instance;
120
+ }
121
+
122
+ /**
123
+ * Returns the names of all currently registered modules.
124
+ * @returns {string[]}
125
+ */
126
+ listModules() {
127
+ return Array.from(this._modules.keys());
128
+ }
129
+
130
+ /**
131
+ * Calls a given lifecycle hook (e.g. "initialize", "start", "stop")
132
+ * on every registered module that implements it, in registration order.
133
+ *
134
+ * @param {"initialize"|"start"|"stop"} hookName
135
+ * @returns {Promise<void>}
136
+ * @private
137
+ */
138
+ async _runModuleHook(hookName) {
139
+ for (const { name, instance } of this._modules.values()) {
140
+ const hook = instance?.[hookName];
141
+
142
+ if (typeof hook !== "function") {
143
+ continue;
144
+ }
145
+
146
+ try {
147
+ await hook.call(instance, { config: this.config, logger: this.logger });
148
+ } catch (err) {
149
+ throw new ModuleError(
150
+ `Module "${name}" failed during "${hookName}": ${err.message}`,
151
+ { moduleName: name, cause: err }
152
+ );
153
+ }
154
+ }
155
+ }
156
+
157
+ /* ------------------------------------------------------------------ */
158
+ /* Lifecycle: initialize / start / stop / status */
159
+ /* ------------------------------------------------------------------ */
160
+
161
+ /**
162
+ * Initializes the ZAYRA system: loads configuration and runs each
163
+ * registered module's initialize() hook. Must be called before start().
164
+ *
165
+ * @returns {Promise<this>}
166
+ * @throws {ZayraError|ModuleError} If initialization fails.
167
+ */
168
+ async initialize() {
169
+ if (this.state === ZayraState.INITIALIZING || this.state === ZayraState.INITIALIZED) {
170
+ this.logger.warn("initialize() called but Zayra is already initialized/initializing — ignoring.");
171
+ return this;
172
+ }
173
+
174
+ this.state = ZayraState.INITIALIZING;
175
+ this.logger.info(`Initializing ZAYRA core v${this.version}...`);
176
+
177
+ try {
178
+ if (this.autoLoadConfig) {
179
+ this.config = await loadConfig({ cwd: this.cwd });
180
+ }
181
+
182
+ await this._runModuleHook("initialize");
183
+
184
+ this.state = ZayraState.INITIALIZED;
185
+ this.logger.info("ZAYRA core initialized successfully.");
186
+ } catch (err) {
187
+ this.state = ZayraState.ERRORED;
188
+ this.logger.error("ZAYRA core failed to initialize.", err);
189
+ throw err instanceof ZayraError
190
+ ? err
191
+ : new ZayraError(`Initialization failed: ${err.message}`, { cause: err });
192
+ }
193
+
194
+ return this;
195
+ }
196
+
197
+ /**
198
+ * Starts the ZAYRA system. Automatically runs initialize() first if
199
+ * it hasn't been run yet. Runs each registered module's start() hook.
200
+ *
201
+ * @returns {Promise<this>}
202
+ * @throws {ZayraError|ModuleError} If starting fails.
203
+ */
204
+ async start() {
205
+ if (this.state === ZayraState.CREATED) {
206
+ await this.initialize();
207
+ }
208
+
209
+ if (this.state === ZayraState.RUNNING) {
210
+ this.logger.warn("start() called but Zayra is already running — ignoring.");
211
+ return this;
212
+ }
213
+
214
+ if (this.state !== ZayraState.INITIALIZED && this.state !== ZayraState.STOPPED) {
215
+ throw new ZayraError(
216
+ `Cannot start ZAYRA core from state "${this.state}". Expected "initialized" or "stopped".`
217
+ );
218
+ }
219
+
220
+ this.state = ZayraState.STARTING;
221
+ this.logger.info("Starting ZAYRA core...");
222
+
223
+ try {
224
+ await this._runModuleHook("start");
225
+
226
+ this.state = ZayraState.RUNNING;
227
+ this.logger.info("ZAYRA core is now running.");
228
+ } catch (err) {
229
+ this.state = ZayraState.ERRORED;
230
+ this.logger.error("ZAYRA core failed to start.", err);
231
+ throw err instanceof ZayraError
232
+ ? err
233
+ : new ZayraError(`Start failed: ${err.message}`, { cause: err });
234
+ }
235
+
236
+ return this;
237
+ }
238
+
239
+ /**
240
+ * Stops the ZAYRA system. Runs each registered module's stop() hook.
241
+ *
242
+ * @returns {Promise<this>}
243
+ * @throws {ZayraError|ModuleError} If stopping fails.
244
+ */
245
+ async stop() {
246
+ if (this.state === ZayraState.STOPPED || this.state === ZayraState.CREATED) {
247
+ this.logger.warn(`stop() called but Zayra is not running (state: "${this.state}") — ignoring.`);
248
+ return this;
249
+ }
250
+
251
+ this.state = ZayraState.STOPPING;
252
+ this.logger.info("Stopping ZAYRA core...");
253
+
254
+ try {
255
+ await this._runModuleHook("stop");
256
+
257
+ this.state = ZayraState.STOPPED;
258
+ this.logger.info("ZAYRA core stopped.");
259
+ } catch (err) {
260
+ this.state = ZayraState.ERRORED;
261
+ this.logger.error("ZAYRA core failed to stop cleanly.", err);
262
+ throw err instanceof ZayraError
263
+ ? err
264
+ : new ZayraError(`Stop failed: ${err.message}`, { cause: err });
265
+ }
266
+
267
+ return this;
268
+ }
269
+
270
+ /**
271
+ * Returns a snapshot of the current core status. Synchronous and
272
+ * side-effect free — safe to call at any time, in any state.
273
+ *
274
+ * @returns {{ state: string, version: string, modules: string[], uptimeMs: number|null }}
275
+ */
276
+ status() {
277
+ return {
278
+ state: this.state,
279
+ version: this.version,
280
+ modules: this.listModules(),
281
+ uptimeMs: this.state === ZayraState.RUNNING ? process.uptime() * 1000 : null,
282
+ };
283
+ }
284
+ }
285
+
286
+ export default Zayra;
package/src/errors.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * errors.js
3
+ *
4
+ * Custom error hierarchy for ZAYRA.
5
+ *
6
+ * Design goal: never fail silently. Every error thrown inside
7
+ * zayra-core should be one of these classes (or a subclass of one),
8
+ * so callers can reliably do `if (err instanceof ConfigError)` etc.
9
+ * instead of parsing error messages.
10
+ */
11
+
12
+ /**
13
+ * Base error class for all ZAYRA-related errors.
14
+ * Every other ZAYRA error extends this one.
15
+ */
16
+ export class ZayraError extends Error {
17
+ /**
18
+ * @param {string} message - Human readable error message.
19
+ * @param {object} [options]
20
+ * @param {string} [options.code] - Short machine-readable error code (e.g. "CORE_NOT_STARTED").
21
+ * @param {unknown} [options.cause] - Original error/cause, if this error wraps another one.
22
+ */
23
+ constructor(message, options = {}) {
24
+ super(message);
25
+
26
+ this.name = this.constructor.name;
27
+ this.code = options.code ?? "ZAYRA_ERROR";
28
+
29
+ // Preserve original cause (e.g. a caught error) for debugging,
30
+ // without losing native Error.cause support if present.
31
+ if (options.cause !== undefined) {
32
+ this.cause = options.cause;
33
+ }
34
+
35
+ // Maintains proper stack trace (V8 / Node specific, no-op elsewhere)
36
+ if (typeof Error.captureStackTrace === "function") {
37
+ Error.captureStackTrace(this, this.constructor);
38
+ }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Thrown when configuration loading/validation fails.
44
+ * Examples: missing required env var, invalid config.json, bad type.
45
+ */
46
+ export class ConfigError extends ZayraError {
47
+ constructor(message, options = {}) {
48
+ super(message, { code: "CONFIG_ERROR", ...options });
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Thrown when a module fails to load, initialize, start, or stop.
54
+ * Examples: module not found, module threw during initialize(), duplicate module name.
55
+ */
56
+ export class ModuleError extends ZayraError {
57
+ /**
58
+ * @param {string} message
59
+ * @param {object} [options]
60
+ * @param {string} [options.moduleName] - Name of the module that caused the error.
61
+ */
62
+ constructor(message, options = {}) {
63
+ super(message, { code: "MODULE_ERROR", ...options });
64
+ this.moduleName = options.moduleName ?? null;
65
+ }
66
+ }
67
+
68
+ export default {
69
+ ZayraError,
70
+ ConfigError,
71
+ ModuleError,
72
+ };
package/src/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * index.js
3
+ *
4
+ * Public entry point for the zayra-core package.
5
+ *
6
+ * Anything a consumer of zayra-core needs should be exported from
7
+ * here — they should never need to import from "zayra-core/src/...".
8
+ * This keeps the internal file layout free to change without
9
+ * breaking downstream zayra-* packages.
10
+ */
11
+
12
+ export { Zayra, ZayraState } from "./core.js";
13
+ export { loadConfig, requireConfigValue, getConfigValue } from "./config.js";
14
+ export { Logger, logger } from "./logger.js";
15
+ export { ZayraError, ConfigError, ModuleError } from "./errors.js";
16
+ export { VERSION } from "./version.js";
17
+ export * as utils from "./utils.js";
18
+
19
+ export { Zayra as default } from "./core.js";
package/src/logger.js ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * logger.js
3
+ *
4
+ * Minimal, dependency-free logger for ZAYRA core.
5
+ *
6
+ * Kept intentionally simple in 0.0.1 — no external logging library,
7
+ * no file transports. The goal here is just consistent, readable
8
+ * console output that's easy to grep during debugging. A more
9
+ * advanced logger (file output, log levels from config, structured
10
+ * JSON logs) can replace this later without changing the public API
11
+ * (info/warn/error), since other zayra-* packages will depend on
12
+ * that shape.
13
+ */
14
+
15
+ const LEVEL_STYLES = {
16
+ info: { label: "INFO", prefix: "[ZAYRA]" },
17
+ warn: { label: "WARN", prefix: "[ZAYRA]" },
18
+ error: { label: "ERROR", prefix: "[ZAYRA]" },
19
+ };
20
+
21
+ function timestamp() {
22
+ return new Date().toISOString();
23
+ }
24
+
25
+ /**
26
+ * Formats a single log line consistently across all levels.
27
+ *
28
+ * @param {"info"|"warn"|"error"} level
29
+ * @param {string} message
30
+ * @returns {string}
31
+ */
32
+ function formatLine(level, message) {
33
+ const { prefix, label } = LEVEL_STYLES[level];
34
+ return `${prefix} ${timestamp()} ${label} ${message}`;
35
+ }
36
+
37
+ /**
38
+ * Logger class.
39
+ *
40
+ * Instantiable so future packages (zayra-config, zayra-memory, etc.)
41
+ * can create scoped loggers, e.g. `new Logger({ scope: "memory" })`,
42
+ * without this needing to change.
43
+ */
44
+ export class Logger {
45
+ /**
46
+ * @param {object} [options]
47
+ * @param {string} [options.scope] - Optional scope/module name shown in log lines.
48
+ */
49
+ constructor(options = {}) {
50
+ this.scope = options.scope ?? null;
51
+ }
52
+
53
+ _withScope(message) {
54
+ return this.scope ? `(${this.scope}) ${message}` : message;
55
+ }
56
+
57
+ /**
58
+ * Log an informational message.
59
+ * @param {string} message
60
+ * @param {...unknown} args - Additional values to log (objects, errors, etc.)
61
+ */
62
+ info(message, ...args) {
63
+ console.log(formatLine("info", this._withScope(message)), ...args);
64
+ }
65
+
66
+ /**
67
+ * Log a warning message.
68
+ * @param {string} message
69
+ * @param {...unknown} args
70
+ */
71
+ warn(message, ...args) {
72
+ console.warn(formatLine("warn", this._withScope(message)), ...args);
73
+ }
74
+
75
+ /**
76
+ * Log an error message.
77
+ * @param {string} message
78
+ * @param {...unknown} args
79
+ */
80
+ error(message, ...args) {
81
+ console.error(formatLine("error", this._withScope(message)), ...args);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Default shared logger instance, used internally by zayra-core.
87
+ * Most consumers can just import { logger } instead of constructing
88
+ * their own Logger instance.
89
+ */
90
+ export const logger = new Logger();
91
+
92
+ export default logger;
package/src/utils.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * utils.js
3
+ *
4
+ * Reusable helper functions shared across zayra-core.
5
+ * Grouped into: file handling, validation, formatting, path utilities.
6
+ *
7
+ * Kept dependency-free on purpose (only Node built-ins) so this
8
+ * package stays lightweight as the foundation for every other
9
+ * zayra-* package.
10
+ */
11
+
12
+ import { promises as fs } from "node:fs";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ /* -------------------------------------------------------------------------- */
17
+ /* File handling */
18
+ /* -------------------------------------------------------------------------- */
19
+
20
+ /**
21
+ * Checks whether a file or directory exists at the given path.
22
+ *
23
+ * @param {string} targetPath
24
+ * @returns {Promise<boolean>}
25
+ */
26
+ export async function fileExists(targetPath) {
27
+ try {
28
+ await fs.access(targetPath);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Reads and parses a JSON file.
37
+ *
38
+ * @param {string} filePath
39
+ * @returns {Promise<any>} Parsed JSON content.
40
+ * @throws {SyntaxError} If the file content is not valid JSON.
41
+ */
42
+ export async function readJsonFile(filePath) {
43
+ const raw = await fs.readFile(filePath, "utf-8");
44
+ return JSON.parse(raw);
45
+ }
46
+
47
+ /**
48
+ * Writes a JavaScript value to a file as formatted JSON.
49
+ *
50
+ * @param {string} filePath
51
+ * @param {any} data
52
+ * @param {object} [options]
53
+ * @param {number} [options.indent=2]
54
+ * @returns {Promise<void>}
55
+ */
56
+ export async function writeJsonFile(filePath, data, options = {}) {
57
+ const indent = options.indent ?? 2;
58
+ const content = JSON.stringify(data, null, indent);
59
+ await fs.writeFile(filePath, content, "utf-8");
60
+ }
61
+
62
+ /* -------------------------------------------------------------------------- */
63
+ /* Validation */
64
+ /* -------------------------------------------------------------------------- */
65
+
66
+ /**
67
+ * Returns true if the value is a non-null plain object
68
+ * (not an array, not null, not a class instance like Map/Set).
69
+ *
70
+ * @param {unknown} value
71
+ * @returns {boolean}
72
+ */
73
+ export function isPlainObject(value) {
74
+ return (
75
+ typeof value === "object" &&
76
+ value !== null &&
77
+ !Array.isArray(value) &&
78
+ Object.getPrototypeOf(value) === Object.prototype
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Returns true if the value is a non-empty string (after trimming).
84
+ *
85
+ * @param {unknown} value
86
+ * @returns {boolean}
87
+ */
88
+ export function isNonEmptyString(value) {
89
+ return typeof value === "string" && value.trim().length > 0;
90
+ }
91
+
92
+ /**
93
+ * Asserts that a condition is true, throwing a TypeError with the
94
+ * given message otherwise. Useful for lightweight argument guards
95
+ * without pulling in a validation library.
96
+ *
97
+ * @param {boolean} condition
98
+ * @param {string} message
99
+ * @throws {TypeError}
100
+ */
101
+ export function assert(condition, message) {
102
+ if (!condition) {
103
+ throw new TypeError(message);
104
+ }
105
+ }
106
+
107
+ /* -------------------------------------------------------------------------- */
108
+ /* Formatting */
109
+ /* -------------------------------------------------------------------------- */
110
+
111
+ /**
112
+ * Formats a Date (defaults to now) as an ISO-8601 string.
113
+ *
114
+ * @param {Date} [date]
115
+ * @returns {string}
116
+ */
117
+ export function formatTimestamp(date = new Date()) {
118
+ return date.toISOString();
119
+ }
120
+
121
+ /**
122
+ * Safely stringifies a value for logging/debugging, falling back to
123
+ * String(value) if JSON.stringify throws (e.g. circular references).
124
+ *
125
+ * @param {unknown} value
126
+ * @param {number} [indent=2]
127
+ * @returns {string}
128
+ */
129
+ export function safeStringify(value, indent = 2) {
130
+ try {
131
+ return JSON.stringify(value, null, indent);
132
+ } catch {
133
+ return String(value);
134
+ }
135
+ }
136
+
137
+ /* -------------------------------------------------------------------------- */
138
+ /* Path utilities */
139
+ /* -------------------------------------------------------------------------- */
140
+
141
+ /**
142
+ * Resolves the absolute directory name for an ES module, given its
143
+ * `import.meta.url`. Equivalent of CommonJS `__dirname` for ESM.
144
+ *
145
+ * Usage: const dir = dirnameFromMeta(import.meta.url);
146
+ *
147
+ * @param {string} metaUrl - Pass `import.meta.url` from the calling module.
148
+ * @returns {string}
149
+ */
150
+ export function dirnameFromMeta(metaUrl) {
151
+ return path.dirname(fileURLToPath(metaUrl));
152
+ }
153
+
154
+ /**
155
+ * Joins and resolves path segments into an absolute path.
156
+ *
157
+ * @param {...string} segments
158
+ * @returns {string}
159
+ */
160
+ export function resolvePath(...segments) {
161
+ return path.resolve(...segments);
162
+ }
163
+
164
+ export default {
165
+ fileExists,
166
+ readJsonFile,
167
+ writeJsonFile,
168
+ isPlainObject,
169
+ isNonEmptyString,
170
+ assert,
171
+ formatTimestamp,
172
+ safeStringify,
173
+ dirnameFromMeta,
174
+ resolvePath,
175
+ };
package/src/version.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * version.js
3
+ *
4
+ * Single source of truth for the current zayra-core version.
5
+ * Kept in sync with package.json manually for now (0.0.1 era).
6
+ * Other modules should import VERSION from here instead of
7
+ * hardcoding a version string anywhere else in the codebase.
8
+ */
9
+
10
+ export const VERSION = "0.0.1";
11
+
12
+ export default VERSION;