foxref-remote 0.1.0
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.ts +19 -0
- package/dist/cli.js +218 -0
- package/dist/client.d.ts +75 -0
- package/dist/client.js +57 -0
- package/dist/init.d.ts +10 -0
- package/dist/init.js +273 -0
- package/package.json +28 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* foxref-remote — Code intelligence CLI for Lienly developers.
|
|
4
|
+
*
|
|
5
|
+
* Queries the FoxRef server for symbol references, search, and impact analysis.
|
|
6
|
+
* All data comes from the centrally-maintained code index.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* foxref-remote who-uses MyClass Find all usages of a symbol
|
|
10
|
+
* foxref-remote search tensor Fuzzy search for symbols
|
|
11
|
+
* foxref-remote symbols-in src/foo.rs List symbols defined in a file
|
|
12
|
+
* foxref-remote impact MyClass What breaks if I change this?
|
|
13
|
+
* foxref-remote health Check server status
|
|
14
|
+
*
|
|
15
|
+
* Environment:
|
|
16
|
+
* FOXREF_SERVER Server URL (default: https://foxref.lienly.com)
|
|
17
|
+
* FOXREF_API_KEY API key for authentication (if required)
|
|
18
|
+
*/
|
|
19
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* foxref-remote — Code intelligence CLI for Lienly developers.
|
|
5
|
+
*
|
|
6
|
+
* Queries the FoxRef server for symbol references, search, and impact analysis.
|
|
7
|
+
* All data comes from the centrally-maintained code index.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* foxref-remote who-uses MyClass Find all usages of a symbol
|
|
11
|
+
* foxref-remote search tensor Fuzzy search for symbols
|
|
12
|
+
* foxref-remote symbols-in src/foo.rs List symbols defined in a file
|
|
13
|
+
* foxref-remote impact MyClass What breaks if I change this?
|
|
14
|
+
* foxref-remote health Check server status
|
|
15
|
+
*
|
|
16
|
+
* Environment:
|
|
17
|
+
* FOXREF_SERVER Server URL (default: https://foxref.lienly.com)
|
|
18
|
+
* FOXREF_API_KEY API key for authentication (if required)
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
const commander_1 = require("commander");
|
|
22
|
+
const client_js_1 = require("./client.js");
|
|
23
|
+
const init_js_1 = require("./init.js");
|
|
24
|
+
const program = new commander_1.Command()
|
|
25
|
+
.name("foxref-remote")
|
|
26
|
+
.description("Code intelligence CLI — search symbols, find references, analyze impact")
|
|
27
|
+
.version("0.1.0")
|
|
28
|
+
.option("--branch <branch>", "Branch to query (also reads FOXREF_BRANCH env var)")
|
|
29
|
+
.option("--server <url>", "Server URL (also reads FOXREF_SERVER env var)")
|
|
30
|
+
.option("--key <key>", "API key (also reads FOXREF_API_KEY env var)")
|
|
31
|
+
.hook("preAction", () => {
|
|
32
|
+
// Initialize client with global options before any command runs
|
|
33
|
+
const opts = program.opts();
|
|
34
|
+
globalClient = new client_js_1.FoxRefClient(opts.server, opts.key, opts.branch);
|
|
35
|
+
});
|
|
36
|
+
let globalClient = new client_js_1.FoxRefClient();
|
|
37
|
+
// ─── who-uses ──────────────────────────────────────────────────
|
|
38
|
+
program
|
|
39
|
+
.command("who-uses <symbol>")
|
|
40
|
+
.description("Find all usages of a symbol across the codebase")
|
|
41
|
+
.option("-n, --limit <n>", "Max results", "20")
|
|
42
|
+
.option("--json", "Output as JSON")
|
|
43
|
+
.action(async (symbol, opts) => {
|
|
44
|
+
try {
|
|
45
|
+
const res = await globalClient.whoUses(symbol, parseInt(opts.limit));
|
|
46
|
+
if (opts.json) {
|
|
47
|
+
console.log(JSON.stringify(res, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (res.count === 0) {
|
|
51
|
+
console.log(`No usages found for '${symbol}'`);
|
|
52
|
+
console.log(` Try: foxref-remote search ${symbol}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
console.log(`${res.count} usages of '${symbol}' (${res.exact} exact, ${res.heuristic} heuristic)`);
|
|
56
|
+
if (res.hits.length > 0) {
|
|
57
|
+
console.log(` Defined in: ${res.hits[0].defined_in}:${res.hits[0].defined_line}`);
|
|
58
|
+
}
|
|
59
|
+
console.log();
|
|
60
|
+
for (const hit of res.hits) {
|
|
61
|
+
const tag = hit.is_heuristic ? "[heuristic]" : "[exact]";
|
|
62
|
+
console.log(` ${tag} ${hit.source_file}:${hit.source_line}`);
|
|
63
|
+
}
|
|
64
|
+
if (res.count > res.hits.length) {
|
|
65
|
+
console.log(` ... and ${res.count - res.hits.length} more (use -n to show more)`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
handleError(e);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// ─── search ────────────────────────────────────────────────────
|
|
73
|
+
program
|
|
74
|
+
.command("search <query>")
|
|
75
|
+
.description("Fuzzy search for symbols by name")
|
|
76
|
+
.option("-n, --limit <n>", "Max results", "20")
|
|
77
|
+
.option("--json", "Output as JSON")
|
|
78
|
+
.action(async (query, opts) => {
|
|
79
|
+
try {
|
|
80
|
+
const res = await globalClient.search(query, parseInt(opts.limit));
|
|
81
|
+
if (opts.json) {
|
|
82
|
+
console.log(JSON.stringify(res, null, 2));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (res.count === 0) {
|
|
86
|
+
console.log(`No symbols matching '${query}'`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.log(`${res.count} symbols matching '${query}':`);
|
|
90
|
+
console.log();
|
|
91
|
+
for (const r of res.results) {
|
|
92
|
+
console.log(` ${r.name} (${r.file}:${r.line})`);
|
|
93
|
+
}
|
|
94
|
+
if (res.count > res.results.length) {
|
|
95
|
+
console.log(` ... and ${res.count - res.results.length} more`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
handleError(e);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// ─── symbols-in ────────────────────────────────────────────────
|
|
103
|
+
program
|
|
104
|
+
.command("symbols-in <file>")
|
|
105
|
+
.description("List all symbols defined in a file")
|
|
106
|
+
.option("--json", "Output as JSON")
|
|
107
|
+
.action(async (file, opts) => {
|
|
108
|
+
try {
|
|
109
|
+
const res = await globalClient.symbolsIn(file);
|
|
110
|
+
if (opts.json) {
|
|
111
|
+
console.log(JSON.stringify(res, null, 2));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (res.count === 0) {
|
|
115
|
+
console.log(`No symbols found in '${file}'`);
|
|
116
|
+
console.log(` Note: paths are repo-relative (e.g., src/main.rs, not ./src/main.rs)`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.log(`${res.count} symbols in ${file}:`);
|
|
120
|
+
console.log();
|
|
121
|
+
for (const s of res.symbols) {
|
|
122
|
+
const refs = s.use_count > 0 ? ` (${s.use_count} refs)` : "";
|
|
123
|
+
console.log(` L${s.line} ${s.name}${refs}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
handleError(e);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
// ─── impact ────────────────────────────────────────────────────
|
|
131
|
+
program
|
|
132
|
+
.command("impact <symbol>")
|
|
133
|
+
.description("Impact analysis — what breaks if I change this symbol?")
|
|
134
|
+
.option("-n, --top <n>", "Max files to show", "15")
|
|
135
|
+
.option("--json", "Output as JSON")
|
|
136
|
+
.action(async (symbol, opts) => {
|
|
137
|
+
try {
|
|
138
|
+
const res = await globalClient.impact(symbol, parseInt(opts.top));
|
|
139
|
+
if (opts.json) {
|
|
140
|
+
console.log(JSON.stringify(res, null, 2));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (res.total_hits === 0) {
|
|
144
|
+
console.log(`No references found for '${symbol}' — safe to change or unused.`);
|
|
145
|
+
console.log(` Try: foxref-remote search ${symbol}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
console.log(`Impact analysis for '${symbol}':`);
|
|
149
|
+
console.log(` Defined in: ${res.defined_in}:${res.defined_line}`);
|
|
150
|
+
console.log(` Total references: ${res.total_hits}`);
|
|
151
|
+
console.log(` Affected files: ${res.files.length}`);
|
|
152
|
+
console.log();
|
|
153
|
+
for (const f of res.files) {
|
|
154
|
+
const bar = "█".repeat(Math.min(f.hits, 40));
|
|
155
|
+
console.log(` ${f.hits.toString().padStart(4)} ${bar} ${f.file_path}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
handleError(e);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// ─── health ────────────────────────────────────────────────────
|
|
163
|
+
program
|
|
164
|
+
.command("health")
|
|
165
|
+
.description("Check server status and index freshness")
|
|
166
|
+
.option("--json", "Output as JSON")
|
|
167
|
+
.action(async (opts) => {
|
|
168
|
+
try {
|
|
169
|
+
const res = await globalClient.health();
|
|
170
|
+
if (opts.json) {
|
|
171
|
+
console.log(JSON.stringify(res, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const staleWarning = res.stale ? " ⚠ STALE" : "";
|
|
175
|
+
console.log(`FoxRef Server: ${res.status}${staleWarning}`);
|
|
176
|
+
console.log(` Space: ${res.space}`);
|
|
177
|
+
console.log(` Symbols: ${res.symbols.toLocaleString()}`);
|
|
178
|
+
console.log(` Refs: ${res.uses.toLocaleString()}`);
|
|
179
|
+
console.log(` Files: ${res.files.toLocaleString()}`);
|
|
180
|
+
console.log(` Built: ${res.built_at}`);
|
|
181
|
+
console.log(` Age: ${res.index_age_hours.toFixed(1)}h`);
|
|
182
|
+
console.log(` Server: ${process.env.FOXREF_SERVER || "https://foxref.lienly.com"}`);
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
handleError(e);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// ─── init ──────────────────────────────────────────────────────
|
|
189
|
+
program
|
|
190
|
+
.command("init")
|
|
191
|
+
.description("Set up Claude Code integration — hooks, CLAUDE.md, memory")
|
|
192
|
+
.option("--project <dir>", "Project directory", ".")
|
|
193
|
+
.option("--server <url>", "FoxRef server URL", "https://foxref.lienly.com")
|
|
194
|
+
.option("--key <key>", "API key (also reads FOXREF_API_KEY env var)")
|
|
195
|
+
.action(async (opts) => {
|
|
196
|
+
const dir = opts.project === "." ? process.cwd() : opts.project;
|
|
197
|
+
const key = opts.key || process.env.FOXREF_API_KEY;
|
|
198
|
+
console.log(`Setting up foxref-remote in ${dir}...`);
|
|
199
|
+
console.log(` Server: ${opts.server}`);
|
|
200
|
+
if (key)
|
|
201
|
+
console.log(` API key: ${key.slice(0, 8)}...`);
|
|
202
|
+
console.log();
|
|
203
|
+
(0, init_js_1.initProject)(dir, opts.server, key);
|
|
204
|
+
});
|
|
205
|
+
// ─── Error handling ────────────────────────────────────────────
|
|
206
|
+
function handleError(e) {
|
|
207
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
208
|
+
if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED")) {
|
|
209
|
+
console.error(`Error: Cannot reach FoxRef server`);
|
|
210
|
+
console.error(` Server: ${process.env.FOXREF_SERVER || "https://foxref.lienly.com"}`);
|
|
211
|
+
console.error(` Set FOXREF_SERVER env var if using a different server`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.error(`Error: ${msg}`);
|
|
215
|
+
}
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
program.parse();
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FoxRef Remote Client — HTTP client for the foxref API.
|
|
3
|
+
*
|
|
4
|
+
* All code intelligence queries go through this client.
|
|
5
|
+
* Server URL comes from FOXREF_SERVER env var or defaults to foxref.lienly.com.
|
|
6
|
+
*/
|
|
7
|
+
export interface UseHit {
|
|
8
|
+
source_file: string;
|
|
9
|
+
source_line: number;
|
|
10
|
+
source_col: number;
|
|
11
|
+
symbol_name: string;
|
|
12
|
+
defined_in: string;
|
|
13
|
+
defined_line: number;
|
|
14
|
+
is_heuristic: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface WhoUsesResponse {
|
|
17
|
+
symbol: string;
|
|
18
|
+
count: number;
|
|
19
|
+
exact: number;
|
|
20
|
+
heuristic: number;
|
|
21
|
+
hits: UseHit[];
|
|
22
|
+
}
|
|
23
|
+
export interface SearchResult {
|
|
24
|
+
name: string;
|
|
25
|
+
file: string;
|
|
26
|
+
line: number;
|
|
27
|
+
}
|
|
28
|
+
export interface SearchResponse {
|
|
29
|
+
query: string;
|
|
30
|
+
count: number;
|
|
31
|
+
results: SearchResult[];
|
|
32
|
+
}
|
|
33
|
+
export interface SymbolJson {
|
|
34
|
+
name: string;
|
|
35
|
+
line: number;
|
|
36
|
+
use_count: number;
|
|
37
|
+
}
|
|
38
|
+
export interface SymbolsInResponse {
|
|
39
|
+
file: string;
|
|
40
|
+
count: number;
|
|
41
|
+
symbols: SymbolJson[];
|
|
42
|
+
}
|
|
43
|
+
export interface FileImpact {
|
|
44
|
+
file_path: string;
|
|
45
|
+
hits: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ImpactResponse {
|
|
48
|
+
symbol: string;
|
|
49
|
+
defined_in: string;
|
|
50
|
+
defined_line: number;
|
|
51
|
+
total_hits: number;
|
|
52
|
+
files: FileImpact[];
|
|
53
|
+
}
|
|
54
|
+
export interface HealthResponse {
|
|
55
|
+
status: string;
|
|
56
|
+
space: string;
|
|
57
|
+
symbols: number;
|
|
58
|
+
uses: number;
|
|
59
|
+
files: number;
|
|
60
|
+
built_at: string;
|
|
61
|
+
index_age_hours: number;
|
|
62
|
+
stale: boolean;
|
|
63
|
+
}
|
|
64
|
+
export declare class FoxRefClient {
|
|
65
|
+
private baseUrl;
|
|
66
|
+
private apiKey?;
|
|
67
|
+
private branch?;
|
|
68
|
+
constructor(serverUrl?: string, apiKey?: string, branch?: string);
|
|
69
|
+
private request;
|
|
70
|
+
whoUses(symbol: string, limit?: number): Promise<WhoUsesResponse>;
|
|
71
|
+
search(query: string, limit?: number): Promise<SearchResponse>;
|
|
72
|
+
symbolsIn(file: string): Promise<SymbolsInResponse>;
|
|
73
|
+
impact(symbol: string, top?: number): Promise<ImpactResponse>;
|
|
74
|
+
health(): Promise<HealthResponse>;
|
|
75
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FoxRef Remote Client — HTTP client for the foxref API.
|
|
4
|
+
*
|
|
5
|
+
* All code intelligence queries go through this client.
|
|
6
|
+
* Server URL comes from FOXREF_SERVER env var or defaults to foxref.lienly.com.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.FoxRefClient = void 0;
|
|
10
|
+
class FoxRefClient {
|
|
11
|
+
baseUrl;
|
|
12
|
+
apiKey;
|
|
13
|
+
branch;
|
|
14
|
+
constructor(serverUrl, apiKey, branch) {
|
|
15
|
+
this.baseUrl = (serverUrl ||
|
|
16
|
+
process.env.FOXREF_SERVER ||
|
|
17
|
+
"https://foxref.lienly.com").replace(/\/$/, "");
|
|
18
|
+
this.apiKey = apiKey || process.env.FOXREF_API_KEY;
|
|
19
|
+
this.branch = branch || process.env.FOXREF_BRANCH;
|
|
20
|
+
}
|
|
21
|
+
async request(path, params = {}) {
|
|
22
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
23
|
+
for (const [k, v] of Object.entries(params)) {
|
|
24
|
+
url.searchParams.set(k, String(v));
|
|
25
|
+
}
|
|
26
|
+
if (this.branch) {
|
|
27
|
+
url.searchParams.set("branch", this.branch);
|
|
28
|
+
}
|
|
29
|
+
const headers = {};
|
|
30
|
+
if (this.apiKey) {
|
|
31
|
+
headers["x-foxref-key"] = this.apiKey;
|
|
32
|
+
}
|
|
33
|
+
const res = await fetch(url.toString(), { headers });
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
if (res.status === 401)
|
|
36
|
+
throw new Error("Unauthorized — check your FOXREF_API_KEY");
|
|
37
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
return res.json();
|
|
40
|
+
}
|
|
41
|
+
async whoUses(symbol, limit = 20) {
|
|
42
|
+
return this.request("/api/v1/who-uses", { symbol, limit });
|
|
43
|
+
}
|
|
44
|
+
async search(query, limit = 20) {
|
|
45
|
+
return this.request("/api/v1/search", { q: query, limit });
|
|
46
|
+
}
|
|
47
|
+
async symbolsIn(file) {
|
|
48
|
+
return this.request("/api/v1/symbols-in", { file });
|
|
49
|
+
}
|
|
50
|
+
async impact(symbol, top = 15) {
|
|
51
|
+
return this.request("/api/v1/impact", { symbol, top });
|
|
52
|
+
}
|
|
53
|
+
async health() {
|
|
54
|
+
return this.request("/api/v1/health");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.FoxRefClient = FoxRefClient;
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* foxref-remote init — Set up Claude Code integration for an employee's project.
|
|
3
|
+
*
|
|
4
|
+
* Creates:
|
|
5
|
+
* .claude/hooks/foxref-remote-inject.sh — ambient code intelligence on Grep/Glob
|
|
6
|
+
* .claude/settings.json — registers the hook (merges if exists)
|
|
7
|
+
* CLAUDE.md — adds FoxRef section (appends if exists)
|
|
8
|
+
* .claude/memory/foxref-remote.md — memory file for AI context
|
|
9
|
+
*/
|
|
10
|
+
export declare function initProject(projectDir: string, serverUrl?: string, apiKey?: string): void;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* foxref-remote init — Set up Claude Code integration for an employee's project.
|
|
4
|
+
*
|
|
5
|
+
* Creates:
|
|
6
|
+
* .claude/hooks/foxref-remote-inject.sh — ambient code intelligence on Grep/Glob
|
|
7
|
+
* .claude/settings.json — registers the hook (merges if exists)
|
|
8
|
+
* CLAUDE.md — adds FoxRef section (appends if exists)
|
|
9
|
+
* .claude/memory/foxref-remote.md — memory file for AI context
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.initProject = initProject;
|
|
13
|
+
const node_fs_1 = require("node:fs");
|
|
14
|
+
const node_path_1 = require("node:path");
|
|
15
|
+
const HOOK_SCRIPT = `#!/usr/bin/env bash
|
|
16
|
+
# FoxRef Remote Search Injection — ambient code intelligence via foxref.lienly.com
|
|
17
|
+
#
|
|
18
|
+
# PreToolUse:Grep|Glob
|
|
19
|
+
#
|
|
20
|
+
# When an agent searches for something, this hook queries the remote FoxRef
|
|
21
|
+
# server and injects cross-reference context. The agent gets code intelligence
|
|
22
|
+
# without needing to know the API exists.
|
|
23
|
+
#
|
|
24
|
+
# Requires: curl, jq
|
|
25
|
+
# Server: FOXREF_SERVER env var (default: https://foxref.lienly.com)
|
|
26
|
+
|
|
27
|
+
set -uo pipefail
|
|
28
|
+
|
|
29
|
+
FOXREF_SERVER="\${FOXREF_SERVER:-__FOXREF_SERVER__}"
|
|
30
|
+
FOXREF_KEY="\${FOXREF_API_KEY:-__FOXREF_KEY__}"
|
|
31
|
+
MAX_SYMBOLS=3
|
|
32
|
+
RATE_LIMIT_SECONDS=5
|
|
33
|
+
RATE_FILE="/tmp/.foxref-remote-inject-last"
|
|
34
|
+
|
|
35
|
+
# ── Input parsing ───────────────────────────────────────────────
|
|
36
|
+
RAW_INPUT=""
|
|
37
|
+
if [[ -t 0 ]]; then
|
|
38
|
+
RAW_INPUT="\${1:-}"
|
|
39
|
+
else
|
|
40
|
+
RAW_INPUT=$(cat)
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
[[ -z "$RAW_INPUT" ]] && exit 0
|
|
44
|
+
|
|
45
|
+
command -v jq &>/dev/null || exit 0
|
|
46
|
+
command -v curl &>/dev/null || exit 0
|
|
47
|
+
|
|
48
|
+
TOOL_NAME=$(echo "$RAW_INPUT" | jq -r '.tool_name // empty' 2>/dev/null) || exit 0
|
|
49
|
+
if [[ "$TOOL_NAME" != "Grep" ]] && [[ "$TOOL_NAME" != "Glob" ]]; then
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
PATTERN=$(echo "$RAW_INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null) || exit 0
|
|
54
|
+
[[ -z "$PATTERN" ]] && exit 0
|
|
55
|
+
|
|
56
|
+
# ── Extract meaningful text ─────────────────────────────────────
|
|
57
|
+
SEARCH_TERM="$PATTERN"
|
|
58
|
+
SEARCH_TERM="\${SEARCH_TERM//\\*/}"
|
|
59
|
+
SEARCH_TERM="\${SEARCH_TERM//\\?/}"
|
|
60
|
+
SEARCH_TERM="\${SEARCH_TERM//\\^/}"
|
|
61
|
+
SEARCH_TERM="\${SEARCH_TERM//$/}"
|
|
62
|
+
SEARCH_TERM="\${SEARCH_TERM//\\\\b/}"
|
|
63
|
+
SEARCH_TERM="\${SEARCH_TERM//\\\\s+/ }"
|
|
64
|
+
SEARCH_TERM="\${SEARCH_TERM//\\\\w+/}"
|
|
65
|
+
SEARCH_TERM="\${SEARCH_TERM//\\.\\*/}"
|
|
66
|
+
SEARCH_TERM="\${SEARCH_TERM//\\(/}"
|
|
67
|
+
SEARCH_TERM="\${SEARCH_TERM//\\)/}"
|
|
68
|
+
SEARCH_TERM="\${SEARCH_TERM//\\[/}"
|
|
69
|
+
SEARCH_TERM="\${SEARCH_TERM//\\]/}"
|
|
70
|
+
SEARCH_TERM="\${SEARCH_TERM//\\{/}"
|
|
71
|
+
SEARCH_TERM="\${SEARCH_TERM//\\}/}"
|
|
72
|
+
SEARCH_TERM="\${SEARCH_TERM//|/}"
|
|
73
|
+
SEARCH_TERM=$(echo "$SEARCH_TERM" | sed 's|.*/||')
|
|
74
|
+
SEARCH_TERM="\${SEARCH_TERM//\\./}"
|
|
75
|
+
SEARCH_TERM=$(echo "$SEARCH_TERM" | xargs)
|
|
76
|
+
|
|
77
|
+
# Take longest word from multi-word patterns
|
|
78
|
+
if [[ "$SEARCH_TERM" == *" "* ]]; then
|
|
79
|
+
LONGEST=""
|
|
80
|
+
for word in $SEARCH_TERM; do
|
|
81
|
+
if [[ \${#word} -gt \${#LONGEST} ]]; then
|
|
82
|
+
LONGEST="$word"
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
SEARCH_TERM="$LONGEST"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
[[ \${#SEARCH_TERM} -lt 4 ]] && exit 0
|
|
89
|
+
|
|
90
|
+
# Skip common keywords
|
|
91
|
+
case "\${SEARCH_TERM,,}" in
|
|
92
|
+
import|require|function|class|struct|enum|trait|interface|const|type|return|async|await|pub|fn|let|mut|self|impl|use|mod|crate)
|
|
93
|
+
exit 0 ;;
|
|
94
|
+
esac
|
|
95
|
+
|
|
96
|
+
# ── Rate limiting ───────────────────────────────────────────────
|
|
97
|
+
NOW=$(date +%s)
|
|
98
|
+
if [[ -f "$RATE_FILE" ]]; then
|
|
99
|
+
LAST=$(cat "$RATE_FILE" 2>/dev/null || echo "0")
|
|
100
|
+
if (( NOW - LAST < RATE_LIMIT_SECONDS )); then
|
|
101
|
+
exit 0
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# ── Query remote FoxRef ─────────────────────────────────────────
|
|
106
|
+
CURL_ARGS=(-s --max-time 3)
|
|
107
|
+
if [[ -n "$FOXREF_KEY" ]]; then
|
|
108
|
+
CURL_ARGS+=(-H "X-FoxRef-Key: $FOXREF_KEY")
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
SEARCH_OUTPUT=$(curl "\${CURL_ARGS[@]}" "\${FOXREF_SERVER}/api/v1/search?q=\${SEARCH_TERM}&limit=\${MAX_SYMBOLS}" 2>/dev/null) || exit 0
|
|
112
|
+
[[ -z "$SEARCH_OUTPUT" ]] && exit 0
|
|
113
|
+
|
|
114
|
+
TOTAL=$(echo "$SEARCH_OUTPUT" | jq -r '.count // 0' 2>/dev/null)
|
|
115
|
+
[[ "$TOTAL" == "0" ]] && exit 0
|
|
116
|
+
|
|
117
|
+
echo "$NOW" > "$RATE_FILE" 2>/dev/null || true
|
|
118
|
+
|
|
119
|
+
# Build context
|
|
120
|
+
CONTEXT="FOXREF_REMOTE: Pattern '\${SEARCH_TERM}' matches \${TOTAL} indexed symbol(s).\\n"
|
|
121
|
+
|
|
122
|
+
# Add top matches
|
|
123
|
+
echo "$SEARCH_OUTPUT" | jq -r '.results[] | " \\(.name) (\\(.file):\\(.line))"' 2>/dev/null | while IFS= read -r line; do
|
|
124
|
+
CONTEXT="\${CONTEXT}\${line}\\n"
|
|
125
|
+
done
|
|
126
|
+
|
|
127
|
+
# Get who-uses count for top match
|
|
128
|
+
TOP_SYMBOL=$(echo "$SEARCH_OUTPUT" | jq -r '.results[0].name // empty' 2>/dev/null)
|
|
129
|
+
if [[ -n "$TOP_SYMBOL" ]]; then
|
|
130
|
+
USES_OUTPUT=$(curl "\${CURL_ARGS[@]}" "\${FOXREF_SERVER}/api/v1/who-uses?symbol=\${TOP_SYMBOL}&limit=3" 2>/dev/null) || true
|
|
131
|
+
if [[ -n "$USES_OUTPUT" ]]; then
|
|
132
|
+
USES_COUNT=$(echo "$USES_OUTPUT" | jq -r '.count // 0' 2>/dev/null)
|
|
133
|
+
if [[ "$USES_COUNT" != "0" ]]; then
|
|
134
|
+
CONTEXT="\${CONTEXT}Top symbol '\${TOP_SYMBOL}' has \${USES_COUNT} cross-references.\\n"
|
|
135
|
+
echo "$USES_OUTPUT" | jq -r '.hits[:3][] | " [\\(if .is_heuristic then "heuristic" else "exact" end)] \\(.source_file):\\(.source_line)"' 2>/dev/null | while IFS= read -r line; do
|
|
136
|
+
CONTEXT="\${CONTEXT} \${line}\\n"
|
|
137
|
+
done
|
|
138
|
+
CONTEXT="\${CONTEXT}Run: foxref-remote who-uses \${TOP_SYMBOL} | foxref-remote impact \${TOP_SYMBOL}"
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# ── Output ──────────────────────────────────────────────────────
|
|
144
|
+
ESCAPED_CONTEXT=$(printf '%s' "$CONTEXT" | jq -Rs '.')
|
|
145
|
+
|
|
146
|
+
cat << EOF
|
|
147
|
+
{
|
|
148
|
+
"hookSpecificOutput": {
|
|
149
|
+
"hookEventName": "PreToolUse",
|
|
150
|
+
"additionalContext": $ESCAPED_CONTEXT
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
EOF
|
|
154
|
+
`;
|
|
155
|
+
const CLAUDE_MD_SECTION = `
|
|
156
|
+
## FoxRef Remote — Code Intelligence
|
|
157
|
+
|
|
158
|
+
Your AI has access to a centrally-maintained code intelligence index via \`foxref-remote\`.
|
|
159
|
+
|
|
160
|
+
### Available Commands
|
|
161
|
+
|
|
162
|
+
| Command | What it does |
|
|
163
|
+
|---------|-------------|
|
|
164
|
+
| \`foxref-remote who-uses <Symbol>\` | Find all usages of a symbol |
|
|
165
|
+
| \`foxref-remote search <query>\` | Fuzzy search for symbols by name |
|
|
166
|
+
| \`foxref-remote symbols-in <file>\` | List symbols defined in a file |
|
|
167
|
+
| \`foxref-remote impact <Symbol>\` | What breaks if I change this? |
|
|
168
|
+
| \`foxref-remote health\` | Check index status and freshness |
|
|
169
|
+
|
|
170
|
+
All commands support \`--json\` for machine-readable output.
|
|
171
|
+
|
|
172
|
+
### Ambient Intelligence
|
|
173
|
+
|
|
174
|
+
A hook automatically injects cross-reference context when you search with Grep or Glob.
|
|
175
|
+
You don't need to call foxref-remote explicitly — it enriches your searches automatically.
|
|
176
|
+
|
|
177
|
+
### Tips
|
|
178
|
+
|
|
179
|
+
- Use \`foxref-remote search\` before refactoring to understand symbol scope
|
|
180
|
+
- Use \`foxref-remote impact\` before changing a function to see blast radius
|
|
181
|
+
- All data comes from a centrally-maintained index that updates on every push
|
|
182
|
+
`;
|
|
183
|
+
const MEMORY_CONTENT = `---
|
|
184
|
+
name: foxref-remote
|
|
185
|
+
description: Code intelligence server at foxref.lienly.com — symbol search, cross-references, impact analysis
|
|
186
|
+
type: reference
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## FoxRef Remote Server
|
|
190
|
+
|
|
191
|
+
- **URL:** https://foxref.lienly.com
|
|
192
|
+
- **What:** Centrally-maintained code intelligence index for the Lienly codebase
|
|
193
|
+
- **Commands:** foxref-remote who-uses, search, symbols-in, impact, health
|
|
194
|
+
- **Hook:** .claude/hooks/foxref-remote-inject.sh auto-injects context on Grep/Glob
|
|
195
|
+
- **Updated:** Index refreshes automatically when code is pushed to GitHub
|
|
196
|
+
- **Note:** All commands support --json flag for structured output
|
|
197
|
+
`;
|
|
198
|
+
function initProject(projectDir, serverUrl = "https://foxref.lienly.com", apiKey) {
|
|
199
|
+
const claudeDir = (0, node_path_1.join)(projectDir, ".claude");
|
|
200
|
+
const hooksDir = (0, node_path_1.join)(claudeDir, "hooks");
|
|
201
|
+
const memoryDir = (0, node_path_1.join)(claudeDir, "memory");
|
|
202
|
+
const settingsPath = (0, node_path_1.join)(claudeDir, "settings.json");
|
|
203
|
+
const claudeMdPath = (0, node_path_1.join)(projectDir, "CLAUDE.md");
|
|
204
|
+
const hookPath = (0, node_path_1.join)(hooksDir, "foxref-remote-inject.sh");
|
|
205
|
+
const memoryPath = (0, node_path_1.join)(memoryDir, "foxref-remote.md");
|
|
206
|
+
// Ensure directories exist
|
|
207
|
+
(0, node_fs_1.mkdirSync)(hooksDir, { recursive: true });
|
|
208
|
+
(0, node_fs_1.mkdirSync)(memoryDir, { recursive: true });
|
|
209
|
+
// 1. Write the hook script (with server URL and API key baked in)
|
|
210
|
+
let hookContent = HOOK_SCRIPT
|
|
211
|
+
.replace("__FOXREF_SERVER__", serverUrl)
|
|
212
|
+
.replace("__FOXREF_KEY__", apiKey || "");
|
|
213
|
+
(0, node_fs_1.writeFileSync)(hookPath, hookContent, { mode: 0o755 });
|
|
214
|
+
console.log(` Created: ${hookPath}`);
|
|
215
|
+
// 2. Update .claude/settings.json
|
|
216
|
+
let settings = {};
|
|
217
|
+
if ((0, node_fs_1.existsSync)(settingsPath)) {
|
|
218
|
+
try {
|
|
219
|
+
settings = JSON.parse((0, node_fs_1.readFileSync)(settingsPath, "utf-8"));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Start fresh if corrupted
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Ensure hooks array exists
|
|
226
|
+
if (!Array.isArray(settings.hooks)) {
|
|
227
|
+
settings.hooks = [];
|
|
228
|
+
}
|
|
229
|
+
const hooks = settings.hooks;
|
|
230
|
+
const hookExists = hooks.some((h) => h.command && String(h.command).includes("foxref-remote-inject"));
|
|
231
|
+
if (!hookExists) {
|
|
232
|
+
hooks.push({
|
|
233
|
+
matcher: "Grep|Glob",
|
|
234
|
+
hooks: [
|
|
235
|
+
{
|
|
236
|
+
type: "command",
|
|
237
|
+
command: hookPath,
|
|
238
|
+
event: "PreToolUse",
|
|
239
|
+
timeout: 5000,
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
});
|
|
243
|
+
(0, node_fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
244
|
+
console.log(` Updated: ${settingsPath} (added foxref hook)`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.log(` Skipped: ${settingsPath} (hook already registered)`);
|
|
248
|
+
}
|
|
249
|
+
// 3. Append to CLAUDE.md
|
|
250
|
+
if ((0, node_fs_1.existsSync)(claudeMdPath)) {
|
|
251
|
+
const existing = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
|
|
252
|
+
if (existing.includes("foxref-remote") || existing.includes("FoxRef Remote")) {
|
|
253
|
+
console.log(` Skipped: ${claudeMdPath} (FoxRef section already exists)`);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
(0, node_fs_1.writeFileSync)(claudeMdPath, existing + "\n" + CLAUDE_MD_SECTION);
|
|
257
|
+
console.log(` Updated: ${claudeMdPath} (appended FoxRef section)`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
(0, node_fs_1.writeFileSync)(claudeMdPath, `# Project Instructions\n${CLAUDE_MD_SECTION}`);
|
|
262
|
+
console.log(` Created: ${claudeMdPath}`);
|
|
263
|
+
}
|
|
264
|
+
// 4. Write memory file
|
|
265
|
+
(0, node_fs_1.writeFileSync)(memoryPath, MEMORY_CONTENT);
|
|
266
|
+
console.log(` Created: ${memoryPath}`);
|
|
267
|
+
console.log();
|
|
268
|
+
console.log("Done! foxref-remote is ready.");
|
|
269
|
+
console.log();
|
|
270
|
+
console.log("Test it:");
|
|
271
|
+
console.log(" foxref-remote health");
|
|
272
|
+
console.log(" foxref-remote search MyFunction");
|
|
273
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "foxref-remote",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Code intelligence CLI — search symbols, find references, analyze impact across the Lienly codebase",
|
|
5
|
+
"bin": {
|
|
6
|
+
"foxref-remote": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsx src/cli.ts"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"commander": "^12.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.4.0",
|
|
20
|
+
"tsx": "^4.0.0",
|
|
21
|
+
"@types/node": "^20.0.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Lienly <dev@lienly.com>"
|
|
28
|
+
}
|