@neat.is/core 0.5.1 → 0.5.3

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.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveNeatVersion,
10
10
  validateConnectorEntry,
11
11
  writeDaemonRecord
12
- } from "./chunk-DPEPI2N6.js";
12
+ } from "./chunk-X2AMX3QZ.js";
13
13
  import {
14
14
  buildSearchIndex
15
15
  } from "./chunk-BIY46Q6U.js";
@@ -68,7 +68,7 @@ import {
68
68
  startPersistLoop,
69
69
  startStalenessLoop,
70
70
  upsertConnectorEntry
71
- } from "./chunk-BI3XKGVG.js";
71
+ } from "./chunk-GJHEZC5K.js";
72
72
  import {
73
73
  startOtelGrpcReceiver
74
74
  } from "./chunk-I72HTUOG.js";
@@ -82,8 +82,8 @@ import {
82
82
  } from "./chunk-CFDPIMRP.js";
83
83
 
84
84
  // src/cli.ts
85
- import path9 from "path";
86
- import { promises as fs7 } from "fs";
85
+ import path10 from "path";
86
+ import { promises as fs8 } from "fs";
87
87
 
88
88
  // src/banner.ts
89
89
  import path from "path";
@@ -810,7 +810,7 @@ import semver from "semver";
810
810
 
811
811
  // src/installers/templates.ts
812
812
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
813
- var OTEL_INIT_STAMP = "// neat-template-version: 6 \u2014 daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
813
+ var OTEL_INIT_STAMP = "// neat-template-version: 7 \u2014 OTel init degrades to no-OBSERVED instead of crashing the host app when @opentelemetry deps are absent (#820); daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
814
814
  var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
815
815
  var OTEL_OTLP_PROTOCOL_JS = "process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||= 'http/json'";
816
816
  var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
@@ -1146,6 +1146,14 @@ ${OTEL_ENDPOINT_RESOLVER_CJS}
1146
1146
  ${OTEL_OTLP_PROTOCOL_JS}
1147
1147
  ${OTEL_OTLP_HEADERS_JS}
1148
1148
 
1149
+ // Instrumentation is ambient \u2014 it must never break the host app. If the
1150
+ // @opentelemetry packages aren't installed (a package-manager install that
1151
+ // failed or hasn't run yet), degrade to running WITHOUT OBSERVED rather than
1152
+ // crashing the process on a missing module (#820). The require-based flavor can
1153
+ // guard this at runtime; do so around the whole SDK setup, not just the
1154
+ // requires, so a broken instrumentation registration can't take the app down
1155
+ // either.
1156
+ try {
1149
1157
  const { NodeSDK } = require('@opentelemetry/sdk-node')
1150
1158
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
1151
1159
  const { trace, context } = require('@opentelemetry/api')
@@ -1157,6 +1165,14 @@ __INSTRUMENTATION_BLOCK__
1157
1165
  const sdk = new NodeSDK({ instrumentations })
1158
1166
  sdk.start()
1159
1167
  ${neatWireCaptureSource(false)}
1168
+ } catch (__neatOtelErr) {
1169
+ const __neatMsg = String((__neatOtelErr && __neatOtelErr.message) || __neatOtelErr)
1170
+ if (/Cannot find module|MODULE_NOT_FOUND/.test(__neatMsg)) {
1171
+ console.warn('[neat] OpenTelemetry is not active: its packages are not installed, so this app is running without OBSERVED tracing. Run your package manager install and restart to enable it.')
1172
+ } else {
1173
+ console.warn('[neat] OpenTelemetry failed to start; the app is running without OBSERVED tracing: ' + __neatMsg)
1174
+ }
1175
+ }
1160
1176
  `;
1161
1177
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
1162
1178
  ${OTEL_INIT_STAMP}
@@ -3836,8 +3852,181 @@ async function runConnectorCommand(rawArgs, deps = {}) {
3836
3852
  }
3837
3853
  }
3838
3854
 
3839
- // src/cli-verbs.ts
3855
+ // src/hooks-cli.ts
3840
3856
  import path8 from "path";
3857
+ import os from "os";
3858
+ import { promises as fs7 } from "fs";
3859
+ import { fileURLToPath as fileURLToPath3 } from "url";
3860
+ var HOOK_FILENAME = "neat-search-nudge.mjs";
3861
+ var GUIDE_FILENAME = "GRAPH_FIRST.md";
3862
+ var GUIDE_INSTALL_NAME = "neat-graph-first.md";
3863
+ var HOOK_MATCHER = "Grep|Glob|Bash";
3864
+ function moduleDir() {
3865
+ return typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath3(import.meta.url));
3866
+ }
3867
+ async function readSkillAsset(rel) {
3868
+ const here = moduleDir();
3869
+ const candidates = [
3870
+ path8.resolve(here, "../../claude-skill", rel),
3871
+ path8.resolve(here, "../../../claude-skill", rel),
3872
+ path8.resolve(here, "../claude-skill", rel)
3873
+ ];
3874
+ for (const candidate of candidates) {
3875
+ try {
3876
+ return await fs7.readFile(candidate, "utf8");
3877
+ } catch {
3878
+ }
3879
+ }
3880
+ throw new Error(
3881
+ `neat hooks: could not find @neat.is/claude-skill/${rel} \u2014 is the package installed?`
3882
+ );
3883
+ }
3884
+ function neatHome() {
3885
+ const override = process.env.NEAT_HOME;
3886
+ if (override && override.length > 0) return path8.resolve(override);
3887
+ return path8.join(os.homedir(), ".neat");
3888
+ }
3889
+ function claudeSettingsPath() {
3890
+ const override = process.env.NEAT_CLAUDE_SETTINGS;
3891
+ if (override && override.length > 0) return path8.resolve(override);
3892
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
3893
+ return path8.join(home, ".claude", "settings.json");
3894
+ }
3895
+ function installedHookPath() {
3896
+ return path8.join(neatHome(), "hooks", HOOK_FILENAME);
3897
+ }
3898
+ function isNeatSearchEntry(entry2) {
3899
+ return (entry2.hooks ?? []).some(
3900
+ (h) => typeof h.command === "string" && h.command.includes(HOOK_FILENAME)
3901
+ );
3902
+ }
3903
+ function neatHookEntry(command) {
3904
+ return { matcher: HOOK_MATCHER, hooks: [{ type: "command", command }] };
3905
+ }
3906
+ function hookCommand(scriptPath) {
3907
+ return `node "${scriptPath}"`;
3908
+ }
3909
+ async function runHooks(opts) {
3910
+ if (opts.printHook) {
3911
+ process.stdout.write(await readSkillAsset(`hooks/${HOOK_FILENAME}`));
3912
+ return { exitCode: 0 };
3913
+ }
3914
+ if (opts.printGuide) {
3915
+ process.stdout.write(await readSkillAsset(GUIDE_FILENAME));
3916
+ return { exitCode: 0 };
3917
+ }
3918
+ if (opts.printSettings) {
3919
+ const block = {
3920
+ hooks: { PreToolUse: [neatHookEntry(hookCommand(installedHookPath()))] }
3921
+ };
3922
+ process.stdout.write(JSON.stringify(block, null, 2) + "\n");
3923
+ return { exitCode: 0 };
3924
+ }
3925
+ if (opts.apply) {
3926
+ const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
3927
+ const guide = await readSkillAsset(GUIDE_FILENAME);
3928
+ const scriptPath = installedHookPath();
3929
+ await fs7.mkdir(path8.dirname(scriptPath), { recursive: true });
3930
+ await fs7.writeFile(scriptPath, hookScript, { mode: 493 });
3931
+ const guidePath = path8.join(neatHome(), GUIDE_INSTALL_NAME);
3932
+ await fs7.writeFile(guidePath, guide, "utf8");
3933
+ const settingsFile = claudeSettingsPath();
3934
+ let settings = {};
3935
+ try {
3936
+ settings = JSON.parse(await fs7.readFile(settingsFile, "utf8"));
3937
+ } catch (err) {
3938
+ if (err.code !== "ENOENT") {
3939
+ console.error(
3940
+ `neat hooks: failed to read ${settingsFile} \u2014 ${err.message}`
3941
+ );
3942
+ return { exitCode: 1 };
3943
+ }
3944
+ }
3945
+ const hooks = settings.hooks ?? {};
3946
+ const preToolUse = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
3947
+ const command = hookCommand(scriptPath);
3948
+ const existingIdx = preToolUse.findIndex(isNeatSearchEntry);
3949
+ if (existingIdx >= 0) {
3950
+ preToolUse[existingIdx] = neatHookEntry(command);
3951
+ } else {
3952
+ preToolUse.push(neatHookEntry(command));
3953
+ }
3954
+ const merged = {
3955
+ ...settings,
3956
+ hooks: { ...hooks, PreToolUse: preToolUse }
3957
+ };
3958
+ await fs7.mkdir(path8.dirname(settingsFile), { recursive: true });
3959
+ await fs7.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
3960
+ console.log(`neat hooks: installed the search-nudge hook`);
3961
+ console.log(` script: ${scriptPath}`);
3962
+ console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
3963
+ console.log(` guidance: ${guidePath}`);
3964
+ console.log("");
3965
+ console.log("restart Claude Code to load the hook. On a Grep/Glob or a Bash grep,");
3966
+ console.log("your agent will now be nudged to query NEAT first.");
3967
+ console.log("");
3968
+ console.log("The hook is Claude-Code-specific. For agents on other harnesses, paste");
3969
+ console.log(`the guidance above into your project instructions (CLAUDE.md / AGENTS.md).`);
3970
+ return { exitCode: 0 };
3971
+ }
3972
+ usage();
3973
+ return { exitCode: 0 };
3974
+ }
3975
+ function usage() {
3976
+ console.log("neat hooks \u2014 wire NEAT into your agent so it queries the graph before grepping");
3977
+ console.log("");
3978
+ console.log(" --apply install the Claude Code search-nudge hook and write the");
3979
+ console.log(" graph-first guidance to ~/.neat/, merging into");
3980
+ console.log(" ~/.claude/settings.json without touching your other hooks");
3981
+ console.log(" --print-hook print the hook script to stdout");
3982
+ console.log(" --print-guide print the agent-agnostic graph-first guidance to stdout");
3983
+ console.log(" --print-settings print the settings.json PreToolUse block --apply would add");
3984
+ console.log("");
3985
+ console.log("The hook is a gentle, non-blocking nudge \u2014 searches still run. It is");
3986
+ console.log("Claude-Code-specific; other harnesses get the same steer from the guidance.");
3987
+ }
3988
+ async function runHooksCommand(args) {
3989
+ const opts = {
3990
+ apply: false,
3991
+ printHook: false,
3992
+ printGuide: false,
3993
+ printSettings: false
3994
+ };
3995
+ for (const arg of args) {
3996
+ switch (arg) {
3997
+ case "--apply":
3998
+ opts.apply = true;
3999
+ break;
4000
+ case "--print-hook":
4001
+ opts.printHook = true;
4002
+ break;
4003
+ case "--print-guide":
4004
+ opts.printGuide = true;
4005
+ break;
4006
+ case "--print-settings":
4007
+ opts.printSettings = true;
4008
+ break;
4009
+ case "-h":
4010
+ case "--help":
4011
+ usage();
4012
+ return 0;
4013
+ default:
4014
+ console.error(`neat hooks: unknown flag "${arg}"`);
4015
+ usage();
4016
+ return 2;
4017
+ }
4018
+ }
4019
+ try {
4020
+ const { exitCode } = await runHooks(opts);
4021
+ return exitCode;
4022
+ } catch (err) {
4023
+ console.error(err.message);
4024
+ return 1;
4025
+ }
4026
+ }
4027
+
4028
+ // src/cli-verbs.ts
4029
+ import path9 from "path";
3841
4030
 
3842
4031
  // src/cli-client.ts
3843
4032
  import { Provenance as Provenance2 } from "@neat.is/types";
@@ -3865,10 +4054,10 @@ function createHttpClient(baseUrl, bearerToken) {
3865
4054
  const root = baseUrl.replace(/\/$/, "");
3866
4055
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
3867
4056
  return {
3868
- async get(path10) {
4057
+ async get(path11) {
3869
4058
  let res;
3870
4059
  try {
3871
- res = await fetch(`${root}${path10}`, {
4060
+ res = await fetch(`${root}${path11}`, {
3872
4061
  headers: { ...authHeader }
3873
4062
  });
3874
4063
  } catch (err) {
@@ -3880,16 +4069,16 @@ function createHttpClient(baseUrl, bearerToken) {
3880
4069
  const body = await res.text().catch(() => "");
3881
4070
  throw new HttpError(
3882
4071
  res.status,
3883
- `${res.status} ${res.statusText} on GET ${path10}: ${body}`,
4072
+ `${res.status} ${res.statusText} on GET ${path11}: ${body}`,
3884
4073
  body
3885
4074
  );
3886
4075
  }
3887
4076
  return await res.json();
3888
4077
  },
3889
- async post(path10, body) {
4078
+ async post(path11, body) {
3890
4079
  let res;
3891
4080
  try {
3892
- res = await fetch(`${root}${path10}`, {
4081
+ res = await fetch(`${root}${path11}`, {
3893
4082
  method: "POST",
3894
4083
  headers: { "content-type": "application/json", ...authHeader },
3895
4084
  body: JSON.stringify(body)
@@ -3903,7 +4092,7 @@ function createHttpClient(baseUrl, bearerToken) {
3903
4092
  const text = await res.text().catch(() => "");
3904
4093
  throw new HttpError(
3905
4094
  res.status,
3906
- `${res.status} ${res.statusText} on POST ${path10}: ${text}`,
4095
+ `${res.status} ${res.statusText} on POST ${path11}: ${text}`,
3907
4096
  text
3908
4097
  );
3909
4098
  }
@@ -3917,12 +4106,12 @@ function projectPath(project, suffix) {
3917
4106
  }
3918
4107
  async function runRootCause(client, input) {
3919
4108
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
3920
- const path10 = projectPath(
4109
+ const path11 = projectPath(
3921
4110
  input.project,
3922
4111
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
3923
4112
  );
3924
4113
  try {
3925
- const result = await client.get(path10);
4114
+ const result = await client.get(path11);
3926
4115
  const arrowPath = result.traversalPath.join(" \u2190 ");
3927
4116
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
3928
4117
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -3948,12 +4137,12 @@ async function runRootCause(client, input) {
3948
4137
  }
3949
4138
  async function runBlastRadius(client, input) {
3950
4139
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
3951
- const path10 = projectPath(
4140
+ const path11 = projectPath(
3952
4141
  input.project,
3953
4142
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
3954
4143
  );
3955
4144
  try {
3956
- const result = await client.get(path10);
4145
+ const result = await client.get(path11);
3957
4146
  if (result.totalAffected === 0) {
3958
4147
  return {
3959
4148
  summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
@@ -3987,12 +4176,12 @@ function formatBlastEntry(n) {
3987
4176
  }
3988
4177
  async function runDependencies(client, input) {
3989
4178
  const depth = input.depth ?? 3;
3990
- const path10 = projectPath(
4179
+ const path11 = projectPath(
3991
4180
  input.project,
3992
4181
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
3993
4182
  );
3994
4183
  try {
3995
- const result = await client.get(path10);
4184
+ const result = await client.get(path11);
3996
4185
  if (result.total === 0) {
3997
4186
  return {
3998
4187
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -4084,9 +4273,9 @@ function formatDuration(ms) {
4084
4273
  return `${Math.round(h / 24)}d`;
4085
4274
  }
4086
4275
  async function runIncidents(client, input) {
4087
- const path10 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
4276
+ const path11 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
4088
4277
  try {
4089
- const body = await client.get(path10);
4278
+ const body = await client.get(path11);
4090
4279
  const events = body.events;
4091
4280
  if (events.length === 0) {
4092
4281
  return {
@@ -4376,7 +4565,7 @@ async function resolveProjectEntry(opts) {
4376
4565
  const cwd = opts.cwd ?? process.cwd();
4377
4566
  const resolvedCwd = await normalizeProjectPath(cwd);
4378
4567
  for (const entry2 of entries) {
4379
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path8.sep}`)) {
4568
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path9.sep}`)) {
4380
4569
  return entry2;
4381
4570
  }
4382
4571
  }
@@ -4541,7 +4730,7 @@ function isNpxInvocation() {
4541
4730
  function commandPrefix() {
4542
4731
  return isNpxInvocation() ? "npx neat.is" : "neat";
4543
4732
  }
4544
- function usage() {
4733
+ function usage2() {
4545
4734
  const neat = commandPrefix();
4546
4735
  console.log("Installed via npx? Prefix commands with `npx neat.is`, or install once: `npm i -g neat.is`.");
4547
4736
  console.log("");
@@ -4578,6 +4767,14 @@ function usage() {
4578
4767
  console.log(" Flags:");
4579
4768
  console.log(" --print-config print the JSON snippet to stdout");
4580
4769
  console.log(" --apply merge mcpServers.neat into ~/.claude.json");
4770
+ console.log(" hooks Wire NEAT into your agent so it queries the graph before");
4771
+ console.log(" grepping. Installs a gentle Claude Code search-nudge hook and");
4772
+ console.log(" writes agent-agnostic graph-first guidance for other harnesses.");
4773
+ console.log(" Flags:");
4774
+ console.log(" --apply install the hook + guidance");
4775
+ console.log(" --print-hook print the hook script");
4776
+ console.log(" --print-guide print the graph-first guidance");
4777
+ console.log(" --print-settings print the settings.json block --apply adds");
4581
4778
  console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
4582
4779
  console.log(" emit a docker-compose / systemd / docker run artifact, and");
4583
4780
  console.log(" print the OTel env-vars block to paste into your platform.");
@@ -4822,7 +5019,7 @@ async function buildPatchSections(services, project) {
4822
5019
  }
4823
5020
  async function runInit(opts) {
4824
5021
  const written = [];
4825
- const stat = await fs7.stat(opts.scanPath).catch(() => null);
5022
+ const stat = await fs8.stat(opts.scanPath).catch(() => null);
4826
5023
  if (!stat || !stat.isDirectory()) {
4827
5024
  console.error(`neat init: ${opts.scanPath} is not a directory`);
4828
5025
  return { exitCode: 2, writtenFiles: written };
@@ -4831,13 +5028,13 @@ async function runInit(opts) {
4831
5028
  printDiscoveryReport(opts, services);
4832
5029
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
4833
5030
  const patch = renderPatch(sections);
4834
- const patchPath = path9.join(opts.scanPath, "neat.patch");
5031
+ const patchPath = path10.join(opts.scanPath, "neat.patch");
4835
5032
  if (opts.dryRun) {
4836
- await fs7.writeFile(patchPath, patch, "utf8");
5033
+ await fs8.writeFile(patchPath, patch, "utf8");
4837
5034
  written.push(patchPath);
4838
5035
  console.log(`dry-run: patch written to ${patchPath}`);
4839
- const gitignorePath = path9.join(opts.scanPath, ".gitignore");
4840
- const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
5036
+ const gitignorePath = path10.join(opts.scanPath, ".gitignore");
5037
+ const gitignoreExists = await fs8.stat(gitignorePath).then(() => true).catch(() => false);
4841
5038
  const verb = gitignoreExists ? "append" : "create";
4842
5039
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
4843
5040
  console.log("rerun without --dry-run to register and snapshot.");
@@ -4848,9 +5045,9 @@ async function runInit(opts) {
4848
5045
  const graph = getGraph(graphKey);
4849
5046
  const projectPaths = pathsForProject(
4850
5047
  graphKey,
4851
- path9.join(opts.scanPath, "neat-out")
5048
+ path10.join(opts.scanPath, "neat-out")
4852
5049
  );
4853
- const errorsPath = path9.join(path9.dirname(opts.outPath), path9.basename(projectPaths.errorsPath));
5050
+ const errorsPath = path10.join(path10.dirname(opts.outPath), path10.basename(projectPaths.errorsPath));
4854
5051
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
4855
5052
  await saveGraphToDisk(graph, opts.outPath);
4856
5053
  written.push(opts.outPath);
@@ -4929,7 +5126,7 @@ async function runInit(opts) {
4929
5126
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
4930
5127
  }
4931
5128
  } else {
4932
- await fs7.writeFile(patchPath, patch, "utf8");
5129
+ await fs8.writeFile(patchPath, patch, "utf8");
4933
5130
  written.push(patchPath);
4934
5131
  }
4935
5132
  }
@@ -4969,9 +5166,9 @@ var CLAUDE_SKILL_CONFIG = {
4969
5166
  };
4970
5167
  function claudeConfigPath() {
4971
5168
  const override = process.env.NEAT_CLAUDE_CONFIG;
4972
- if (override && override.length > 0) return path9.resolve(override);
5169
+ if (override && override.length > 0) return path10.resolve(override);
4973
5170
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4974
- return path9.join(home, ".claude.json");
5171
+ return path10.join(home, ".claude.json");
4975
5172
  }
4976
5173
  async function runSkill(opts) {
4977
5174
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -4983,7 +5180,7 @@ async function runSkill(opts) {
4983
5180
  const target = claudeConfigPath();
4984
5181
  let existing = {};
4985
5182
  try {
4986
- existing = JSON.parse(await fs7.readFile(target, "utf8"));
5183
+ existing = JSON.parse(await fs8.readFile(target, "utf8"));
4987
5184
  } catch (err) {
4988
5185
  if (err.code !== "ENOENT") {
4989
5186
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -4995,10 +5192,13 @@ async function runSkill(opts) {
4995
5192
  ...existing,
4996
5193
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
4997
5194
  };
4998
- await fs7.mkdir(path9.dirname(target), { recursive: true });
4999
- await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
5195
+ await fs8.mkdir(path10.dirname(target), { recursive: true });
5196
+ await fs8.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
5000
5197
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
5001
5198
  console.log("restart Claude Code to pick up the new MCP server.");
5199
+ console.log("");
5200
+ console.log("Tip: run `neat hooks --apply` to also install the search-nudge hook, so");
5201
+ console.log("your agent reaches for the graph before it grep-scans the repo.");
5002
5202
  return { exitCode: 0 };
5003
5203
  }
5004
5204
  console.log("neat skill \u2014 Claude Code MCP drop-in for NEAT");
@@ -5011,13 +5211,16 @@ async function runSkill(opts) {
5011
5211
  console.log("");
5012
5212
  console.log("The MCP server reads NEAT_CORE_URL for the daemon URL \u2014 point it at a");
5013
5213
  console.log("non-default daemon by editing that value in the generated config.");
5214
+ console.log("");
5215
+ console.log("See also `neat hooks --apply` \u2014 the search-nudge hook + graph-first guidance");
5216
+ console.log("that steer an agent to query the graph before falling back to text search.");
5014
5217
  return { exitCode: 0 };
5015
5218
  }
5016
5219
  async function main() {
5017
5220
  const argv = process.argv.slice(2);
5018
5221
  const cmd0 = argv[0];
5019
5222
  if (cmd0 === "-h" || cmd0 === "--help") {
5020
- usage();
5223
+ usage2();
5021
5224
  process.exit(0);
5022
5225
  }
5023
5226
  if (cmd0 === "--version" || cmd0 === "-v" || cmd0 === "version") {
@@ -5029,6 +5232,11 @@ async function main() {
5029
5232
  if (code !== 0) process.exit(code);
5030
5233
  return;
5031
5234
  }
5235
+ if (cmd0 === "hooks") {
5236
+ const code = await runHooksCommand(argv.slice(1));
5237
+ if (code !== 0) process.exit(code);
5238
+ return;
5239
+ }
5032
5240
  const argvParsed = parseArgs(argv);
5033
5241
  if (argvParsed.positional.length === 0) {
5034
5242
  const orchestratorCode2 = await tryOrchestrator(process.cwd(), argvParsed);
@@ -5043,19 +5251,19 @@ async function main() {
5043
5251
  const target = positional[0];
5044
5252
  if (!target) {
5045
5253
  console.error("neat init: missing <path>");
5046
- usage();
5254
+ usage2();
5047
5255
  process.exit(2);
5048
5256
  }
5049
5257
  if (apply3 && dryRun) {
5050
5258
  console.error("neat init: --apply and --dry-run are mutually exclusive");
5051
5259
  process.exit(2);
5052
5260
  }
5053
- const scanPath = path9.resolve(target);
5261
+ const scanPath = path10.resolve(target);
5054
5262
  const projectExplicit = parsed.project !== null;
5055
- const projectName = projectExplicit ? project : path9.basename(scanPath);
5263
+ const projectName = projectExplicit ? project : path10.basename(scanPath);
5056
5264
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
5057
- const fallback = pathsForProject(projectKey, path9.join(scanPath, "neat-out")).snapshotPath;
5058
- const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? fallback);
5265
+ const fallback = pathsForProject(projectKey, path10.join(scanPath, "neat-out")).snapshotPath;
5266
+ const outPath = path10.resolve(process.env.NEAT_OUT_PATH ?? fallback);
5059
5267
  const result = await runInit({
5060
5268
  scanPath,
5061
5269
  outPath,
@@ -5073,24 +5281,24 @@ async function main() {
5073
5281
  const target = positional[0];
5074
5282
  if (!target) {
5075
5283
  console.error("neat watch: missing <path>");
5076
- usage();
5284
+ usage2();
5077
5285
  process.exit(2);
5078
5286
  }
5079
- const scanPath = path9.resolve(target);
5080
- const stat = await fs7.stat(scanPath).catch(() => null);
5287
+ const scanPath = path10.resolve(target);
5288
+ const stat = await fs8.stat(scanPath).catch(() => null);
5081
5289
  if (!stat || !stat.isDirectory()) {
5082
5290
  console.error(`neat watch: ${scanPath} is not a directory`);
5083
5291
  process.exit(2);
5084
5292
  }
5085
- const projectPaths = pathsForProject(project, path9.join(scanPath, "neat-out"));
5086
- const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
5087
- const errorsPath = path9.resolve(
5088
- process.env.NEAT_ERRORS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.errorsPath))
5293
+ const projectPaths = pathsForProject(project, path10.join(scanPath, "neat-out"));
5294
+ const outPath = path10.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
5295
+ const errorsPath = path10.resolve(
5296
+ process.env.NEAT_ERRORS_PATH ?? path10.join(path10.dirname(outPath), path10.basename(projectPaths.errorsPath))
5089
5297
  );
5090
- const staleEventsPath = path9.resolve(
5091
- process.env.NEAT_STALE_EVENTS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.staleEventsPath))
5298
+ const staleEventsPath = path10.resolve(
5299
+ process.env.NEAT_STALE_EVENTS_PATH ?? path10.join(path10.dirname(outPath), path10.basename(projectPaths.staleEventsPath))
5092
5300
  );
5093
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path9.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
5301
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path10.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
5094
5302
  const handle = await startWatch(getGraph(project), {
5095
5303
  scanPath,
5096
5304
  outPath,
@@ -5132,7 +5340,7 @@ async function main() {
5132
5340
  const name = positional[0];
5133
5341
  if (!name) {
5134
5342
  console.error("neat pause: missing <name>");
5135
- usage();
5343
+ usage2();
5136
5344
  process.exit(2);
5137
5345
  }
5138
5346
  const daemon = await findDaemonByProject(name);
@@ -5157,7 +5365,7 @@ async function main() {
5157
5365
  const name = positional[0];
5158
5366
  if (!name) {
5159
5367
  console.error("neat resume: missing <name>");
5160
- usage();
5368
+ usage2();
5161
5369
  process.exit(2);
5162
5370
  }
5163
5371
  const daemon = await findDaemonByProject(name);
@@ -5187,7 +5395,7 @@ async function main() {
5187
5395
  const name = positional[0];
5188
5396
  if (!name) {
5189
5397
  console.error("neat uninstall: missing <name>");
5190
- usage();
5398
+ usage2();
5191
5399
  process.exit(2);
5192
5400
  }
5193
5401
  const daemon = await findDaemonByProject(name);
@@ -5269,15 +5477,15 @@ async function main() {
5269
5477
  return;
5270
5478
  }
5271
5479
  console.error(`neat: unknown command "${cmd}"`);
5272
- usage();
5480
+ usage2();
5273
5481
  process.exit(1);
5274
5482
  }
5275
5483
  async function tryOrchestrator(cmd, parsed) {
5276
- const scanPath = path9.resolve(cmd);
5277
- const stat = await fs7.stat(scanPath).catch(() => null);
5484
+ const scanPath = path10.resolve(cmd);
5485
+ const stat = await fs8.stat(scanPath).catch(() => null);
5278
5486
  if (!stat || !stat.isDirectory()) return null;
5279
5487
  const projectExplicit = parsed.project !== null;
5280
- const projectName = projectExplicit ? parsed.project : path9.basename(scanPath);
5488
+ const projectName = projectExplicit ? parsed.project : path10.basename(scanPath);
5281
5489
  const result = await runOrchestrator({
5282
5490
  scanPath,
5283
5491
  project: projectName,
@@ -5533,6 +5741,6 @@ export {
5533
5741
  runInit,
5534
5742
  runQueryVerb,
5535
5743
  runSkill,
5536
- usage
5744
+ usage2 as usage
5537
5745
  };
5538
5746
  //# sourceMappingURL=cli.js.map