@neat.is/core 0.5.2 → 0.5.4-dev.20260721

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
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- PROVIDER_DISPATCH,
4
3
  clearDaemonRecord,
5
- getProviderDispatch,
4
+ deprovisionConnector,
5
+ getProviderFieldSchema,
6
+ isPushProvider,
7
+ knownProviderNames,
6
8
  portFromListenAddress,
9
+ provisionConnector,
7
10
  readDaemonRecord,
8
11
  resolveHost,
9
12
  resolveNeatVersion,
10
13
  validateConnectorEntry,
11
14
  writeDaemonRecord
12
- } from "./chunk-DPEPI2N6.js";
15
+ } from "./chunk-VR73QNJD.js";
13
16
  import {
14
17
  buildSearchIndex
15
18
  } from "./chunk-BIY46Q6U.js";
@@ -68,10 +71,10 @@ import {
68
71
  startPersistLoop,
69
72
  startStalenessLoop,
70
73
  upsertConnectorEntry
71
- } from "./chunk-BI3XKGVG.js";
74
+ } from "./chunk-PEFX3DBR.js";
72
75
  import {
73
76
  startOtelGrpcReceiver
74
- } from "./chunk-I72HTUOG.js";
77
+ } from "./chunk-4RU3AAOI.js";
75
78
  import {
76
79
  __dirname,
77
80
  __require,
@@ -79,11 +82,11 @@ import {
79
82
  buildOtelReceiver,
80
83
  listenSteppingOtlp,
81
84
  readAuthEnv
82
- } from "./chunk-CFDPIMRP.js";
85
+ } from "./chunk-I4NZ7PSN.js";
83
86
 
84
87
  // src/cli.ts
85
- import path9 from "path";
86
- import { promises as fs7 } from "fs";
88
+ import path10 from "path";
89
+ import { promises as fs8 } from "fs";
87
90
 
88
91
  // src/banner.ts
89
92
  import path from "path";
@@ -3566,6 +3569,8 @@ function parseConnectorArgs(args) {
3566
3569
  positional: [],
3567
3570
  skipValidate: false,
3568
3571
  plaintext: false,
3572
+ help: false,
3573
+ json: false,
3569
3574
  fields: {}
3570
3575
  };
3571
3576
  const positional = [];
@@ -3579,6 +3584,14 @@ function parseConnectorArgs(args) {
3579
3584
  out.plaintext = true;
3580
3585
  continue;
3581
3586
  }
3587
+ if (arg === "--help" || arg === "-h") {
3588
+ out.help = true;
3589
+ continue;
3590
+ }
3591
+ if (arg === "--json") {
3592
+ out.json = true;
3593
+ continue;
3594
+ }
3582
3595
  if (arg.startsWith("--")) {
3583
3596
  let flag = arg;
3584
3597
  let value;
@@ -3655,19 +3668,20 @@ function plaintextFields(ref) {
3655
3668
  async function connectorAdd(args, deps) {
3656
3669
  let provider = args.positional[0];
3657
3670
  if (!provider && deps.interactive) {
3658
- provider = (await deps.prompt(`Provider (${Object.keys(PROVIDER_DISPATCH).sort().join(", ")}):`)).trim();
3671
+ provider = (await deps.prompt(`Provider (${knownProviderNames().join(", ")}):`)).trim();
3659
3672
  }
3660
3673
  if (!provider) {
3661
3674
  deps.err("neat connector add: a provider is required (e.g. `neat connector add supabase`)");
3662
3675
  return 2;
3663
3676
  }
3664
- const dispatch = getProviderDispatch(provider);
3677
+ const dispatch = getProviderFieldSchema(provider);
3665
3678
  if (!dispatch) {
3666
3679
  deps.err(
3667
- `neat connector add: unknown provider "${provider}". known providers: ${Object.keys(PROVIDER_DISPATCH).sort().join(", ")}`
3680
+ `neat connector add: unknown provider "${provider}". known providers: ${knownProviderNames().join(", ")}`
3668
3681
  );
3669
3682
  return 2;
3670
3683
  }
3684
+ const push = isPushProvider(provider);
3671
3685
  let project = args.project;
3672
3686
  if (project === void 0 && deps.interactive) {
3673
3687
  const answer = (await deps.prompt("Project (blank = the project the daemon is bootstrapping):")).trim();
@@ -3706,7 +3720,7 @@ async function connectorAdd(args, deps) {
3706
3720
  credential,
3707
3721
  ...Object.keys(options).length > 0 ? { options } : {}
3708
3722
  };
3709
- if (!args.skipValidate) {
3723
+ if (!push && !args.skipValidate) {
3710
3724
  const outcome = await validateConnectorEntry(entry2, deps.env, deps.fetchImpl);
3711
3725
  const code = reportPreWriteValidation(outcome, deps);
3712
3726
  if (code !== 0) return code;
@@ -3717,14 +3731,32 @@ async function connectorAdd(args, deps) {
3717
3731
  `neat connector add: storing a literal secret at rest for ${plaintext.join(", ")} \u2014 prefer an env-var reference like \`$PROVIDER_TOKEN\` (pass --plaintext to silence this).`
3718
3732
  );
3719
3733
  }
3734
+ if (push) {
3735
+ if (args.skipValidate) {
3736
+ deps.out("note: --skip-validate has no effect for a push provider \u2014 provisioning the drain is the add.");
3737
+ }
3738
+ const outcome = await provisionConnector(entry2, deps.env, deps.fetchImpl);
3739
+ if (outcome.status !== "ok") {
3740
+ deps.err(
3741
+ `neat connector add: could not provision the ${provider} connector \u2014 ${outcome.reason}. Nothing was written.`
3742
+ );
3743
+ return 1;
3744
+ }
3745
+ if (outcome.options) entry2.options = { ...entry2.options ?? {}, ...outcome.options };
3746
+ if (outcome.note) deps.out(`note: ${outcome.note}`);
3747
+ }
3720
3748
  const { replaced } = await upsertConnectorEntry(entry2, deps.home);
3721
3749
  const verb = replaced ? "updated" : "added";
3722
3750
  const where = project ? `project "${project}"` : "the bootstrapping project";
3723
3751
  deps.out(`${verb} connector "${id}" (${provider}) for ${where}.`);
3724
- if (args.skipValidate) {
3725
- deps.out("skipped validation (--skip-validate) \u2014 the credential is checked at the next daemon poll.");
3752
+ if (push) {
3753
+ deps.out("The drain is live \u2014 traces arrive at the daemon's OTLP receiver. No poll or restart needed.");
3754
+ } else {
3755
+ if (args.skipValidate) {
3756
+ deps.out("skipped validation (--skip-validate) \u2014 the credential is checked at the next daemon poll.");
3757
+ }
3758
+ deps.out("Restart the project's daemon (or start it) to begin polling this connector.");
3726
3759
  }
3727
- deps.out("Restart the project's daemon (or start it) to begin polling this connector.");
3728
3760
  return 0;
3729
3761
  }
3730
3762
  function reportPreWriteValidation(outcome, deps) {
@@ -3750,10 +3782,26 @@ function reportPreWriteValidation(outcome, deps) {
3750
3782
  return 2;
3751
3783
  }
3752
3784
  }
3753
- async function connectorList(deps, filterProject) {
3785
+ async function connectorList(deps, filterProject, json = false) {
3754
3786
  const config = await readConnectorsConfig(deps.home);
3755
3787
  let entries = config.connectors;
3756
3788
  if (filterProject) entries = entries.filter((e) => e.project === filterProject);
3789
+ if (json) {
3790
+ const rows = entries.map((e) => ({
3791
+ id: e.id,
3792
+ provider: e.provider,
3793
+ project: e.project ?? null,
3794
+ kind: isPushProvider(e.provider) ? "push" : "pull",
3795
+ credential: describeCredential(e.credential, deps.env).map((c) => ({
3796
+ ...c.field ? { field: c.field } : {},
3797
+ ref: c.display,
3798
+ ...c.status ? { status: c.status } : { kind: c.kind }
3799
+ })),
3800
+ ...e.options ? { options: e.options } : {}
3801
+ }));
3802
+ deps.out(JSON.stringify(rows, null, 2));
3803
+ return 0;
3804
+ }
3757
3805
  if (entries.length === 0) {
3758
3806
  deps.out(
3759
3807
  filterProject ? `no connectors configured for project "${filterProject}".` : "no connectors configured. run `neat connector add <provider>` to add one."
@@ -3776,6 +3824,22 @@ async function connectorRemove(deps, id) {
3776
3824
  deps.err("neat connector remove: missing <id>. run `neat connector list` to see configured ids.");
3777
3825
  return 2;
3778
3826
  }
3827
+ const config = await readConnectorsConfig(deps.home);
3828
+ const entry2 = config.connectors.find((c) => c.id === id);
3829
+ if (!entry2) {
3830
+ deps.err(`neat connector remove: no connector with id "${id}". run \`neat connector list\` to see configured ids.`);
3831
+ return 1;
3832
+ }
3833
+ if (isPushProvider(entry2.provider)) {
3834
+ const outcome = await deprovisionConnector(entry2, deps.env, deps.fetchImpl);
3835
+ if (outcome.status !== "ok") {
3836
+ deps.err(
3837
+ `neat connector remove: could not delete the ${entry2.provider} drain \u2014 ${outcome.reason}. The entry was kept so you can retry (or delete the drain in the Vercel dashboard, then re-run).`
3838
+ );
3839
+ return 1;
3840
+ }
3841
+ if (outcome.note) deps.out(`note: ${outcome.note}`);
3842
+ }
3779
3843
  const removed = await removeConnectorEntry(id, deps.home);
3780
3844
  if (!removed) {
3781
3845
  deps.err(`neat connector remove: no connector with id "${id}". run \`neat connector list\` to see configured ids.`);
@@ -3816,13 +3880,20 @@ async function connectorTest(deps, id) {
3816
3880
  }
3817
3881
  function printConnectorUsage(write) {
3818
3882
  write("usage: neat connector <add|list|remove|test> [args]");
3883
+ write(` providers: ${knownProviderNames().join(", ")}`);
3884
+ write(" pull (polled): supabase, railway, firebase, cloudflare");
3885
+ write(" push (drains): vercel \u2014 provisions a Vercel trace Drain that forwards");
3886
+ write(" traces to the daemon's OTLP receiver; no app instrumentation");
3819
3887
  write(" add <provider> add a connector; validates the credential first (--skip-validate to skip)");
3820
3888
  write(" flags: --project <name> --credential/--token <$VAR|value> --id <id>");
3821
3889
  write(" --<option> <value> (provider-specific) --plaintext --skip-validate");
3890
+ write(" vercel: neat connector add vercel --token $VERCEL_TOKEN \\");
3891
+ write(" --otel-token $NEAT_OTEL_TOKEN --team-id <teamId> \\");
3892
+ write(" --endpoint https://<public-host>/v1/traces [--project-ids <id,id>]");
3822
3893
  write(" list list configured connectors (credentials shown redacted)");
3823
3894
  write(" flags: --project <name>");
3824
- write(" remove <id> remove a connector by id");
3825
- write(" test <id> re-run the credential validation for an existing connector");
3895
+ write(" remove <id> remove a connector by id (a push provider also has its drain deleted)");
3896
+ write(" test <id> re-run validation for an existing connector (a push provider re-tests its drain)");
3826
3897
  }
3827
3898
  async function runConnectorCommand(rawArgs, deps = {}) {
3828
3899
  const resolved = resolveDeps(deps);
@@ -3832,11 +3903,15 @@ async function runConnectorCommand(rawArgs, deps = {}) {
3832
3903
  return 2;
3833
3904
  }
3834
3905
  const a = parsed.value;
3906
+ if (a.help) {
3907
+ printConnectorUsage(resolved.out);
3908
+ return 0;
3909
+ }
3835
3910
  switch (a.subcommand) {
3836
3911
  case "add":
3837
3912
  return connectorAdd(a, resolved);
3838
3913
  case "list":
3839
- return connectorList(resolved, a.project);
3914
+ return connectorList(resolved, a.project, a.json);
3840
3915
  case "remove":
3841
3916
  return connectorRemove(resolved, a.positional[0]);
3842
3917
  case "test":
@@ -3852,8 +3927,181 @@ async function runConnectorCommand(rawArgs, deps = {}) {
3852
3927
  }
3853
3928
  }
3854
3929
 
3855
- // src/cli-verbs.ts
3930
+ // src/hooks-cli.ts
3856
3931
  import path8 from "path";
3932
+ import os from "os";
3933
+ import { promises as fs7 } from "fs";
3934
+ import { fileURLToPath as fileURLToPath3 } from "url";
3935
+ var HOOK_FILENAME = "neat-search-nudge.mjs";
3936
+ var GUIDE_FILENAME = "GRAPH_FIRST.md";
3937
+ var GUIDE_INSTALL_NAME = "neat-graph-first.md";
3938
+ var HOOK_MATCHER = "Grep|Glob|Bash";
3939
+ function moduleDir() {
3940
+ return typeof __dirname !== "undefined" ? __dirname : path8.dirname(fileURLToPath3(import.meta.url));
3941
+ }
3942
+ async function readSkillAsset(rel) {
3943
+ const here = moduleDir();
3944
+ const candidates = [
3945
+ path8.resolve(here, "../../claude-skill", rel),
3946
+ path8.resolve(here, "../../../claude-skill", rel),
3947
+ path8.resolve(here, "../claude-skill", rel)
3948
+ ];
3949
+ for (const candidate of candidates) {
3950
+ try {
3951
+ return await fs7.readFile(candidate, "utf8");
3952
+ } catch {
3953
+ }
3954
+ }
3955
+ throw new Error(
3956
+ `neat hooks: could not find @neat.is/claude-skill/${rel} \u2014 is the package installed?`
3957
+ );
3958
+ }
3959
+ function neatHome() {
3960
+ const override = process.env.NEAT_HOME;
3961
+ if (override && override.length > 0) return path8.resolve(override);
3962
+ return path8.join(os.homedir(), ".neat");
3963
+ }
3964
+ function claudeSettingsPath() {
3965
+ const override = process.env.NEAT_CLAUDE_SETTINGS;
3966
+ if (override && override.length > 0) return path8.resolve(override);
3967
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
3968
+ return path8.join(home, ".claude", "settings.json");
3969
+ }
3970
+ function installedHookPath() {
3971
+ return path8.join(neatHome(), "hooks", HOOK_FILENAME);
3972
+ }
3973
+ function isNeatSearchEntry(entry2) {
3974
+ return (entry2.hooks ?? []).some(
3975
+ (h) => typeof h.command === "string" && h.command.includes(HOOK_FILENAME)
3976
+ );
3977
+ }
3978
+ function neatHookEntry(command) {
3979
+ return { matcher: HOOK_MATCHER, hooks: [{ type: "command", command }] };
3980
+ }
3981
+ function hookCommand(scriptPath) {
3982
+ return `node "${scriptPath}"`;
3983
+ }
3984
+ async function runHooks(opts) {
3985
+ if (opts.printHook) {
3986
+ process.stdout.write(await readSkillAsset(`hooks/${HOOK_FILENAME}`));
3987
+ return { exitCode: 0 };
3988
+ }
3989
+ if (opts.printGuide) {
3990
+ process.stdout.write(await readSkillAsset(GUIDE_FILENAME));
3991
+ return { exitCode: 0 };
3992
+ }
3993
+ if (opts.printSettings) {
3994
+ const block = {
3995
+ hooks: { PreToolUse: [neatHookEntry(hookCommand(installedHookPath()))] }
3996
+ };
3997
+ process.stdout.write(JSON.stringify(block, null, 2) + "\n");
3998
+ return { exitCode: 0 };
3999
+ }
4000
+ if (opts.apply) {
4001
+ const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
4002
+ const guide = await readSkillAsset(GUIDE_FILENAME);
4003
+ const scriptPath = installedHookPath();
4004
+ await fs7.mkdir(path8.dirname(scriptPath), { recursive: true });
4005
+ await fs7.writeFile(scriptPath, hookScript, { mode: 493 });
4006
+ const guidePath = path8.join(neatHome(), GUIDE_INSTALL_NAME);
4007
+ await fs7.writeFile(guidePath, guide, "utf8");
4008
+ const settingsFile = claudeSettingsPath();
4009
+ let settings = {};
4010
+ try {
4011
+ settings = JSON.parse(await fs7.readFile(settingsFile, "utf8"));
4012
+ } catch (err) {
4013
+ if (err.code !== "ENOENT") {
4014
+ console.error(
4015
+ `neat hooks: failed to read ${settingsFile} \u2014 ${err.message}`
4016
+ );
4017
+ return { exitCode: 1 };
4018
+ }
4019
+ }
4020
+ const hooks = settings.hooks ?? {};
4021
+ const preToolUse = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
4022
+ const command = hookCommand(scriptPath);
4023
+ const existingIdx = preToolUse.findIndex(isNeatSearchEntry);
4024
+ if (existingIdx >= 0) {
4025
+ preToolUse[existingIdx] = neatHookEntry(command);
4026
+ } else {
4027
+ preToolUse.push(neatHookEntry(command));
4028
+ }
4029
+ const merged = {
4030
+ ...settings,
4031
+ hooks: { ...hooks, PreToolUse: preToolUse }
4032
+ };
4033
+ await fs7.mkdir(path8.dirname(settingsFile), { recursive: true });
4034
+ await fs7.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
4035
+ console.log(`neat hooks: installed the search-nudge hook`);
4036
+ console.log(` script: ${scriptPath}`);
4037
+ console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
4038
+ console.log(` guidance: ${guidePath}`);
4039
+ console.log("");
4040
+ console.log("restart Claude Code to load the hook. On a Grep/Glob or a Bash grep,");
4041
+ console.log("your agent will now be nudged to query NEAT first.");
4042
+ console.log("");
4043
+ console.log("The hook is Claude-Code-specific. For agents on other harnesses, paste");
4044
+ console.log(`the guidance above into your project instructions (CLAUDE.md / AGENTS.md).`);
4045
+ return { exitCode: 0 };
4046
+ }
4047
+ usage();
4048
+ return { exitCode: 0 };
4049
+ }
4050
+ function usage() {
4051
+ console.log("neat hooks \u2014 wire NEAT into your agent so it queries the graph before grepping");
4052
+ console.log("");
4053
+ console.log(" --apply install the Claude Code search-nudge hook and write the");
4054
+ console.log(" graph-first guidance to ~/.neat/, merging into");
4055
+ console.log(" ~/.claude/settings.json without touching your other hooks");
4056
+ console.log(" --print-hook print the hook script to stdout");
4057
+ console.log(" --print-guide print the agent-agnostic graph-first guidance to stdout");
4058
+ console.log(" --print-settings print the settings.json PreToolUse block --apply would add");
4059
+ console.log("");
4060
+ console.log("The hook is a gentle, non-blocking nudge \u2014 searches still run. It is");
4061
+ console.log("Claude-Code-specific; other harnesses get the same steer from the guidance.");
4062
+ }
4063
+ async function runHooksCommand(args) {
4064
+ const opts = {
4065
+ apply: false,
4066
+ printHook: false,
4067
+ printGuide: false,
4068
+ printSettings: false
4069
+ };
4070
+ for (const arg of args) {
4071
+ switch (arg) {
4072
+ case "--apply":
4073
+ opts.apply = true;
4074
+ break;
4075
+ case "--print-hook":
4076
+ opts.printHook = true;
4077
+ break;
4078
+ case "--print-guide":
4079
+ opts.printGuide = true;
4080
+ break;
4081
+ case "--print-settings":
4082
+ opts.printSettings = true;
4083
+ break;
4084
+ case "-h":
4085
+ case "--help":
4086
+ usage();
4087
+ return 0;
4088
+ default:
4089
+ console.error(`neat hooks: unknown flag "${arg}"`);
4090
+ usage();
4091
+ return 2;
4092
+ }
4093
+ }
4094
+ try {
4095
+ const { exitCode } = await runHooks(opts);
4096
+ return exitCode;
4097
+ } catch (err) {
4098
+ console.error(err.message);
4099
+ return 1;
4100
+ }
4101
+ }
4102
+
4103
+ // src/cli-verbs.ts
4104
+ import path9 from "path";
3857
4105
 
3858
4106
  // src/cli-client.ts
3859
4107
  import { Provenance as Provenance2 } from "@neat.is/types";
@@ -3881,10 +4129,10 @@ function createHttpClient(baseUrl, bearerToken) {
3881
4129
  const root = baseUrl.replace(/\/$/, "");
3882
4130
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
3883
4131
  return {
3884
- async get(path10) {
4132
+ async get(path11) {
3885
4133
  let res;
3886
4134
  try {
3887
- res = await fetch(`${root}${path10}`, {
4135
+ res = await fetch(`${root}${path11}`, {
3888
4136
  headers: { ...authHeader }
3889
4137
  });
3890
4138
  } catch (err) {
@@ -3896,16 +4144,16 @@ function createHttpClient(baseUrl, bearerToken) {
3896
4144
  const body = await res.text().catch(() => "");
3897
4145
  throw new HttpError(
3898
4146
  res.status,
3899
- `${res.status} ${res.statusText} on GET ${path10}: ${body}`,
4147
+ `${res.status} ${res.statusText} on GET ${path11}: ${body}`,
3900
4148
  body
3901
4149
  );
3902
4150
  }
3903
4151
  return await res.json();
3904
4152
  },
3905
- async post(path10, body) {
4153
+ async post(path11, body) {
3906
4154
  let res;
3907
4155
  try {
3908
- res = await fetch(`${root}${path10}`, {
4156
+ res = await fetch(`${root}${path11}`, {
3909
4157
  method: "POST",
3910
4158
  headers: { "content-type": "application/json", ...authHeader },
3911
4159
  body: JSON.stringify(body)
@@ -3919,7 +4167,7 @@ function createHttpClient(baseUrl, bearerToken) {
3919
4167
  const text = await res.text().catch(() => "");
3920
4168
  throw new HttpError(
3921
4169
  res.status,
3922
- `${res.status} ${res.statusText} on POST ${path10}: ${text}`,
4170
+ `${res.status} ${res.statusText} on POST ${path11}: ${text}`,
3923
4171
  text
3924
4172
  );
3925
4173
  }
@@ -3933,12 +4181,12 @@ function projectPath(project, suffix) {
3933
4181
  }
3934
4182
  async function runRootCause(client, input) {
3935
4183
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
3936
- const path10 = projectPath(
4184
+ const path11 = projectPath(
3937
4185
  input.project,
3938
4186
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
3939
4187
  );
3940
4188
  try {
3941
- const result = await client.get(path10);
4189
+ const result = await client.get(path11);
3942
4190
  const arrowPath = result.traversalPath.join(" \u2190 ");
3943
4191
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
3944
4192
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -3964,12 +4212,12 @@ async function runRootCause(client, input) {
3964
4212
  }
3965
4213
  async function runBlastRadius(client, input) {
3966
4214
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
3967
- const path10 = projectPath(
4215
+ const path11 = projectPath(
3968
4216
  input.project,
3969
4217
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
3970
4218
  );
3971
4219
  try {
3972
- const result = await client.get(path10);
4220
+ const result = await client.get(path11);
3973
4221
  if (result.totalAffected === 0) {
3974
4222
  return {
3975
4223
  summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
@@ -4003,12 +4251,12 @@ function formatBlastEntry(n) {
4003
4251
  }
4004
4252
  async function runDependencies(client, input) {
4005
4253
  const depth = input.depth ?? 3;
4006
- const path10 = projectPath(
4254
+ const path11 = projectPath(
4007
4255
  input.project,
4008
4256
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
4009
4257
  );
4010
4258
  try {
4011
- const result = await client.get(path10);
4259
+ const result = await client.get(path11);
4012
4260
  if (result.total === 0) {
4013
4261
  return {
4014
4262
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -4100,9 +4348,9 @@ function formatDuration(ms) {
4100
4348
  return `${Math.round(h / 24)}d`;
4101
4349
  }
4102
4350
  async function runIncidents(client, input) {
4103
- const path10 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
4351
+ const path11 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
4104
4352
  try {
4105
- const body = await client.get(path10);
4353
+ const body = await client.get(path11);
4106
4354
  const events = body.events;
4107
4355
  if (events.length === 0) {
4108
4356
  return {
@@ -4392,7 +4640,7 @@ async function resolveProjectEntry(opts) {
4392
4640
  const cwd = opts.cwd ?? process.cwd();
4393
4641
  const resolvedCwd = await normalizeProjectPath(cwd);
4394
4642
  for (const entry2 of entries) {
4395
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path8.sep}`)) {
4643
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path9.sep}`)) {
4396
4644
  return entry2;
4397
4645
  }
4398
4646
  }
@@ -4557,7 +4805,7 @@ function isNpxInvocation() {
4557
4805
  function commandPrefix() {
4558
4806
  return isNpxInvocation() ? "npx neat.is" : "neat";
4559
4807
  }
4560
- function usage() {
4808
+ function usage2() {
4561
4809
  const neat = commandPrefix();
4562
4810
  console.log("Installed via npx? Prefix commands with `npx neat.is`, or install once: `npm i -g neat.is`.");
4563
4811
  console.log("");
@@ -4594,6 +4842,14 @@ function usage() {
4594
4842
  console.log(" Flags:");
4595
4843
  console.log(" --print-config print the JSON snippet to stdout");
4596
4844
  console.log(" --apply merge mcpServers.neat into ~/.claude.json");
4845
+ console.log(" hooks Wire NEAT into your agent so it queries the graph before");
4846
+ console.log(" grepping. Installs a gentle Claude Code search-nudge hook and");
4847
+ console.log(" writes agent-agnostic graph-first guidance for other harnesses.");
4848
+ console.log(" Flags:");
4849
+ console.log(" --apply install the hook + guidance");
4850
+ console.log(" --print-hook print the hook script");
4851
+ console.log(" --print-guide print the graph-first guidance");
4852
+ console.log(" --print-settings print the settings.json block --apply adds");
4597
4853
  console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
4598
4854
  console.log(" emit a docker-compose / systemd / docker run artifact, and");
4599
4855
  console.log(" print the OTel env-vars block to paste into your platform.");
@@ -4606,8 +4862,9 @@ function usage() {
4606
4862
  console.log(" --dry-run run extraction in-memory; do not write");
4607
4863
  console.log(" --no-instrument skip the SDK install apply step");
4608
4864
  console.log(" --json emit the delta summary as JSON");
4609
- console.log(" connector Configure pull-based OBSERVED connectors (supabase, railway,");
4610
- console.log(" firebase, cloudflare). Subcommands:");
4865
+ console.log(" connector Configure OBSERVED connectors \u2014 pull (supabase, railway,");
4866
+ console.log(" firebase, cloudflare) and push (vercel, via a trace Drain).");
4867
+ console.log(" Subcommands:");
4611
4868
  console.log(" add <provider> add a connector; validates the credential");
4612
4869
  console.log(" against the provider first (--skip-validate to skip)");
4613
4870
  console.log(" flags: --project <name>, --credential/--token <$VAR|value>,");
@@ -4838,7 +5095,7 @@ async function buildPatchSections(services, project) {
4838
5095
  }
4839
5096
  async function runInit(opts) {
4840
5097
  const written = [];
4841
- const stat = await fs7.stat(opts.scanPath).catch(() => null);
5098
+ const stat = await fs8.stat(opts.scanPath).catch(() => null);
4842
5099
  if (!stat || !stat.isDirectory()) {
4843
5100
  console.error(`neat init: ${opts.scanPath} is not a directory`);
4844
5101
  return { exitCode: 2, writtenFiles: written };
@@ -4847,13 +5104,13 @@ async function runInit(opts) {
4847
5104
  printDiscoveryReport(opts, services);
4848
5105
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
4849
5106
  const patch = renderPatch(sections);
4850
- const patchPath = path9.join(opts.scanPath, "neat.patch");
5107
+ const patchPath = path10.join(opts.scanPath, "neat.patch");
4851
5108
  if (opts.dryRun) {
4852
- await fs7.writeFile(patchPath, patch, "utf8");
5109
+ await fs8.writeFile(patchPath, patch, "utf8");
4853
5110
  written.push(patchPath);
4854
5111
  console.log(`dry-run: patch written to ${patchPath}`);
4855
- const gitignorePath = path9.join(opts.scanPath, ".gitignore");
4856
- const gitignoreExists = await fs7.stat(gitignorePath).then(() => true).catch(() => false);
5112
+ const gitignorePath = path10.join(opts.scanPath, ".gitignore");
5113
+ const gitignoreExists = await fs8.stat(gitignorePath).then(() => true).catch(() => false);
4857
5114
  const verb = gitignoreExists ? "append" : "create";
4858
5115
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
4859
5116
  console.log("rerun without --dry-run to register and snapshot.");
@@ -4864,9 +5121,9 @@ async function runInit(opts) {
4864
5121
  const graph = getGraph(graphKey);
4865
5122
  const projectPaths = pathsForProject(
4866
5123
  graphKey,
4867
- path9.join(opts.scanPath, "neat-out")
5124
+ path10.join(opts.scanPath, "neat-out")
4868
5125
  );
4869
- const errorsPath = path9.join(path9.dirname(opts.outPath), path9.basename(projectPaths.errorsPath));
5126
+ const errorsPath = path10.join(path10.dirname(opts.outPath), path10.basename(projectPaths.errorsPath));
4870
5127
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
4871
5128
  await saveGraphToDisk(graph, opts.outPath);
4872
5129
  written.push(opts.outPath);
@@ -4945,7 +5202,7 @@ async function runInit(opts) {
4945
5202
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
4946
5203
  }
4947
5204
  } else {
4948
- await fs7.writeFile(patchPath, patch, "utf8");
5205
+ await fs8.writeFile(patchPath, patch, "utf8");
4949
5206
  written.push(patchPath);
4950
5207
  }
4951
5208
  }
@@ -4985,9 +5242,9 @@ var CLAUDE_SKILL_CONFIG = {
4985
5242
  };
4986
5243
  function claudeConfigPath() {
4987
5244
  const override = process.env.NEAT_CLAUDE_CONFIG;
4988
- if (override && override.length > 0) return path9.resolve(override);
5245
+ if (override && override.length > 0) return path10.resolve(override);
4989
5246
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4990
- return path9.join(home, ".claude.json");
5247
+ return path10.join(home, ".claude.json");
4991
5248
  }
4992
5249
  async function runSkill(opts) {
4993
5250
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -4999,7 +5256,7 @@ async function runSkill(opts) {
4999
5256
  const target = claudeConfigPath();
5000
5257
  let existing = {};
5001
5258
  try {
5002
- existing = JSON.parse(await fs7.readFile(target, "utf8"));
5259
+ existing = JSON.parse(await fs8.readFile(target, "utf8"));
5003
5260
  } catch (err) {
5004
5261
  if (err.code !== "ENOENT") {
5005
5262
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -5011,10 +5268,13 @@ async function runSkill(opts) {
5011
5268
  ...existing,
5012
5269
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
5013
5270
  };
5014
- await fs7.mkdir(path9.dirname(target), { recursive: true });
5015
- await fs7.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
5271
+ await fs8.mkdir(path10.dirname(target), { recursive: true });
5272
+ await fs8.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
5016
5273
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
5017
5274
  console.log("restart Claude Code to pick up the new MCP server.");
5275
+ console.log("");
5276
+ console.log("Tip: run `neat hooks --apply` to also install the search-nudge hook, so");
5277
+ console.log("your agent reaches for the graph before it grep-scans the repo.");
5018
5278
  return { exitCode: 0 };
5019
5279
  }
5020
5280
  console.log("neat skill \u2014 Claude Code MCP drop-in for NEAT");
@@ -5027,13 +5287,16 @@ async function runSkill(opts) {
5027
5287
  console.log("");
5028
5288
  console.log("The MCP server reads NEAT_CORE_URL for the daemon URL \u2014 point it at a");
5029
5289
  console.log("non-default daemon by editing that value in the generated config.");
5290
+ console.log("");
5291
+ console.log("See also `neat hooks --apply` \u2014 the search-nudge hook + graph-first guidance");
5292
+ console.log("that steer an agent to query the graph before falling back to text search.");
5030
5293
  return { exitCode: 0 };
5031
5294
  }
5032
5295
  async function main() {
5033
5296
  const argv = process.argv.slice(2);
5034
5297
  const cmd0 = argv[0];
5035
5298
  if (cmd0 === "-h" || cmd0 === "--help") {
5036
- usage();
5299
+ usage2();
5037
5300
  process.exit(0);
5038
5301
  }
5039
5302
  if (cmd0 === "--version" || cmd0 === "-v" || cmd0 === "version") {
@@ -5045,6 +5308,11 @@ async function main() {
5045
5308
  if (code !== 0) process.exit(code);
5046
5309
  return;
5047
5310
  }
5311
+ if (cmd0 === "hooks") {
5312
+ const code = await runHooksCommand(argv.slice(1));
5313
+ if (code !== 0) process.exit(code);
5314
+ return;
5315
+ }
5048
5316
  const argvParsed = parseArgs(argv);
5049
5317
  if (argvParsed.positional.length === 0) {
5050
5318
  const orchestratorCode2 = await tryOrchestrator(process.cwd(), argvParsed);
@@ -5059,19 +5327,19 @@ async function main() {
5059
5327
  const target = positional[0];
5060
5328
  if (!target) {
5061
5329
  console.error("neat init: missing <path>");
5062
- usage();
5330
+ usage2();
5063
5331
  process.exit(2);
5064
5332
  }
5065
5333
  if (apply3 && dryRun) {
5066
5334
  console.error("neat init: --apply and --dry-run are mutually exclusive");
5067
5335
  process.exit(2);
5068
5336
  }
5069
- const scanPath = path9.resolve(target);
5337
+ const scanPath = path10.resolve(target);
5070
5338
  const projectExplicit = parsed.project !== null;
5071
- const projectName = projectExplicit ? project : path9.basename(scanPath);
5339
+ const projectName = projectExplicit ? project : path10.basename(scanPath);
5072
5340
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
5073
- const fallback = pathsForProject(projectKey, path9.join(scanPath, "neat-out")).snapshotPath;
5074
- const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? fallback);
5341
+ const fallback = pathsForProject(projectKey, path10.join(scanPath, "neat-out")).snapshotPath;
5342
+ const outPath = path10.resolve(process.env.NEAT_OUT_PATH ?? fallback);
5075
5343
  const result = await runInit({
5076
5344
  scanPath,
5077
5345
  outPath,
@@ -5089,24 +5357,24 @@ async function main() {
5089
5357
  const target = positional[0];
5090
5358
  if (!target) {
5091
5359
  console.error("neat watch: missing <path>");
5092
- usage();
5360
+ usage2();
5093
5361
  process.exit(2);
5094
5362
  }
5095
- const scanPath = path9.resolve(target);
5096
- const stat = await fs7.stat(scanPath).catch(() => null);
5363
+ const scanPath = path10.resolve(target);
5364
+ const stat = await fs8.stat(scanPath).catch(() => null);
5097
5365
  if (!stat || !stat.isDirectory()) {
5098
5366
  console.error(`neat watch: ${scanPath} is not a directory`);
5099
5367
  process.exit(2);
5100
5368
  }
5101
- const projectPaths = pathsForProject(project, path9.join(scanPath, "neat-out"));
5102
- const outPath = path9.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
5103
- const errorsPath = path9.resolve(
5104
- process.env.NEAT_ERRORS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.errorsPath))
5369
+ const projectPaths = pathsForProject(project, path10.join(scanPath, "neat-out"));
5370
+ const outPath = path10.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
5371
+ const errorsPath = path10.resolve(
5372
+ process.env.NEAT_ERRORS_PATH ?? path10.join(path10.dirname(outPath), path10.basename(projectPaths.errorsPath))
5105
5373
  );
5106
- const staleEventsPath = path9.resolve(
5107
- process.env.NEAT_STALE_EVENTS_PATH ?? path9.join(path9.dirname(outPath), path9.basename(projectPaths.staleEventsPath))
5374
+ const staleEventsPath = path10.resolve(
5375
+ process.env.NEAT_STALE_EVENTS_PATH ?? path10.join(path10.dirname(outPath), path10.basename(projectPaths.staleEventsPath))
5108
5376
  );
5109
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path9.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
5377
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path10.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
5110
5378
  const handle = await startWatch(getGraph(project), {
5111
5379
  scanPath,
5112
5380
  outPath,
@@ -5148,7 +5416,7 @@ async function main() {
5148
5416
  const name = positional[0];
5149
5417
  if (!name) {
5150
5418
  console.error("neat pause: missing <name>");
5151
- usage();
5419
+ usage2();
5152
5420
  process.exit(2);
5153
5421
  }
5154
5422
  const daemon = await findDaemonByProject(name);
@@ -5173,7 +5441,7 @@ async function main() {
5173
5441
  const name = positional[0];
5174
5442
  if (!name) {
5175
5443
  console.error("neat resume: missing <name>");
5176
- usage();
5444
+ usage2();
5177
5445
  process.exit(2);
5178
5446
  }
5179
5447
  const daemon = await findDaemonByProject(name);
@@ -5203,7 +5471,7 @@ async function main() {
5203
5471
  const name = positional[0];
5204
5472
  if (!name) {
5205
5473
  console.error("neat uninstall: missing <name>");
5206
- usage();
5474
+ usage2();
5207
5475
  process.exit(2);
5208
5476
  }
5209
5477
  const daemon = await findDaemonByProject(name);
@@ -5285,15 +5553,15 @@ async function main() {
5285
5553
  return;
5286
5554
  }
5287
5555
  console.error(`neat: unknown command "${cmd}"`);
5288
- usage();
5556
+ usage2();
5289
5557
  process.exit(1);
5290
5558
  }
5291
5559
  async function tryOrchestrator(cmd, parsed) {
5292
- const scanPath = path9.resolve(cmd);
5293
- const stat = await fs7.stat(scanPath).catch(() => null);
5560
+ const scanPath = path10.resolve(cmd);
5561
+ const stat = await fs8.stat(scanPath).catch(() => null);
5294
5562
  if (!stat || !stat.isDirectory()) return null;
5295
5563
  const projectExplicit = parsed.project !== null;
5296
- const projectName = projectExplicit ? parsed.project : path9.basename(scanPath);
5564
+ const projectName = projectExplicit ? parsed.project : path10.basename(scanPath);
5297
5565
  const result = await runOrchestrator({
5298
5566
  scanPath,
5299
5567
  project: projectName,
@@ -5549,6 +5817,6 @@ export {
5549
5817
  runInit,
5550
5818
  runQueryVerb,
5551
5819
  runSkill,
5552
- usage
5820
+ usage2 as usage
5553
5821
  };
5554
5822
  //# sourceMappingURL=cli.js.map