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 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();
@@ -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
+ }