@noir-ai/daemon 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,21 @@
1
+ # @noir-ai/daemon
2
+
3
+ The runtime authority: owns the store write handle, resolves the embedder once, and exposes the single Noir MCP server (stdio + Streamable HTTP). Store-touching CLI commands are MCP clients to this daemon; a read-only filesystem fallback covers the daemon-down case.
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/daemon
11
+ ```
12
+
13
+ > Most users install the CLI instead, which manages the daemon lifecycle (`noir daemon start|stop|status`):
14
+ >
15
+ > ```bash
16
+ > npm install -g @noir-ai/cli
17
+ > ```
18
+
19
+ ## License
20
+
21
+ MIT © agaaaptr
@@ -0,0 +1,324 @@
1
+ import { EmbedderConfig, ContextEngine, EmbedFn } from '@noir-ai/context';
2
+ export { ContextStatus } from '@noir-ai/context';
3
+ import { ProjectId, ProjectInfo } from '@noir-ai/core';
4
+ import { Store } from '@noir-ai/store';
5
+ import { MemoryConfig, MemoryEngine } from '@noir-ai/memory';
6
+ import { ResolvedModelConfig } from '@noir-ai/model';
7
+ import { McpServer } from '@modelcontextprotocol/server';
8
+ import { WorkflowEngine, Phase, WorkflowState, Mode, GateResult } from '@noir-ai/workflow';
9
+
10
+ /**
11
+ * Build the daemon's {@link ContextEngine} from its already-open store handle
12
+ * + the project `root` + `projectId` + a resolved {@link EmbedderConfig}.
13
+ *
14
+ * One engine per serve lifecycle — constructed once alongside the store (see
15
+ * {@link openStoreForDaemon}) and the workflow engine (see
16
+ * {@link buildWorkflowEngine}), and reused across every HTTP request, exactly
17
+ * as those handles are. The engine resolves its embedder once (a lazy local
18
+ * model loads at most once per lifecycle) and then owns the indexer (the only
19
+ * context writer) + the retriever (the only context reader) over the SAME
20
+ * injected handle — it never opens a second connection, so the daemon's
21
+ * single-writer discipline is preserved (blueprint D6: in-process, no sidecar,
22
+ * canonical `ProjectId`).
23
+ *
24
+ * Degraded story (mirrors the store + the engine's own contract): pass the
25
+ * store's `storeDegraded` flag so the engine's persistent `degraded` field is
26
+ * honest — `context_status` then reports `degraded:true` for a read-only
27
+ * (daemon-down) handle, and `context_index` short-circuits with a clear error
28
+ * envelope instead of letting the first write throw mid-run. A `kind:'none'`
29
+ * embedder on a WRITABLE store is NOT blocking here: docs still index without
30
+ * vectors (the indexer's tested degraded path), so only the read-only case is
31
+ * fenced off at the tool boundary.
32
+ */
33
+ declare function buildContextEngine(store: Store, root: string, projectId: ProjectId, embedderCfg: EmbedderConfig, storeDegraded?: boolean): ContextEngine;
34
+
35
+ interface EnsureResult {
36
+ port: number;
37
+ url: string;
38
+ started: boolean;
39
+ /**
40
+ * Stops the daemon THIS call started. When `started` is true this closes the
41
+ * in-process http server, clears `daemon.json`, and clears the idle timer.
42
+ * When `started` is false (a healthy daemon was reused) this is a no-op: the
43
+ * reused daemon is owned by whichever process started it (in tests that pid is
44
+ * `process.pid`, so killing it would be fatal) and must not be torn down by a
45
+ * mere consumer.
46
+ */
47
+ stop: () => Promise<void>;
48
+ }
49
+ declare function ensureDaemonRunning(opts: {
50
+ project: ProjectInfo;
51
+ idleTimeoutSec: number;
52
+ }): Promise<EnsureResult>;
53
+
54
+ interface StartHttpOptions {
55
+ project: ProjectInfo;
56
+ port?: number;
57
+ idleTimeoutSec: number;
58
+ }
59
+ interface RunningDaemon {
60
+ port: number;
61
+ pid: number;
62
+ startedAt: number;
63
+ stop: () => Promise<void>;
64
+ }
65
+ declare function startHttpServer(opts: StartHttpOptions): Promise<RunningDaemon>;
66
+
67
+ interface DaemonRecord {
68
+ pid: number;
69
+ port: number;
70
+ startedAt: number;
71
+ }
72
+ declare function noirHome(): string;
73
+ declare function daemonJsonPath(): string;
74
+ declare function readDaemonRecord(): DaemonRecord | null;
75
+ declare function writeDaemonRecord(rec: DaemonRecord): void;
76
+ declare function clearDaemonRecord(): void;
77
+ declare function pidAlive(pid: number): boolean;
78
+
79
+ /**
80
+ * Derive the consolidation provider + model id from a resolved MODEL config —
81
+ * the MODEL-DERIVED FALLBACK used ONLY when the user enabled consolidation
82
+ * under `memory:` but did NOT name a provider there (see
83
+ * {@link resolveConsolidationCapability}). Blueprint D5/D6 — provider-EXPLICIT,
84
+ * never env-inferred.
85
+ *
86
+ * The provider is resolved ONLY from an explicit opt-in: the `consolidate` tier
87
+ * key, falling back to `defaultProvider`. Env-var presence is NEVER consulted
88
+ * (DS-6) — `ANTHROPIC_API_KEY` being set for another tool does NOT activate
89
+ * consolidation here. A provider is "consolidation-capable" only when that key
90
+ * resolves to a configured provider block that itself declares a `model` id;
91
+ * otherwise this returns `null`.
92
+ *
93
+ * NOTE: this function ALONE is NOT the consolidation gate — it ignores the
94
+ * user's `memory.consolidation.enabled` master switch. The AND of that switch
95
+ * with this derivation (and with the memory-block provider) lives in
96
+ * {@link resolveConsolidationCapability}, which is what the daemon wiring +
97
+ * {@link buildMemoryEngine} actually consult to decide tool registration.
98
+ */
99
+ declare function resolveMemoryConsolidation(modelCfg: ResolvedModelConfig | undefined): {
100
+ provider: string;
101
+ model: string;
102
+ } | null;
103
+ /**
104
+ * The consolidation capability gate — the SINGLE source of truth both
105
+ * {@link buildMemoryEngine} and the daemon wiring (stdio/http) consult to decide
106
+ * (a) whether the `memory_consolidate` MCP tool registers and (b) whether the
107
+ * engine's `consolidate` can run. Returns the usable `{provider, model}` when
108
+ * consolidation is ON, or `null` when it must be fully off.
109
+ *
110
+ * The gate is the AND of ALL three (blueprint D6 / §9 — NEVER a silent paid
111
+ * call, the Agent-Memory anti-pattern):
112
+ * (a) `resolvedMemory.consolidation.enabled === true` — the user's EXPLICIT
113
+ * master switch under `memory:`. This is the LOAD-BEARING gate: a config
114
+ * with no `memory:` block (or `enabled:false`) resolves to `null`
115
+ * REGARDLESS of `model.defaultProvider`, so a `model:` block set for
116
+ * summarize/title/draft does NOT silently enable a paid consolidation call.
117
+ * (b) a usable provider+model — prefer the one named under `memory:` (with its
118
+ * own `model` id); fall back to the MODEL-derived derivation
119
+ * ({@link resolveMemoryConsolidation}) ONLY when the user enabled
120
+ * consolidation but did NOT name a provider under `memory:`.
121
+ * (c) the resolved pair has both a `provider` and a `model` id.
122
+ *
123
+ * `resolvedMemory` is the output of `resolveMemoryConfig(config.memory)` — the
124
+ * pure core→memory bridge (no env inference, no cycle). Pure projection — reads
125
+ * NO env, holds NO secrets, never throws. Whether the provider is actually
126
+ * CALLABLE at runtime (key present, network up) is decided inside `complete()`
127
+ * (S8) — here we only decide config-time capability.
128
+ */
129
+ declare function resolveConsolidationCapability(resolvedMemory: MemoryConfig | undefined, modelCfg: ResolvedModelConfig | undefined): {
130
+ provider: string;
131
+ model: string;
132
+ } | null;
133
+ /**
134
+ * Build the daemon's {@link MemoryEngine} from its already-open store handle +
135
+ * the project `root` + `projectId` + the SAME `EmbedFn` the daemon resolved once
136
+ * for S6 (the daemon owns one embedder; memory takes `{store, embed, ...}`, no
137
+ * embedder duplication — plan §Architecture).
138
+ *
139
+ * One engine per serve lifecycle — constructed once alongside the store (see
140
+ * {@link openStoreForDaemon}), the workflow engine (see
141
+ * {@link buildWorkflowEngine}), and the context engine (see
142
+ * {@link buildContextEngine}), and reused across every HTTP request, exactly as
143
+ * those handles are. The engine — like the context indexer — is the ONLY thing
144
+ * that writes `source:'memory'` rows through the injected handle; it never opens
145
+ * a second connection, so the daemon's single-writer discipline is preserved
146
+ * (blueprint D6: in-process, no sidecar, canonical `ProjectId`).
147
+ *
148
+ * Consolidation is OPT-IN + provider-explicit (DS-6 / §9 — NEVER a silent paid
149
+ * call). The capability gate is {@link resolveConsolidationCapability} — the AND
150
+ * of the user's `memory.consolidation.enabled` master switch (the load-bearing
151
+ * gate), a usable provider+model (preferring the `memory:` block, falling back
152
+ * to the model-derived derivation when enabled but no provider named under
153
+ * `memory:`), and `modelCfg` being present to bind S8's `complete`. When the
154
+ * gate resolves a `{provider, model}`:
155
+ * - S8's {@link complete} is bound as the engine's model injection, AND
156
+ * - the runtime gate `config.consolidation` is set so `consolidate` can run.
157
+ * When the gate is `null` — most importantly when `enabled === false`, regardless
158
+ * of `model.defaultProvider` — NO model is wired and `engine.consolidate`
159
+ * self-refuses (`'no-provider'`) WITHOUT calling the model. The daemon registers
160
+ * the `memory_consolidate` MCP tool only in the capable case.
161
+ *
162
+ * `resolvedMemory` is the output of `resolveMemoryConfig(config.memory)` — the
163
+ * pure core→memory bridge. Passing it here means the engine's `config` reflects
164
+ * the user's `memory:` consent exactly (never a hardcoded `enabled:true`).
165
+ *
166
+ * Degraded story (mirrors the store + the context engine): pass the store's
167
+ * `storeDegraded` flag so the engine's persistent `degraded` field is honest —
168
+ * `memory_save` / `memory_forget` then refuse with a clear envelope (the engine
169
+ * throws upfront on a read-only handle) while reads (`memory_recall` /
170
+ * `memory_search` / `memory_sessions`) keep working off the same handle.
171
+ */
172
+ declare function buildMemoryEngine(store: Store, root: string, projectId: ProjectId, embed: EmbedFn, modelCfg?: ResolvedModelConfig, storeDegraded?: boolean, resolvedMemory?: MemoryConfig): MemoryEngine;
173
+
174
+ type Transport = 'stdio' | 'streamable-http';
175
+ interface HostStatus {
176
+ noir: string;
177
+ project: {
178
+ id: string;
179
+ name: string;
180
+ };
181
+ host: string;
182
+ transport: Transport;
183
+ daemon: boolean;
184
+ pid?: number;
185
+ uptimeSec?: number;
186
+ }
187
+ interface StatusContext {
188
+ transport: Transport;
189
+ daemon: boolean;
190
+ pid?: number;
191
+ startedAt?: number;
192
+ }
193
+ declare function buildStatus(project: ProjectInfo, ctx: StatusContext): HostStatus;
194
+
195
+ interface ServerContext {
196
+ project: ProjectInfo;
197
+ transport: Transport;
198
+ daemon: boolean;
199
+ pid?: number;
200
+ startedAt?: number;
201
+ /**
202
+ * Optional store handle. When present, the `store_status` tool is
203
+ * registered. The daemon is the single writer; stdio/HTTP serve paths open
204
+ * the store once per serve lifecycle and reuse the same handle.
205
+ */
206
+ store?: Store;
207
+ /** Filesystem path to the store DB (reported by `store_status`). */
208
+ dbPath?: string;
209
+ /** True when the store was opened read-only (writable open failed). */
210
+ storeDegraded?: boolean;
211
+ /**
212
+ * Optional workflow engine. When present, the `workflow_status` and
213
+ * `checkpoint` tools are registered. Built once per serve lifecycle from the
214
+ * same store handle (see {@link buildWorkflowEngine}) and reused across HTTP
215
+ * requests, mirroring the store.
216
+ */
217
+ engine?: WorkflowEngine;
218
+ /**
219
+ * Optional context engine. When present, the `context_search`,
220
+ * `context_index`, and `context_status` tools are registered. Built once per
221
+ * serve lifecycle from the same store handle (see `buildContextEngine` in
222
+ * `./context-seam.js`) and reused across HTTP requests, mirroring the store
223
+ * + engine. The engine — through its indexer — is the only thing that writes
224
+ * context rows, so the daemon's single-writer discipline is preserved.
225
+ */
226
+ context?: ContextEngine;
227
+ /**
228
+ * Optional memory engine. When present, the `memory_save`, `memory_recall`,
229
+ * `memory_search`, `memory_sessions`, and `memory_forget` tools are
230
+ * registered. Built once per serve lifecycle from the same store handle + the
231
+ * SAME `EmbedFn` already resolved for S6 (see `buildMemoryEngine` in
232
+ * `./memory-seam.js`) and reused across HTTP requests, mirroring the store +
233
+ * engine + context. The engine is the only thing that writes `source:memory`
234
+ * rows through the injected handle, so the daemon's single-writer discipline
235
+ * is preserved (blueprint D6: in-process, no sidecar, canonical `ProjectId`).
236
+ */
237
+ memory?: MemoryEngine;
238
+ /**
239
+ * Whether the {@link memory} engine is consolidation-capable — i.e. the user
240
+ * opted in (`memory.consolidation.enabled === true`) AND a usable provider+model
241
+ * resolved (see `resolveConsolidationCapability` in `./memory-seam.js`, the AND
242
+ * of the master switch + the provider derivation). Only when this is true is the
243
+ * `memory_consolidate` tool registered: consolidation is OPT-IN +
244
+ * provider-explicit (blueprint D5/D6/DS-6 / §9), NEVER a silent paid call — a
245
+ * `model:` block set for summarize/title/draft does NOT flip this when the user
246
+ * left `memory.consolidation.enabled` false. The engine's `consolidate`
247
+ * self-refuses (`no-provider`/`model-unavailable`) when this flag is false, so
248
+ * the flag + the engine agree by construction.
249
+ */
250
+ memoryConsolidation?: boolean;
251
+ }
252
+ /** JSON returned by the `store_status` tool. */
253
+ interface StoreStatus {
254
+ ok: boolean;
255
+ projectId: string;
256
+ docCount: number;
257
+ vecCount: number;
258
+ dbPath: string | null;
259
+ degraded: boolean;
260
+ }
261
+ /** JSON returned by the `workflow_status` / `checkpoint` tools. */
262
+ interface WorkflowStatus {
263
+ ok: boolean;
264
+ taskId: string;
265
+ phase: Phase;
266
+ state: WorkflowState;
267
+ /** Next gate-phase ahead of the current phase (null past verify, or blocked). */
268
+ nextGate: Phase | null;
269
+ mode: Mode;
270
+ /** In-process view of the observable gate audit (Noir §9.1). */
271
+ history: GateResult[];
272
+ updatedAt: number;
273
+ /** Mirrors `store_status`: true when the store is a read-only fallback. */
274
+ degraded: boolean;
275
+ }
276
+ declare function createNoirServer(ctx: ServerContext): McpServer;
277
+
278
+ declare function startStdioServer(ctx: ServerContext): Promise<void>;
279
+
280
+ /**
281
+ * A store handle opened by the daemon for its own lifecycle.
282
+ *
283
+ * - `store` — the single writer/reader handle (the daemon is the only writer).
284
+ * - `dbPath` — absolute path to `<root>/.noir/store/<projectId>.db`; reported by
285
+ * `store_status`.
286
+ * - `degraded` — `true` when the writable open failed and we fell back to a
287
+ * read-only handle. In degraded mode writes throw
288
+ * `"store is read-only (daemon down)"`, but reads (FTS, kNN, counts) keep
289
+ * working, so the MCP surface still reports accurate state.
290
+ */
291
+ interface DaemonStore {
292
+ store: Store;
293
+ dbPath: string;
294
+ degraded: boolean;
295
+ }
296
+ /**
297
+ * Open the project's store for the daemon — the single writer.
298
+ *
299
+ * Tries a read-write open first (creating the DB + migrating schema if
300
+ * needed). On any failure (DB locked by another writer, permissions, transient
301
+ * FS error) it falls back to a read-only open so the daemon still has an
302
+ * accurate read handle — the FS-fallback / degraded story. The caller threads
303
+ * `degraded` into `ServerContext` so `store_status` can surface it.
304
+ */
305
+ declare function openStoreForDaemon(projectId: ProjectId, root: string): Promise<DaemonStore>;
306
+
307
+ /**
308
+ * Build the daemon's {@link WorkflowEngine} from its already-open store handle
309
+ * + the project `root` + `projectId`.
310
+ *
311
+ * One engine per serve lifecycle — constructed once alongside the store (see
312
+ * {@link openStoreForDaemon}) and reused across every HTTP request, exactly as
313
+ * the store handle is. The engine is a thin orchestrator over the store KV, so
314
+ * it inherits the daemon's single-writer discipline for free.
315
+ *
316
+ * Degraded story: the engine itself is mode-agnostic; when the underlying store
317
+ * was opened read-only (`storeDegraded === true`), `workflow_status` still works
318
+ * (a pure KV read via {@link Store.getState}) while `checkpoint { action:'save' }`
319
+ * throws `"store is read-only (daemon down)"` — the tool handler catches that and
320
+ * surfaces a clear JSON error instead of crashing the daemon.
321
+ */
322
+ declare function buildWorkflowEngine(store: Store, root: string, projectId: ProjectId): WorkflowEngine;
323
+
324
+ export { type DaemonRecord, type DaemonStore, type EnsureResult, type HostStatus, type RunningDaemon, type ServerContext, type StartHttpOptions, type StatusContext, type StoreStatus, type Transport, type WorkflowStatus, buildContextEngine, buildMemoryEngine, buildStatus, buildWorkflowEngine, clearDaemonRecord, createNoirServer, daemonJsonPath, ensureDaemonRunning, noirHome, openStoreForDaemon, pidAlive, readDaemonRecord, resolveConsolidationCapability, resolveMemoryConsolidation, startHttpServer, startStdioServer, writeDaemonRecord };