@neat.is/core 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.cts CHANGED
@@ -80,5 +80,6 @@ declare class ProjectResolutionError extends Error {
80
80
  declare function resolveProjectForVerb(client: HttpClient, parsed: ParsedArgs): Promise<string | undefined>;
81
81
  declare function resolveDaemonUrl(project?: string): Promise<string>;
82
82
  declare function runQueryVerb(cmd: string, parsed: ParsedArgs): Promise<number>;
83
+ declare function runMonitorVerb(parsed: ParsedArgs): Promise<number>;
83
84
 
84
- export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveDaemonUrl, resolveProjectForVerb, runInit, runQueryVerb, runSkill, usage };
85
+ export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveDaemonUrl, resolveProjectForVerb, runInit, runMonitorVerb, runQueryVerb, runSkill, usage };
package/dist/cli.d.ts CHANGED
@@ -80,5 +80,6 @@ declare class ProjectResolutionError extends Error {
80
80
  declare function resolveProjectForVerb(client: HttpClient, parsed: ParsedArgs): Promise<string | undefined>;
81
81
  declare function resolveDaemonUrl(project?: string): Promise<string>;
82
82
  declare function runQueryVerb(cmd: string, parsed: ParsedArgs): Promise<number>;
83
+ declare function runMonitorVerb(parsed: ParsedArgs): Promise<number>;
83
84
 
84
- export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveDaemonUrl, resolveProjectForVerb, runInit, runQueryVerb, runSkill, usage };
85
+ export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveDaemonUrl, resolveProjectForVerb, runInit, runMonitorVerb, runQueryVerb, runSkill, usage };
package/dist/cli.js CHANGED
@@ -3590,8 +3590,8 @@ async function healthIsForProject(restPort, project) {
3590
3590
  }
3591
3591
  return false;
3592
3592
  }
3593
- function daemonLogPath(projectPath2) {
3594
- return path8.join(projectPath2, "neat-out", "daemon.log");
3593
+ function daemonLogPath(projectPath3) {
3594
+ return path8.join(projectPath3, "neat-out", "daemon.log");
3595
3595
  }
3596
3596
  function spawnDaemonDetached(spec) {
3597
3597
  const here = path8.dirname(fileURLToPath2(import.meta.url));
@@ -4455,8 +4455,8 @@ async function runHooksCommand(args) {
4455
4455
  }
4456
4456
  }
4457
4457
 
4458
- // src/cli-verbs.ts
4459
- import path10 from "path";
4458
+ // src/monitor.ts
4459
+ import { EdgeType, parseEdgeId, Provenance as Provenance3 } from "@neat.is/types";
4460
4460
 
4461
4461
  // src/cli-client.ts
4462
4462
  import { Provenance as Provenance2 } from "@neat.is/types";
@@ -4988,7 +4988,310 @@ async function pushSnapshotToRemote(input) {
4988
4988
  );
4989
4989
  }
4990
4990
 
4991
+ // src/monitor.ts
4992
+ var OBSERVED_DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
4993
+ EdgeType.CALLS,
4994
+ EdgeType.CONNECTS_TO,
4995
+ EdgeType.PUBLISHES_TO,
4996
+ EdgeType.CONSUMES_FROM
4997
+ ]);
4998
+ function divergenceKey(d) {
4999
+ const column = "column" in d && d.column ? d.column : "";
5000
+ const edgeType = "edgeType" in d && d.edgeType ? d.edgeType : "";
5001
+ let extra = "";
5002
+ switch (d.type) {
5003
+ case "version-mismatch":
5004
+ extra = `${d.extractedVersion}->${d.observedVersion}`;
5005
+ break;
5006
+ case "host-mismatch":
5007
+ extra = `${d.extractedHost}->${d.observedHost}`;
5008
+ break;
5009
+ case "compat-violation":
5010
+ extra = `${d.rule.kind}:${d.rule.package ?? ""}`;
5011
+ break;
5012
+ default:
5013
+ extra = "";
5014
+ }
5015
+ return `div|${d.type}|${d.source}|${d.target}|${edgeType}|${column}|${extra}`;
5016
+ }
5017
+ function tableLabel(d) {
5018
+ return "table" in d && d.table ? d.table : d.source;
5019
+ }
5020
+ function formatDivergenceLine2(d) {
5021
+ switch (d.type) {
5022
+ case "missing-observed":
5023
+ if (d.column) {
5024
+ return `\u26A0 divergence [missing-observed] ${tableLabel(d)}.${d.column} declared, never observed in production`;
5025
+ }
5026
+ return `\u26A0 divergence [missing-observed] ${d.source} \u2192 ${d.target} declared, never observed in production`;
5027
+ case "missing-extracted":
5028
+ if (d.column) {
5029
+ return `\u26A0 divergence [missing-extracted] production writes ${tableLabel(d)}.${d.column} \u2014 not declared in code`;
5030
+ }
5031
+ return `\u26A0 divergence [missing-extracted] production ${d.source} \u2192 ${d.target} \u2014 not declared in code`;
5032
+ case "version-mismatch":
5033
+ return `\u26A0 divergence [version-mismatch] ${d.source} \u2192 ${d.target} declared ${d.extractedVersion}, observed ${d.observedVersion} (${d.compatibility})`;
5034
+ case "host-mismatch":
5035
+ return `\u26A0 divergence [host-mismatch] ${d.source} \u2192 ${d.target} declared host ${d.extractedHost}, observed host ${d.observedHost}`;
5036
+ case "compat-violation":
5037
+ return `\u26A0 divergence [compat-violation] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
5038
+ }
5039
+ }
5040
+ function formatStaleLine(edgeId) {
5041
+ const parsed = parseEdgeId(edgeId);
5042
+ if (parsed) {
5043
+ return `\u22EF stale ${parsed.source} \u2192 ${parsed.target} (observed edge went quiet)`;
5044
+ }
5045
+ return `\u22EF stale ${edgeId} (observed edge went quiet)`;
5046
+ }
5047
+ function formatObservedEdgeLine(edge) {
5048
+ return `+ observed ${edge.source} \u2192 ${edge.target} (new runtime dependency)`;
5049
+ }
5050
+ function divergenceJson(d) {
5051
+ return JSON.stringify({ kind: "divergence", ...d });
5052
+ }
5053
+ function staleJson(edgeId) {
5054
+ const parsed = parseEdgeId(edgeId);
5055
+ return JSON.stringify({
5056
+ kind: "stale",
5057
+ edgeId,
5058
+ ...parsed ? { source: parsed.source, target: parsed.target, edgeType: parsed.type } : {}
5059
+ });
5060
+ }
5061
+ function observedEdgeJson(edge) {
5062
+ return JSON.stringify({
5063
+ kind: "observed",
5064
+ id: edge.id,
5065
+ source: edge.source,
5066
+ target: edge.target,
5067
+ edgeType: edge.type,
5068
+ provenance: edge.provenance
5069
+ });
5070
+ }
5071
+ var MonitorEmitter = class {
5072
+ constructor(opts) {
5073
+ this.opts = opts;
5074
+ }
5075
+ opts;
5076
+ seen = /* @__PURE__ */ new Set();
5077
+ out(line) {
5078
+ this.opts.write(line + "\n");
5079
+ }
5080
+ // Emit every not-yet-seen divergence in a fresh result. Returns the count
5081
+ // newly emitted (0 → nothing printed, the silent path). Idempotent across
5082
+ // re-reads: the seen-set keys off divergenceKey.
5083
+ emitDivergences(result) {
5084
+ let emitted = 0;
5085
+ for (const d of result.divergences) {
5086
+ const key = divergenceKey(d);
5087
+ if (this.seen.has(key)) continue;
5088
+ this.seen.add(key);
5089
+ this.out(this.opts.json ? divergenceJson(d) : formatDivergenceLine2(d));
5090
+ emitted++;
5091
+ }
5092
+ return emitted;
5093
+ }
5094
+ // Emit a stale-transition once, keyed on the edge id.
5095
+ emitStale(edgeId) {
5096
+ const key = `stale|${edgeId}`;
5097
+ if (this.seen.has(key)) return false;
5098
+ this.seen.add(key);
5099
+ this.out(this.opts.json ? staleJson(edgeId) : formatStaleLine(edgeId));
5100
+ return true;
5101
+ }
5102
+ // Emit a new OBSERVED runtime dependency once, keyed on the edge id. Silently
5103
+ // ignores non-OBSERVED edges and non-dependency edge types (structural
5104
+ // ownership), so only real runtime dependencies reach stdout.
5105
+ emitObservedEdge(edge) {
5106
+ if (edge.provenance !== Provenance3.OBSERVED) return false;
5107
+ if (!OBSERVED_DEP_EDGE_TYPES.has(edge.type)) return false;
5108
+ const key = `edge|${edge.id}`;
5109
+ if (this.seen.has(key)) return false;
5110
+ this.seen.add(key);
5111
+ this.out(this.opts.json ? observedEdgeJson(edge) : formatObservedEdgeLine(edge));
5112
+ return true;
5113
+ }
5114
+ };
5115
+ function parseFrame(raw) {
5116
+ let event = "message";
5117
+ const dataLines = [];
5118
+ for (const line of raw.split("\n")) {
5119
+ if (line.length === 0 || line.startsWith(":")) continue;
5120
+ if (line.startsWith("event:")) {
5121
+ event = line.slice("event:".length).trim();
5122
+ } else if (line.startsWith("data:")) {
5123
+ dataLines.push(line.slice("data:".length).replace(/^ /, ""));
5124
+ }
5125
+ }
5126
+ if (dataLines.length === 0) return null;
5127
+ return { event, data: dataLines.join("\n") };
5128
+ }
5129
+ async function drainSse(body, onFrame) {
5130
+ const reader = body.getReader();
5131
+ const decoder = new TextDecoder();
5132
+ let buf = "";
5133
+ try {
5134
+ for (; ; ) {
5135
+ const { done, value } = await reader.read();
5136
+ if (done) break;
5137
+ buf += decoder.decode(value, { stream: true });
5138
+ buf = buf.replace(/\r\n/g, "\n");
5139
+ let idx;
5140
+ while ((idx = buf.indexOf("\n\n")) !== -1) {
5141
+ const rawFrame = buf.slice(0, idx);
5142
+ buf = buf.slice(idx + 2);
5143
+ const frame = parseFrame(rawFrame);
5144
+ if (frame) onFrame(frame);
5145
+ }
5146
+ }
5147
+ } finally {
5148
+ try {
5149
+ reader.releaseLock();
5150
+ } catch {
5151
+ }
5152
+ }
5153
+ }
5154
+ function safeParse(data) {
5155
+ try {
5156
+ return JSON.parse(data);
5157
+ } catch {
5158
+ return null;
5159
+ }
5160
+ }
5161
+ function projectPath2(project, suffix) {
5162
+ if (!project) return suffix;
5163
+ return `/projects/${encodeURIComponent(project)}${suffix}`;
5164
+ }
5165
+ function backoffDelay(attempt, capMs) {
5166
+ return Math.min(capMs, 500 * 2 ** Math.min(attempt, 6));
5167
+ }
5168
+ async function runMonitor(opts) {
5169
+ const write = opts.write ?? ((line) => process.stdout.write(line));
5170
+ const debounceMs = opts.debounceMs ?? 400;
5171
+ const backoffCapMs = opts.backoffCapMs ?? 1e4;
5172
+ const maxReconnects = opts.maxReconnects ?? Number.POSITIVE_INFINITY;
5173
+ const client = createHttpClient(opts.baseUrl, opts.authToken);
5174
+ const emitter = new MonitorEmitter({ json: opts.json, write });
5175
+ const divergencesPath = projectPath2(opts.project, "/graph/divergences");
5176
+ const eventsUrl = `${opts.baseUrl.replace(/\/$/, "")}${projectPath2(opts.project, "/events")}`;
5177
+ let readTimer = null;
5178
+ let reading = false;
5179
+ let readPending = false;
5180
+ const doRead = async () => {
5181
+ if (reading) {
5182
+ readPending = true;
5183
+ return;
5184
+ }
5185
+ reading = true;
5186
+ try {
5187
+ const result = await client.get(divergencesPath);
5188
+ emitter.emitDivergences(result);
5189
+ } catch {
5190
+ } finally {
5191
+ reading = false;
5192
+ if (readPending) {
5193
+ readPending = false;
5194
+ scheduleRead();
5195
+ }
5196
+ }
5197
+ };
5198
+ const scheduleRead = () => {
5199
+ if (readTimer) clearTimeout(readTimer);
5200
+ readTimer = setTimeout(() => {
5201
+ readTimer = null;
5202
+ void doRead();
5203
+ }, debounceMs);
5204
+ if (typeof readTimer.unref === "function") readTimer.unref();
5205
+ };
5206
+ const onFrame = (frame) => {
5207
+ switch (frame.event) {
5208
+ case "extraction-complete":
5209
+ scheduleRead();
5210
+ break;
5211
+ case "stale-transition": {
5212
+ const payload = safeParse(frame.data);
5213
+ const edgeId = payload && typeof payload.edgeId === "string" ? payload.edgeId : void 0;
5214
+ if (edgeId) emitter.emitStale(edgeId);
5215
+ scheduleRead();
5216
+ break;
5217
+ }
5218
+ case "edge-added": {
5219
+ const payload = safeParse(frame.data);
5220
+ const edge = payload?.edge;
5221
+ if (edge && edge.provenance === Provenance3.OBSERVED) {
5222
+ emitter.emitObservedEdge(edge);
5223
+ scheduleRead();
5224
+ }
5225
+ break;
5226
+ }
5227
+ default:
5228
+ break;
5229
+ }
5230
+ };
5231
+ const headers = { accept: "text/event-stream" };
5232
+ if (opts.authToken && opts.authToken.length > 0) {
5233
+ headers.authorization = `Bearer ${opts.authToken}`;
5234
+ }
5235
+ let connectedOnce = false;
5236
+ let firstConnect = true;
5237
+ for (let attempt = 0; ; attempt++) {
5238
+ if (opts.signal?.aborted) break;
5239
+ let res;
5240
+ try {
5241
+ res = await fetch(eventsUrl, { headers, signal: opts.signal });
5242
+ } catch (err) {
5243
+ if (err.name === "AbortError") break;
5244
+ if (!connectedOnce || attempt >= maxReconnects) break;
5245
+ await sleep(backoffDelay(attempt, backoffCapMs), opts.signal);
5246
+ continue;
5247
+ }
5248
+ if (!res.ok || !res.body) {
5249
+ await res.body?.cancel().catch(() => {
5250
+ });
5251
+ if (!connectedOnce || attempt >= maxReconnects) break;
5252
+ await sleep(backoffDelay(attempt, backoffCapMs), opts.signal);
5253
+ continue;
5254
+ }
5255
+ connectedOnce = true;
5256
+ attempt = 0;
5257
+ if (firstConnect) {
5258
+ firstConnect = false;
5259
+ await doRead();
5260
+ } else {
5261
+ scheduleRead();
5262
+ }
5263
+ try {
5264
+ await drainSse(res.body, onFrame);
5265
+ } catch {
5266
+ }
5267
+ if (opts.signal?.aborted) break;
5268
+ if (attempt >= maxReconnects) break;
5269
+ await sleep(backoffDelay(attempt, backoffCapMs), opts.signal);
5270
+ }
5271
+ if (readTimer) clearTimeout(readTimer);
5272
+ return 0;
5273
+ }
5274
+ function sleep(ms, signal) {
5275
+ return new Promise((resolve) => {
5276
+ if (signal?.aborted) {
5277
+ resolve();
5278
+ return;
5279
+ }
5280
+ const timer = setTimeout(() => {
5281
+ signal?.removeEventListener("abort", onAbort);
5282
+ resolve();
5283
+ }, ms);
5284
+ if (typeof timer.unref === "function") timer.unref();
5285
+ const onAbort = () => {
5286
+ clearTimeout(timer);
5287
+ resolve();
5288
+ };
5289
+ signal?.addEventListener("abort", onAbort, { once: true });
5290
+ });
5291
+ }
5292
+
4991
5293
  // src/cli-verbs.ts
5294
+ import path10 from "path";
4992
5295
  async function resolveProjectEntry(opts) {
4993
5296
  const entries = await listProjects();
4994
5297
  if (opts.project) {
@@ -5182,6 +5485,14 @@ function usage2() {
5182
5485
  console.log(" watch <path> Start neat-core, watch <path>, re-extract on changes.");
5183
5486
  console.log(" PORT (default 8080), OTEL_PORT (4318), HOST (0.0.0.0)");
5184
5487
  console.log(" control listeners. NEAT_OTLP_GRPC=true also opens 4317.");
5488
+ console.log(" monitor Stream live graph facts to stdout \u2014 one line per new");
5489
+ console.log(" fact \u2014 as the daemon learns them: fresh divergences,");
5490
+ console.log(" integrations that just went stale, and new observed");
5491
+ console.log(" runtime dependencies. Silent when nothing is new; exits");
5492
+ console.log(" clean with no output when no daemon is reachable.");
5493
+ console.log(" Flags:");
5494
+ console.log(" --project <name> watch a registered project by name");
5495
+ console.log(" --json emit one JSON object per line");
5185
5496
  console.log(" list Report the daemons running on this machine (alias: ps).");
5186
5497
  console.log(" Reads ~/.neat/daemons/ and folds in any registered");
5187
5498
  console.log(" project no daemon has self-described yet.");
@@ -5841,14 +6152,14 @@ async function main() {
5841
6152
  console.error(`neat uninstall: no project named "${name}"`);
5842
6153
  process.exit(1);
5843
6154
  }
5844
- const projectPath2 = daemon?.record.projectPath ?? removed?.path ?? "(unknown path)";
6155
+ const projectPath3 = daemon?.record.projectPath ?? removed?.path ?? "(unknown path)";
5845
6156
  if (daemon) {
5846
6157
  if (daemon.live && signalDaemonStop(daemon.record.pid)) {
5847
6158
  console.log(`uninstall: ${name} \u2014 stopped daemon pid ${daemon.record.pid}`);
5848
6159
  }
5849
6160
  await removeDaemonRecord(daemon.source);
5850
6161
  }
5851
- console.log(`unregistered: ${name} (${projectPath2})`);
6162
+ console.log(`unregistered: ${name} (${projectPath3})`);
5852
6163
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
5853
6164
  return;
5854
6165
  }
@@ -5903,6 +6214,11 @@ async function main() {
5903
6214
  if (result.exitCode !== 0) process.exit(result.exitCode);
5904
6215
  return;
5905
6216
  }
6217
+ if (cmd === "monitor") {
6218
+ const code = await runMonitorVerb(parsed);
6219
+ if (code !== 0) process.exit(code);
6220
+ return;
6221
+ }
5906
6222
  if (QUERY_VERBS.has(cmd)) {
5907
6223
  const code = await runQueryVerb(cmd, parsed);
5908
6224
  if (code !== 0) process.exit(code);
@@ -6156,6 +6472,37 @@ async function runQueryVerb(cmd, parsed) {
6156
6472
  return exitCodeForError(err);
6157
6473
  }
6158
6474
  }
6475
+ async function runMonitorVerb(parsed) {
6476
+ const requestedProject = resolveProjectFlag(parsed);
6477
+ const baseUrl = await resolveDaemonUrl(requestedProject);
6478
+ const token = resolveAuthToken();
6479
+ const client = createHttpClient(baseUrl, token);
6480
+ let project;
6481
+ try {
6482
+ project = await resolveProjectForVerb(client, parsed);
6483
+ } catch (err) {
6484
+ if (err instanceof ProjectResolutionError) {
6485
+ console.error(`neat monitor: ${err.message}`);
6486
+ }
6487
+ return 0;
6488
+ }
6489
+ const controller = new AbortController();
6490
+ const onSignal = () => controller.abort();
6491
+ process.once("SIGINT", onSignal);
6492
+ process.once("SIGTERM", onSignal);
6493
+ try {
6494
+ return await runMonitor({
6495
+ baseUrl,
6496
+ project,
6497
+ json: parsed.json,
6498
+ authToken: token,
6499
+ signal: controller.signal
6500
+ });
6501
+ } finally {
6502
+ process.off("SIGINT", onSignal);
6503
+ process.off("SIGTERM", onSignal);
6504
+ }
6505
+ }
6159
6506
  var entry = process.argv[1] ?? "";
6160
6507
  if (/[\\/](?:cli\.(?:cjs|js)|cli|neat|neat\.is)$/.test(entry)) {
6161
6508
  main().catch((err) => {
@@ -6176,6 +6523,7 @@ export {
6176
6523
  resolveDaemonUrl,
6177
6524
  resolveProjectForVerb,
6178
6525
  runInit,
6526
+ runMonitorVerb,
6179
6527
  runQueryVerb,
6180
6528
  runSkill,
6181
6529
  usage2 as usage