@noir-ai/core 1.0.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agaaaptr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @noir-ai/core
2
+
3
+ Shared domain types, the configuration schema (`NoirConfigSchema`), the `.noir/` on-disk layout, and the marker constants that the rest of the Noir toolkit is built on. This is the type-contract package — it holds no runtime I/O of its own.
4
+
5
+ Part of the **[Noir](https://github.com/agaaaptr/noir#readme)** toolkit — the discipline, context, and memory layer for any agentic CLI.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @noir-ai/core
11
+ ```
12
+
13
+ > Most users install the CLI instead, which pulls in the packages it needs:
14
+ >
15
+ > ```bash
16
+ > npm install -g @noir-ai/cli
17
+ > ```
18
+
19
+ This package is mainly consumed by the other `@noir-ai/*` packages. For programmatic usage, see the [root README](https://github.com/agaaaptr/noir#readme) and [`docs/usage.md`](https://github.com/agaaaptr/noir/blob/main/docs/usage.md).
20
+
21
+ ## License
22
+
23
+ MIT © agaaaptr
@@ -0,0 +1,112 @@
1
+ import * as z from 'zod';
2
+
3
+ declare const NoirConfigSchema: z.ZodObject<{
4
+ host: z.ZodLiteral<"claude">;
5
+ name: z.ZodOptional<z.ZodString>;
6
+ mode: z.ZodDefault<z.ZodEnum<{
7
+ full: "full";
8
+ quick: "quick";
9
+ }>>;
10
+ daemon: z.ZodDefault<z.ZodObject<{
11
+ idleTimeoutSec: z.ZodDefault<z.ZodNumber>;
12
+ port: z.ZodOptional<z.ZodNumber>;
13
+ }, z.core.$strip>>;
14
+ context: z.ZodDefault<z.ZodObject<{
15
+ embedder: z.ZodDefault<z.ZodObject<{
16
+ kind: z.ZodDefault<z.ZodEnum<{
17
+ local: "local";
18
+ remote: "remote";
19
+ ollama: "ollama";
20
+ none: "none";
21
+ }>>;
22
+ model: z.ZodOptional<z.ZodString>;
23
+ provider: z.ZodOptional<z.ZodString>;
24
+ baseURL: z.ZodOptional<z.ZodString>;
25
+ dim: z.ZodDefault<z.ZodNumber>;
26
+ }, z.core.$strip>>;
27
+ roots: z.ZodDefault<z.ZodArray<z.ZodString>>;
28
+ budgetTokens: z.ZodDefault<z.ZodNumber>;
29
+ }, z.core.$strip>>;
30
+ model: z.ZodDefault<z.ZodObject<{
31
+ defaultProvider: z.ZodOptional<z.ZodString>;
32
+ tiers: z.ZodOptional<z.ZodObject<{
33
+ draft: z.ZodOptional<z.ZodString>;
34
+ title: z.ZodOptional<z.ZodString>;
35
+ summarize: z.ZodOptional<z.ZodString>;
36
+ consolidate: z.ZodOptional<z.ZodString>;
37
+ }, z.core.$strip>>;
38
+ providers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
39
+ model: z.ZodString;
40
+ baseURL: z.ZodOptional<z.ZodString>;
41
+ apiKeyEnv: z.ZodOptional<z.ZodString>;
42
+ }, z.core.$strip>>>;
43
+ }, z.core.$strip>>;
44
+ memory: z.ZodDefault<z.ZodObject<{
45
+ consolidation: z.ZodDefault<z.ZodObject<{
46
+ enabled: z.ZodDefault<z.ZodBoolean>;
47
+ provider: z.ZodOptional<z.ZodString>;
48
+ model: z.ZodOptional<z.ZodString>;
49
+ types: z.ZodOptional<z.ZodArray<z.ZodString>>;
50
+ }, z.core.$strip>>;
51
+ }, z.core.$strip>>;
52
+ }, z.core.$strip>;
53
+ type NoirConfig = z.infer<typeof NoirConfigSchema>;
54
+ declare function parseConfig(raw: unknown): NoirConfig;
55
+
56
+ declare const NOIR_DIR = ".noir";
57
+ /**
58
+ * User-global Noir home: `~/.noir` (HOME-relative, NOT project `.noir/`).
59
+ *
60
+ * Hosts cross-project, user-scoped state — today the daemon record
61
+ * (`~/.noir/daemon.json`, written by @noir-ai/daemon) and the embedding-model
62
+ * cache ({@link modelsDir}). Kept HOME-relative so a single project checkout
63
+ * stays portable across machines (blueprint: `ProjectId` is canonical, never a
64
+ * filesystem path; the project `.noir/` dir never holds machine-local caches).
65
+ */
66
+ declare function noirHome(): string;
67
+ /**
68
+ * User-global cache for downloaded embedding-model weights: `~/.noir/models/`.
69
+ *
70
+ * Used by @noir-ai/context's local embedder (`@huggingface/transformers` pins
71
+ * `env.cacheDir` here on first load). HOME-relative per spec OQ-7 (resolved) —
72
+ * the ~22 MB MiniLM download is user-scoped, not per-project, so projects stay
73
+ * portable and the weight is fetched at most once per machine.
74
+ */
75
+ declare function modelsDir(): string;
76
+ declare const paths: {
77
+ readonly noirDir: (root: string) => string;
78
+ readonly noirMd: (root: string) => string;
79
+ readonly config: (root: string) => string;
80
+ readonly projectId: (root: string) => string;
81
+ readonly storeDir: (root: string) => string;
82
+ readonly storeDb: (root: string, projectId: string) => string;
83
+ readonly specsDir: (root: string) => string;
84
+ readonly specFile: (root: string, taskId: string, slug: string) => string;
85
+ readonly plansDir: (root: string) => string;
86
+ readonly planFile: (root: string, taskId: string, slug: string) => string;
87
+ readonly tasksDir: (root: string) => string;
88
+ readonly taskFile: (root: string, taskId: string, taskName: string) => string;
89
+ readonly decisionsDir: (root: string) => string;
90
+ readonly decisionFile: (root: string, n: number) => string;
91
+ readonly auditDir: (root: string) => string;
92
+ readonly auditFile: (root: string, taskId: string) => string;
93
+ };
94
+
95
+ declare const CONTEXT_BLOCK_BEGIN = "<!-- noir:context begin -->";
96
+ declare const CONTEXT_BLOCK_END = "<!-- noir:context end -->";
97
+
98
+ /** Canonical, machine-stable project identity. NEVER a filesystem path. */
99
+ type ProjectId = string;
100
+ declare function createProjectId(): ProjectId;
101
+
102
+ interface ProjectInfo {
103
+ id: ProjectId;
104
+ name: string;
105
+ root: string;
106
+ config: NoirConfig;
107
+ }
108
+ declare function loadProjectInfo(root: string): ProjectInfo;
109
+
110
+ declare const NOIR_VERSION: string;
111
+
112
+ export { CONTEXT_BLOCK_BEGIN, CONTEXT_BLOCK_END, NOIR_DIR, NOIR_VERSION, type NoirConfig, NoirConfigSchema, type ProjectId, type ProjectInfo, createProjectId, loadProjectInfo, modelsDir, noirHome, parseConfig, paths };
package/dist/index.js ADDED
@@ -0,0 +1,201 @@
1
+ // src/config.ts
2
+ import * as z from "zod";
3
+ var NoirConfigSchema = z.object({
4
+ host: z.literal("claude"),
5
+ name: z.string().optional(),
6
+ mode: z.enum(["full", "quick"]).default("full"),
7
+ daemon: z.object({
8
+ idleTimeoutSec: z.number().int().positive().default(900),
9
+ port: z.number().int().min(0).max(65535).optional()
10
+ }).default({ idleTimeoutSec: 900 }),
11
+ // Slice S6 context layer (@noir-ai/context). Mirrors the `daemon` idiom — a
12
+ // top-level object with `.default({})` so a config with NO `context:` block
13
+ // still parses and behaves as local-embedder-attempted (AC-7 / NFR-6). The
14
+ // embedder shape is `kind`-based to match the discriminated `EmbedderConfig`
15
+ // the context factory consumes; `resolveEmbedderConfig` (@noir-ai/context) is
16
+ // the single bridge from this user-facing schema to the factory input, so core
17
+ // never imports context (no core→context cycle). Provider-explicit, NEVER
18
+ // silent remote (blueprint D6): `kind:'remote'`/`'ollama'` are opt-in only;
19
+ // the default `'local'` is in-process, offline, free, private.
20
+ context: z.object({
21
+ embedder: z.object({
22
+ kind: z.enum(["local", "remote", "ollama", "none"]).default("local"),
23
+ // HF repo id (local) / provider model id (remote) / Ollama tag.
24
+ model: z.string().optional(),
25
+ // Remote provider key, e.g. 'openai' (only meaningful when kind:'remote').
26
+ // Free-form string so openai-compatible providers stay expressible; the
27
+ // resolver maps known names (openai/voyage/cohere) to their API-key env var.
28
+ provider: z.string().optional(),
29
+ // Ollama base URL, e.g. http://localhost:11434 (only when kind:'ollama').
30
+ baseURL: z.string().optional(),
31
+ // Target dimensionality — must be 384 to match the existing vec0 table.
32
+ dim: z.number().int().positive().default(384)
33
+ }).default({ kind: "local", dim: 384 }),
34
+ // Configured index roots (informational in core; the daemon/indexer consume).
35
+ roots: z.array(z.string()).default([]),
36
+ // Default token budget for a `context_search` result set (retriever default).
37
+ budgetTokens: z.number().int().positive().default(4096)
38
+ }).default({ embedder: { kind: "local", dim: 384 }, roots: [], budgetTokens: 4096 }),
39
+ // Slice S8 bounded model layer (@noir-ai/model). Mirrors the `daemon` idiom —
40
+ // a top-level object with `.default({})` so a config with NO `model:` block
41
+ // still parses and behaves as fully-degraded (every `complete()` call returns
42
+ // `null`; callers substitute a template — the always-available offline path,
43
+ // blueprint D5 / DS-5). `resolveModelConfig` (@noir-ai/model) is the single
44
+ // bridge from this user-facing schema to the runtime shape `complete()`
45
+ // consumes, so core never imports model (no core→model cycle).
46
+ //
47
+ // Provider-EXPLICIT, never silent paid (blueprint D5 / DS-6): the provider is
48
+ // resolved ONLY from explicit `defaultProvider` / a tier's provider key. Env-
49
+ // var presence is NEVER consulted to pick a provider — `ANTHROPIC_API_KEY`
50
+ // being set for another tool does NOT activate Anthropic in Noir. No explicit,
51
+ // configured provider ⇒ `null`. Secrets live in env vars only (DS-8):
52
+ // `apiKeyEnv` stores the env-var NAME (`ANTHROPIC_API_KEY`), never the value,
53
+ // so `.noir/config.yml` stays safe to commit and share; `complete()` reads the
54
+ // value at call time. `tiers` map a tier name → provider block key; each
55
+ // `providers[name]` declares the model id, optional `baseURL` (openai-compat),
56
+ // and optional `apiKeyEnv` (omit for anonymous local providers like Ollama).
57
+ model: z.object({
58
+ // Fallback provider name (key into `providers`) when a call's tier resolves
59
+ // no explicit provider. Free-form string so openai-compatible providers stay
60
+ // expressible without an enum churn.
61
+ defaultProvider: z.string().optional(),
62
+ // Per-tier provider-key overrides (draft / title / summarize / consolidate).
63
+ // Each value is a key into `providers{}`; resolution is the consumer's job
64
+ // (tier → tier.provider → defaultProvider → providers[name]).
65
+ tiers: z.object({
66
+ draft: z.string().optional(),
67
+ title: z.string().optional(),
68
+ summarize: z.string().optional(),
69
+ consolidate: z.string().optional()
70
+ }).optional(),
71
+ // Configured provider blocks, keyed by name. `model` is required (a provider
72
+ // without a model id is meaningless); `apiKeyEnv` is omitted for anonymous
73
+ // local providers (Ollama / LM Studio) which then send no auth header.
74
+ providers: z.record(
75
+ z.string(),
76
+ z.object({
77
+ model: z.string(),
78
+ baseURL: z.string().optional(),
79
+ apiKeyEnv: z.string().optional()
80
+ })
81
+ ).optional()
82
+ }).default({}),
83
+ // Slice S7 cross-session memory (@noir-ai/memory). Mirrors the `daemon` idiom —
84
+ // a top-level object with `.default({})` so a config with NO `memory:` block
85
+ // still parses and behaves as consolidation-disabled (the safe default —
86
+ // capture/store/retrieve are always local + free; consolidation is the ONLY
87
+ // LLM touch and it is opt-in + provider-explicit). `resolveMemoryConfig`
88
+ // (@noir-ai/memory) is the single bridge from this user-facing schema to the
89
+ // runtime `MemoryConfig` the engine consumes, so core never imports memory
90
+ // (no core→memory cycle; mirrors @noir-ai/context + @noir-ai/model).
91
+ //
92
+ // Provider-EXPLICIT, never silent paid (blueprint D6 / DS-6): the provider is
93
+ // resolved ONLY from explicit `consolidation.provider`. Env-var presence is
94
+ // NEVER consulted to pick a provider — `ANTHROPIC_API_KEY` being set for
95
+ // another tool does NOT activate consolidation. No explicit, enabled provider
96
+ // ⇒ `runConsolidation` refuses with `'no-provider'` + writes a miss audit, and
97
+ // NO S8 `complete()` call is made (the Agent-Memory anti-pattern, §9). The
98
+ // outer default matches the parsed output shape (Zod v4 requirement).
99
+ memory: z.object({
100
+ consolidation: z.object({
101
+ // Master switch (default false). When false, `consolidate` refuses +
102
+ // logs (`no-provider`) regardless of provider/model — the first gate.
103
+ enabled: z.boolean().default(false),
104
+ // Provider key, e.g. 'anthropic' | 'openai' | 'ollama'. Free-form
105
+ // string so openai-compatible providers stay expressible without an
106
+ // enum churn; required (alongside enabled) for consolidation to run.
107
+ provider: z.string().optional(),
108
+ // Provider-specific model id (consumed by S8 complete(); optional
109
+ // here only because a future anonymous local provider may not need it
110
+ // — runConsolidation still refuses when model is absent).
111
+ model: z.string().optional(),
112
+ // Restrict candidates to these types (default: every non-`lesson`).
113
+ types: z.array(z.string()).optional()
114
+ }).default({ enabled: false })
115
+ }).default({ consolidation: { enabled: false } })
116
+ });
117
+ function parseConfig(raw) {
118
+ return NoirConfigSchema.parse(raw);
119
+ }
120
+
121
+ // src/layout.ts
122
+ import { homedir } from "os";
123
+ import { join } from "path";
124
+ var NOIR_DIR = ".noir";
125
+ function noirHome() {
126
+ return join(homedir(), NOIR_DIR);
127
+ }
128
+ function modelsDir() {
129
+ return join(noirHome(), "models");
130
+ }
131
+ var paths = {
132
+ noirDir: (root) => join(root, NOIR_DIR),
133
+ noirMd: (root) => join(root, NOIR_DIR, "NOIR.md"),
134
+ config: (root) => join(root, NOIR_DIR, "config.yml"),
135
+ projectId: (root) => join(root, NOIR_DIR, "project.id"),
136
+ storeDir: (root) => join(root, NOIR_DIR, "store"),
137
+ storeDb: (root, projectId) => join(root, NOIR_DIR, "store", `${projectId}.db`),
138
+ // Artifact directories and files
139
+ specsDir: (root) => join(root, NOIR_DIR, "specs"),
140
+ specFile: (root, taskId, slug) => join(root, NOIR_DIR, "specs", `${taskId}-${slug}.md`),
141
+ plansDir: (root) => join(root, NOIR_DIR, "plans"),
142
+ planFile: (root, taskId, slug) => join(root, NOIR_DIR, "plans", `${taskId}-${slug}.md`),
143
+ tasksDir: (root) => join(root, NOIR_DIR, "tasks"),
144
+ taskFile: (root, taskId, taskName) => join(root, NOIR_DIR, "tasks", `${taskId}-${taskName}.md`),
145
+ decisionsDir: (root) => join(root, NOIR_DIR, "decisions"),
146
+ decisionFile: (root, n) => join(root, NOIR_DIR, "decisions", `${String(n).padStart(4, "0")}.md`),
147
+ auditDir: (root) => join(root, NOIR_DIR, "audit"),
148
+ auditFile: (root, taskId) => join(root, NOIR_DIR, "audit", `${taskId}.json`)
149
+ };
150
+
151
+ // src/markers.ts
152
+ var CONTEXT_BLOCK_BEGIN = "<!-- noir:context begin -->";
153
+ var CONTEXT_BLOCK_END = "<!-- noir:context end -->";
154
+
155
+ // src/project.ts
156
+ import { readFileSync } from "fs";
157
+ import { basename } from "path";
158
+ import { parse as parseYaml } from "yaml";
159
+ function loadProjectInfo(root) {
160
+ let rawId;
161
+ let rawConfig;
162
+ try {
163
+ rawId = readFileSync(paths.projectId(root), "utf8").trim();
164
+ rawConfig = parseYaml(readFileSync(paths.config(root), "utf8"));
165
+ } catch {
166
+ throw new Error(`Noir is not initialized in ${root}. Run \`noir init\` first.`);
167
+ }
168
+ const config = parseConfig(rawConfig);
169
+ return {
170
+ id: rawId,
171
+ name: config.name ?? basename(root),
172
+ root,
173
+ config
174
+ };
175
+ }
176
+
177
+ // src/project-id.ts
178
+ import { randomUUID } from "crypto";
179
+ function createProjectId() {
180
+ return randomUUID();
181
+ }
182
+
183
+ // src/version.ts
184
+ import { readFileSync as readFileSync2 } from "fs";
185
+ import { fileURLToPath } from "url";
186
+ var pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
187
+ var NOIR_VERSION = JSON.parse(readFileSync2(pkgPath, "utf8")).version ?? "0.0.0";
188
+ export {
189
+ CONTEXT_BLOCK_BEGIN,
190
+ CONTEXT_BLOCK_END,
191
+ NOIR_DIR,
192
+ NOIR_VERSION,
193
+ NoirConfigSchema,
194
+ createProjectId,
195
+ loadProjectInfo,
196
+ modelsDir,
197
+ noirHome,
198
+ parseConfig,
199
+ paths
200
+ };
201
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/layout.ts","../src/markers.ts","../src/project.ts","../src/project-id.ts","../src/version.ts"],"sourcesContent":["import * as z from 'zod';\n\nexport const NoirConfigSchema = z.object({\n host: z.literal('claude'),\n name: z.string().optional(),\n mode: z.enum(['full', 'quick']).default('full'),\n daemon: z\n .object({\n idleTimeoutSec: z.number().int().positive().default(900),\n port: z.number().int().min(0).max(65535).optional(),\n })\n .default({ idleTimeoutSec: 900 }),\n // Slice S6 context layer (@noir-ai/context). Mirrors the `daemon` idiom — a\n // top-level object with `.default({})` so a config with NO `context:` block\n // still parses and behaves as local-embedder-attempted (AC-7 / NFR-6). The\n // embedder shape is `kind`-based to match the discriminated `EmbedderConfig`\n // the context factory consumes; `resolveEmbedderConfig` (@noir-ai/context) is\n // the single bridge from this user-facing schema to the factory input, so core\n // never imports context (no core→context cycle). Provider-explicit, NEVER\n // silent remote (blueprint D6): `kind:'remote'`/`'ollama'` are opt-in only;\n // the default `'local'` is in-process, offline, free, private.\n context: z\n .object({\n embedder: z\n .object({\n kind: z.enum(['local', 'remote', 'ollama', 'none']).default('local'),\n // HF repo id (local) / provider model id (remote) / Ollama tag.\n model: z.string().optional(),\n // Remote provider key, e.g. 'openai' (only meaningful when kind:'remote').\n // Free-form string so openai-compatible providers stay expressible; the\n // resolver maps known names (openai/voyage/cohere) to their API-key env var.\n provider: z.string().optional(),\n // Ollama base URL, e.g. http://localhost:11434 (only when kind:'ollama').\n baseURL: z.string().optional(),\n // Target dimensionality — must be 384 to match the existing vec0 table.\n dim: z.number().int().positive().default(384),\n })\n .default({ kind: 'local', dim: 384 }),\n // Configured index roots (informational in core; the daemon/indexer consume).\n roots: z.array(z.string()).default([]),\n // Default token budget for a `context_search` result set (retriever default).\n budgetTokens: z.number().int().positive().default(4096),\n })\n // Zod v4 requires the outer default to match the parsed output shape (every\n // inner field already carries its own default, so an absent `context:` block\n // still resolves to local-embedder/empty-roots/4096). Mirrors `daemon:`.\n .default({ embedder: { kind: 'local', dim: 384 }, roots: [], budgetTokens: 4096 }),\n // Slice S8 bounded model layer (@noir-ai/model). Mirrors the `daemon` idiom —\n // a top-level object with `.default({})` so a config with NO `model:` block\n // still parses and behaves as fully-degraded (every `complete()` call returns\n // `null`; callers substitute a template — the always-available offline path,\n // blueprint D5 / DS-5). `resolveModelConfig` (@noir-ai/model) is the single\n // bridge from this user-facing schema to the runtime shape `complete()`\n // consumes, so core never imports model (no core→model cycle).\n //\n // Provider-EXPLICIT, never silent paid (blueprint D5 / DS-6): the provider is\n // resolved ONLY from explicit `defaultProvider` / a tier's provider key. Env-\n // var presence is NEVER consulted to pick a provider — `ANTHROPIC_API_KEY`\n // being set for another tool does NOT activate Anthropic in Noir. No explicit,\n // configured provider ⇒ `null`. Secrets live in env vars only (DS-8):\n // `apiKeyEnv` stores the env-var NAME (`ANTHROPIC_API_KEY`), never the value,\n // so `.noir/config.yml` stays safe to commit and share; `complete()` reads the\n // value at call time. `tiers` map a tier name → provider block key; each\n // `providers[name]` declares the model id, optional `baseURL` (openai-compat),\n // and optional `apiKeyEnv` (omit for anonymous local providers like Ollama).\n model: z\n .object({\n // Fallback provider name (key into `providers`) when a call's tier resolves\n // no explicit provider. Free-form string so openai-compatible providers stay\n // expressible without an enum churn.\n defaultProvider: z.string().optional(),\n // Per-tier provider-key overrides (draft / title / summarize / consolidate).\n // Each value is a key into `providers{}`; resolution is the consumer's job\n // (tier → tier.provider → defaultProvider → providers[name]).\n tiers: z\n .object({\n draft: z.string().optional(),\n title: z.string().optional(),\n summarize: z.string().optional(),\n consolidate: z.string().optional(),\n })\n .optional(),\n // Configured provider blocks, keyed by name. `model` is required (a provider\n // without a model id is meaningless); `apiKeyEnv` is omitted for anonymous\n // local providers (Ollama / LM Studio) which then send no auth header.\n providers: z\n .record(\n z.string(),\n z.object({\n model: z.string(),\n baseURL: z.string().optional(),\n apiKeyEnv: z.string().optional(),\n }),\n )\n .optional(),\n })\n // Absent `model:` block ⇒ `{}` ⇒ every tier/provider is undefined ⇒ full\n // degradation (offline, free, the default). Inner fields are `.optional()`\n // (no inner `.default()`) so a present-but-empty block also degrades.\n .default({}),\n // Slice S7 cross-session memory (@noir-ai/memory). Mirrors the `daemon` idiom —\n // a top-level object with `.default({})` so a config with NO `memory:` block\n // still parses and behaves as consolidation-disabled (the safe default —\n // capture/store/retrieve are always local + free; consolidation is the ONLY\n // LLM touch and it is opt-in + provider-explicit). `resolveMemoryConfig`\n // (@noir-ai/memory) is the single bridge from this user-facing schema to the\n // runtime `MemoryConfig` the engine consumes, so core never imports memory\n // (no core→memory cycle; mirrors @noir-ai/context + @noir-ai/model).\n //\n // Provider-EXPLICIT, never silent paid (blueprint D6 / DS-6): the provider is\n // resolved ONLY from explicit `consolidation.provider`. Env-var presence is\n // NEVER consulted to pick a provider — `ANTHROPIC_API_KEY` being set for\n // another tool does NOT activate consolidation. No explicit, enabled provider\n // ⇒ `runConsolidation` refuses with `'no-provider'` + writes a miss audit, and\n // NO S8 `complete()` call is made (the Agent-Memory anti-pattern, §9). The\n // outer default matches the parsed output shape (Zod v4 requirement).\n memory: z\n .object({\n consolidation: z\n .object({\n // Master switch (default false). When false, `consolidate` refuses +\n // logs (`no-provider`) regardless of provider/model — the first gate.\n enabled: z.boolean().default(false),\n // Provider key, e.g. 'anthropic' | 'openai' | 'ollama'. Free-form\n // string so openai-compatible providers stay expressible without an\n // enum churn; required (alongside enabled) for consolidation to run.\n provider: z.string().optional(),\n // Provider-specific model id (consumed by S8 complete(); optional\n // here only because a future anonymous local provider may not need it\n // — runConsolidation still refuses when model is absent).\n model: z.string().optional(),\n // Restrict candidates to these types (default: every non-`lesson`).\n types: z.array(z.string()).optional(),\n })\n // Absent consolidation block ⇒ disabled. Outer shape matches output.\n .default({ enabled: false }),\n })\n .default({ consolidation: { enabled: false } }),\n});\n\nexport type NoirConfig = z.infer<typeof NoirConfigSchema>;\n\nexport function parseConfig(raw: unknown): NoirConfig {\n return NoirConfigSchema.parse(raw);\n}\n","import { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nexport const NOIR_DIR = '.noir';\n\n/**\n * User-global Noir home: `~/.noir` (HOME-relative, NOT project `.noir/`).\n *\n * Hosts cross-project, user-scoped state — today the daemon record\n * (`~/.noir/daemon.json`, written by @noir-ai/daemon) and the embedding-model\n * cache ({@link modelsDir}). Kept HOME-relative so a single project checkout\n * stays portable across machines (blueprint: `ProjectId` is canonical, never a\n * filesystem path; the project `.noir/` dir never holds machine-local caches).\n */\nexport function noirHome(): string {\n return join(homedir(), NOIR_DIR);\n}\n\n/**\n * User-global cache for downloaded embedding-model weights: `~/.noir/models/`.\n *\n * Used by @noir-ai/context's local embedder (`@huggingface/transformers` pins\n * `env.cacheDir` here on first load). HOME-relative per spec OQ-7 (resolved) —\n * the ~22 MB MiniLM download is user-scoped, not per-project, so projects stay\n * portable and the weight is fetched at most once per machine.\n */\nexport function modelsDir(): string {\n return join(noirHome(), 'models');\n}\n\nexport const paths = {\n noirDir: (root: string) => join(root, NOIR_DIR),\n noirMd: (root: string) => join(root, NOIR_DIR, 'NOIR.md'),\n config: (root: string) => join(root, NOIR_DIR, 'config.yml'),\n projectId: (root: string) => join(root, NOIR_DIR, 'project.id'),\n storeDir: (root: string) => join(root, NOIR_DIR, 'store'),\n storeDb: (root: string, projectId: string) => join(root, NOIR_DIR, 'store', `${projectId}.db`),\n // Artifact directories and files\n specsDir: (root: string) => join(root, NOIR_DIR, 'specs'),\n specFile: (root: string, taskId: string, slug: string) =>\n join(root, NOIR_DIR, 'specs', `${taskId}-${slug}.md`),\n plansDir: (root: string) => join(root, NOIR_DIR, 'plans'),\n planFile: (root: string, taskId: string, slug: string) =>\n join(root, NOIR_DIR, 'plans', `${taskId}-${slug}.md`),\n tasksDir: (root: string) => join(root, NOIR_DIR, 'tasks'),\n taskFile: (root: string, taskId: string, taskName: string) =>\n join(root, NOIR_DIR, 'tasks', `${taskId}-${taskName}.md`),\n decisionsDir: (root: string) => join(root, NOIR_DIR, 'decisions'),\n decisionFile: (root: string, n: number) =>\n join(root, NOIR_DIR, 'decisions', `${String(n).padStart(4, '0')}.md`),\n auditDir: (root: string) => join(root, NOIR_DIR, 'audit'),\n auditFile: (root: string, taskId: string) => join(root, NOIR_DIR, 'audit', `${taskId}.json`),\n} as const;\n","export const CONTEXT_BLOCK_BEGIN = '<!-- noir:context begin -->';\nexport const CONTEXT_BLOCK_END = '<!-- noir:context end -->';\n","import { readFileSync } from 'node:fs';\nimport { basename } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { type NoirConfig, parseConfig } from './config.js';\nimport { paths } from './layout.js';\nimport type { ProjectId } from './project-id.js';\n\nexport interface ProjectInfo {\n id: ProjectId;\n name: string;\n root: string;\n config: NoirConfig;\n}\n\nexport function loadProjectInfo(root: string): ProjectInfo {\n let rawId: string;\n let rawConfig: unknown;\n try {\n rawId = readFileSync(paths.projectId(root), 'utf8').trim();\n rawConfig = parseYaml(readFileSync(paths.config(root), 'utf8'));\n } catch {\n throw new Error(`Noir is not initialized in ${root}. Run \\`noir init\\` first.`);\n }\n const config = parseConfig(rawConfig);\n return {\n id: rawId,\n name: config.name ?? basename(root),\n root,\n config,\n };\n}\n","import { randomUUID } from 'node:crypto';\n\n/** Canonical, machine-stable project identity. NEVER a filesystem path. */\nexport type ProjectId = string;\n\nexport function createProjectId(): ProjectId {\n return randomUUID();\n}\n","import { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\n\n// Read the version from THIS package's package.json at module load (NOT a hardcoded\n// constant) so a release bump (scripts/bump-version.mjs) is reflected everywhere\n// `noir` self-identifies — `noir --version`, `noir doctor`/`status`, the MCP\n// initialize handshake — with zero drift between package.json and the binary.\n//\n// `import.meta.url` resolves correctly in every layout the package ships in:\n// src (vitest): packages/core/src/version.ts → ../package.json = packages/core/package.json\n// dist (tsup): packages/core/dist/index.js → ../package.json = packages/core/package.json\n// published: node_modules/@noir-ai/core/dist/... → ../package.json = @noir-ai/core/package.json\n// (npm ALWAYS ships package.json alongside dist/, so the read is safe in the tarball.)\nconst pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));\nexport const NOIR_VERSION: string =\n (JSON.parse(readFileSync(pkgPath, 'utf8')) as { version?: string }).version ?? '0.0.0';\n"],"mappings":"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","names":["readFileSync"]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@noir-ai/core",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "Noir core — shared types, the config schema (NoirConfigSchema), the .noir/ layout, and markers.",
5
+ "license": "MIT",
6
+ "author": "agaaaptr",
7
+ "homepage": "https://github.com/agaaaptr/noir#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/agaaaptr/noir.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/agaaaptr/noir/issues"
15
+ },
16
+ "keywords": [
17
+ "noir",
18
+ "agent",
19
+ "sdd",
20
+ "spec-driven",
21
+ "config",
22
+ "domain",
23
+ "toolkit"
24
+ ],
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "provenance": true
31
+ },
32
+ "type": "module",
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "README.md"
44
+ ],
45
+ "dependencies": {
46
+ "yaml": "^2.5.0",
47
+ "zod": "^4.2.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^26.1.1"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup",
54
+ "typecheck": "tsc --noEmit"
55
+ }
56
+ }