skillrepo 1.6.2 → 1.7.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/package.json +1 -1
- package/src/commands/init.mjs +29 -13
- package/src/hooks/skillrepo-prompt-match.mjs +344 -0
- package/src/hooks/skillrepo-sync.mjs +769 -0
- package/src/lib/first-sync.mjs +51 -0
- package/src/lib/mergers/gitignore.mjs +39 -0
- package/src/lib/mergers/hooks-json.mjs +107 -57
- package/src/lib/paths.mjs +10 -0
- package/src/lib/write-configs.mjs +151 -71
- package/src/test/detect-ides.test.mjs +1 -1
- package/src/test/e2e/cli-init.test.mjs +37 -928
- package/src/test/e2e/mock-server.mjs +13 -0
- package/src/test/e2e/payload-factory.mjs +0 -212
- package/src/test/hooks/detect-migration.test.mjs +93 -0
- package/src/test/hooks/skillrepo-prompt-match.test.mjs +357 -0
- package/src/test/hooks/skillrepo-sync.test.mjs +1036 -0
- package/src/test/mergers/gitignore.test.mjs +63 -0
- package/src/test/mergers/hooks-json.test.mjs +106 -71
package/package.json
CHANGED
package/src/commands/init.mjs
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
* Flow:
|
|
5
5
|
* 1. Detect IDEs
|
|
6
6
|
* 2. Prompt for access key
|
|
7
|
-
* 3. Validate key + fetch skill
|
|
8
|
-
* 4. Write
|
|
9
|
-
* 5.
|
|
7
|
+
* 3. Validate key + fetch skill count
|
|
8
|
+
* 4. Write configs (global config, hooks, MCP, .gitignore)
|
|
9
|
+
* 5. Run first sync (writes rules files + local cache)
|
|
10
|
+
* 6. Print summary
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { detectIdes, formatDetectedIdes, getDetectedIdeKeys } from "../lib/detect-ides.mjs";
|
|
13
14
|
import { fetchSetupPayload, AuthError, SuspendedError, NetworkError } from "../lib/http.mjs";
|
|
14
15
|
import { writeAllConfigs } from "../lib/write-configs.mjs";
|
|
16
|
+
import { runFirstSync } from "../lib/first-sync.mjs";
|
|
15
17
|
import {
|
|
16
18
|
printHeader,
|
|
17
19
|
printStep,
|
|
@@ -66,7 +68,7 @@ export async function runInit(argv) {
|
|
|
66
68
|
printHeader("SkillRepo Setup");
|
|
67
69
|
|
|
68
70
|
// ── Step 1: Detect IDEs ───────────────────────────────────────────────
|
|
69
|
-
printStep(1,
|
|
71
|
+
printStep(1, 5, "Detecting IDEs...");
|
|
70
72
|
|
|
71
73
|
const detected = detectIdes();
|
|
72
74
|
const ideList = formatDetectedIdes(detected);
|
|
@@ -95,7 +97,7 @@ export async function runInit(argv) {
|
|
|
95
97
|
printBlank();
|
|
96
98
|
|
|
97
99
|
// ── Step 2: Access Key ────────────────────────────────────────────────
|
|
98
|
-
printStep(2,
|
|
100
|
+
printStep(2, 5, "Access Key");
|
|
99
101
|
|
|
100
102
|
let apiKey = flags.key;
|
|
101
103
|
if (!apiKey) {
|
|
@@ -111,7 +113,7 @@ export async function runInit(argv) {
|
|
|
111
113
|
printBlank();
|
|
112
114
|
|
|
113
115
|
// ── Step 3: Validate + Fetch ──────────────────────────────────────────
|
|
114
|
-
printStep(3,
|
|
116
|
+
printStep(3, 5, "Validating key...");
|
|
115
117
|
|
|
116
118
|
let payload;
|
|
117
119
|
try {
|
|
@@ -135,13 +137,13 @@ export async function runInit(argv) {
|
|
|
135
137
|
printSuccess(`Valid. ${payload.skillCount} skills in your library.`);
|
|
136
138
|
|
|
137
139
|
if (payload.skillCount === 0) {
|
|
138
|
-
printWarning("No skills in your library yet.
|
|
140
|
+
printWarning("No skills in your library yet. Config will be written — skills will activate when you add them.");
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
printBlank();
|
|
142
144
|
|
|
143
145
|
// ── Step 4: Write configs ─────────────────────────────────────────────
|
|
144
|
-
printStep(4,
|
|
146
|
+
printStep(4, 5, "Writing configuration...");
|
|
145
147
|
printBlank();
|
|
146
148
|
|
|
147
149
|
const mcpUrl = `${flags.url}/api/mcp`;
|
|
@@ -152,7 +154,7 @@ export async function runInit(argv) {
|
|
|
152
154
|
ides: detectedKeys,
|
|
153
155
|
mcpUrl,
|
|
154
156
|
apiKey,
|
|
155
|
-
|
|
157
|
+
serverUrl: flags.url,
|
|
156
158
|
});
|
|
157
159
|
} catch (err) {
|
|
158
160
|
printError(err.message);
|
|
@@ -163,6 +165,19 @@ export async function runInit(argv) {
|
|
|
163
165
|
printResult(r.path, r.action);
|
|
164
166
|
}
|
|
165
167
|
|
|
168
|
+
printBlank();
|
|
169
|
+
|
|
170
|
+
// ── Step 5: First sync ────────────────────────────────────────────────
|
|
171
|
+
printStep(5, 5, "Running first sync...");
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await runFirstSync();
|
|
175
|
+
printSuccess("Skills synced and rules files written.");
|
|
176
|
+
} catch (err) {
|
|
177
|
+
printWarning(`First sync failed: ${err.message}`);
|
|
178
|
+
printWarning("Skills will sync automatically on your next session start.");
|
|
179
|
+
}
|
|
180
|
+
|
|
166
181
|
// ── Summary ───────────────────────────────────────────────────────────
|
|
167
182
|
printBlank();
|
|
168
183
|
printSuccess("SkillRepo is ready.");
|
|
@@ -172,14 +187,15 @@ export async function runInit(argv) {
|
|
|
172
187
|
console.log(" • Each team member runs: npx skillrepo init");
|
|
173
188
|
console.log(" • SKILLREPO_ACCESS_KEY is in .env.local (gitignored)");
|
|
174
189
|
|
|
175
|
-
if (detectedKeys.includes("
|
|
190
|
+
if (detectedKeys.includes("claudeCode")) {
|
|
176
191
|
printBlank();
|
|
177
|
-
console.log("
|
|
192
|
+
console.log(" Claude Code: Skills are delivered via .claude/rules/ files.");
|
|
193
|
+
console.log(" They refresh automatically on each session start.");
|
|
178
194
|
}
|
|
179
195
|
|
|
180
|
-
if (detectedKeys.includes("
|
|
196
|
+
if (detectedKeys.includes("vscode")) {
|
|
181
197
|
printBlank();
|
|
182
|
-
console.log("
|
|
198
|
+
console.log(" Note: VS Code will prompt for your access key on first use.");
|
|
183
199
|
}
|
|
184
200
|
|
|
185
201
|
printBlank();
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SkillRepo UserPromptSubmit hook — matches prompts against the local skill
|
|
4
|
+
* index and reports telemetry. No content injection.
|
|
5
|
+
*
|
|
6
|
+
* Standalone script: no npm dependencies, Node.js built-ins only.
|
|
7
|
+
* Installed by `npx skillrepo init` to `.claude/hooks/skillrepo-prompt-match.mjs`.
|
|
8
|
+
*
|
|
9
|
+
* Part of #532 (Re-architect skill delivery, Phase B).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { homedir, tmpdir } from "node:os";
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const DEFAULT_SERVER_URL = "https://skillrepo.dev";
|
|
24
|
+
const MAX_EVENTS_PER_BATCH = 50;
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Config resolution (lightweight — only needs API key and server URL)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read config from ~/.claude/skillrepo/config.json with fallbacks.
|
|
32
|
+
* Returns { apiKey, serverUrl } or null.
|
|
33
|
+
*/
|
|
34
|
+
export function readConfig() {
|
|
35
|
+
const configPath = join(homedir(), ".claude", "skillrepo", "config.json");
|
|
36
|
+
try {
|
|
37
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
38
|
+
if (cfg.apiKey) {
|
|
39
|
+
return { apiKey: cfg.apiKey, serverUrl: cfg.serverUrl || DEFAULT_SERVER_URL };
|
|
40
|
+
}
|
|
41
|
+
} catch { /* not found */ }
|
|
42
|
+
|
|
43
|
+
const envKey = process.env.SKILLREPO_ACCESS_KEY;
|
|
44
|
+
if (envKey) {
|
|
45
|
+
return { apiKey: envKey, serverUrl: process.env.SKILLREPO_SERVER_URL || DEFAULT_SERVER_URL };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// .env.local fallback
|
|
49
|
+
for (const file of [".env.local", ".env"]) {
|
|
50
|
+
try {
|
|
51
|
+
const lines = readFileSync(join(process.cwd(), file), "utf-8").split("\n");
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
const trimmed = line.trim();
|
|
54
|
+
if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
55
|
+
const eqIdx = trimmed.indexOf("=");
|
|
56
|
+
if (trimmed.slice(0, eqIdx).trim() === "SKILLREPO_ACCESS_KEY") {
|
|
57
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
58
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
59
|
+
val = val.slice(1, -1);
|
|
60
|
+
}
|
|
61
|
+
val = val.replace(/\s+#.*$/, "");
|
|
62
|
+
if (val) return { apiKey: val, serverUrl: DEFAULT_SERVER_URL };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch { /* file doesn't exist */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Skill matching
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
const STOP_WORDS = new Set([
|
|
76
|
+
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
|
|
77
|
+
"of", "with", "by", "is", "are", "was", "were", "be", "been",
|
|
78
|
+
"this", "that", "it", "its", "as", "if", "not", "no", "do", "does",
|
|
79
|
+
"can", "will", "use", "when", "how", "what", "which", "who", "any",
|
|
80
|
+
"all", "your", "you", "their", "they", "has", "have", "had",
|
|
81
|
+
"i", "me", "my", "we", "our",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Match a prompt against all skills in the index.
|
|
86
|
+
* Returns scored matches sorted by relevance (highest first).
|
|
87
|
+
*/
|
|
88
|
+
export function matchSkills(prompt, skills) {
|
|
89
|
+
if (!prompt || !skills?.length) return [];
|
|
90
|
+
|
|
91
|
+
const lower = prompt.toLowerCase();
|
|
92
|
+
const tokens = new Set(
|
|
93
|
+
lower.replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter(w => w.length > 2 && !STOP_WORDS.has(w))
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const matches = [];
|
|
97
|
+
|
|
98
|
+
for (const skill of skills) {
|
|
99
|
+
let score = 0;
|
|
100
|
+
|
|
101
|
+
// High-weight: contextSignals.tasks phrase matching (+3 per match)
|
|
102
|
+
const tasks = skill.contextSignals?.tasks ?? [];
|
|
103
|
+
for (const task of tasks) {
|
|
104
|
+
if (lower.includes(task.toLowerCase())) score += 3;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Standard-weight: keyword token matching
|
|
108
|
+
for (const kw of (skill.keywords ?? [])) {
|
|
109
|
+
const kwLower = kw.toLowerCase();
|
|
110
|
+
if (tokens.has(kwLower)) {
|
|
111
|
+
score += 1;
|
|
112
|
+
} else if (kwLower.length > 3 && lower.includes(kwLower)) {
|
|
113
|
+
score += 0.5;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Name-based matching: skill name parts
|
|
118
|
+
for (const part of skill.name.split("-")) {
|
|
119
|
+
if (part.length > 2 && tokens.has(part.toLowerCase())) score += 0.5;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Description-based matching: key description words
|
|
123
|
+
if (skill.description) {
|
|
124
|
+
const descWords = skill.description.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/)
|
|
125
|
+
.filter(w => w.length > 3 && !STOP_WORDS.has(w));
|
|
126
|
+
for (const word of descWords) {
|
|
127
|
+
if (tokens.has(word)) score += 0.3;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (score > 0) matches.push({ skill, score });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return matches.sort((a, b) => b.score - a.score);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Session dedup
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Read session state for deduplication.
|
|
143
|
+
* State is stored in tmpdir keyed by session hash.
|
|
144
|
+
*/
|
|
145
|
+
export function readSessionState(sessionId) {
|
|
146
|
+
const hash = createHash("sha256").update(sessionId || "default").digest("hex").slice(0, 16);
|
|
147
|
+
const statePath = join(tmpdir(), `skillrepo-match-${hash}.json`);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
return { path: statePath, state: JSON.parse(readFileSync(statePath, "utf-8")) };
|
|
151
|
+
} catch {
|
|
152
|
+
return { path: statePath, state: { reported: {} } };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Filter matches to exclude skills already reported in this session.
|
|
158
|
+
*/
|
|
159
|
+
export function deduplicateMatches(matches, sessionState) {
|
|
160
|
+
return matches.filter(m => {
|
|
161
|
+
const key = `${m.skill.owner}/${m.skill.name}`;
|
|
162
|
+
return !sessionState.reported[key];
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Mark skills as reported in session state.
|
|
168
|
+
*/
|
|
169
|
+
export function updateSessionState(statePath, state, reportedMatches) {
|
|
170
|
+
for (const m of reportedMatches) {
|
|
171
|
+
const key = `${m.skill.owner}/${m.skill.name}`;
|
|
172
|
+
state.reported[key] = new Date().toISOString();
|
|
173
|
+
}
|
|
174
|
+
try { writeFileSync(statePath, JSON.stringify(state), "utf-8"); }
|
|
175
|
+
catch { /* non-critical */ }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// GitHub username resolution
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
const GITHUB_USERNAME_CACHE_PATH = join(tmpdir(), "skillrepo-gh-user.json");
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Resolve GitHub username via `gh api user`. Cached per session.
|
|
186
|
+
*/
|
|
187
|
+
export function resolveGithubUsername() {
|
|
188
|
+
// Check cache first
|
|
189
|
+
try {
|
|
190
|
+
const cached = JSON.parse(readFileSync(GITHUB_USERNAME_CACHE_PATH, "utf-8"));
|
|
191
|
+
if (cached.username && cached.expiresAt > Date.now()) return cached.username;
|
|
192
|
+
} catch { /* no cache */ }
|
|
193
|
+
|
|
194
|
+
// Resolve via gh CLI
|
|
195
|
+
try {
|
|
196
|
+
const result = execSync("gh api user --jq .login", {
|
|
197
|
+
encoding: "utf-8",
|
|
198
|
+
timeout: 3_000,
|
|
199
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
200
|
+
}).trim();
|
|
201
|
+
|
|
202
|
+
if (result) {
|
|
203
|
+
// Cache for 1 hour
|
|
204
|
+
try {
|
|
205
|
+
writeFileSync(GITHUB_USERNAME_CACHE_PATH, JSON.stringify({
|
|
206
|
+
username: result,
|
|
207
|
+
expiresAt: Date.now() + 3_600_000,
|
|
208
|
+
}), "utf-8");
|
|
209
|
+
} catch { /* non-critical */ }
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
} catch { /* gh not installed or not authed */ }
|
|
213
|
+
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Telemetry payload
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Build telemetry payload for matched skills.
|
|
223
|
+
*/
|
|
224
|
+
export function buildTelemetryPayload(matches, sessionInfo) {
|
|
225
|
+
const events = matches.slice(0, MAX_EVENTS_PER_BATCH).map(m => ({
|
|
226
|
+
skillOwner: m.skill.owner,
|
|
227
|
+
skillName: m.skill.name,
|
|
228
|
+
skillVersion: m.skill.version ?? "",
|
|
229
|
+
matchedAt: new Date().toISOString(),
|
|
230
|
+
ide: sessionInfo.ide || "claude-code",
|
|
231
|
+
sessionHash: sessionInfo.sessionHash,
|
|
232
|
+
githubUsername: sessionInfo.githubUsername || "",
|
|
233
|
+
wasRulesDelivered: m.skill.isRulesDelivered ?? false,
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
return { events };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Fire-and-forget POST to telemetry endpoint.
|
|
241
|
+
*
|
|
242
|
+
* NOT awaited — the spec requires "do NOT block the agent" and Claude Code
|
|
243
|
+
* waits for hook process exit. The OS TCP stack flushes small payloads after
|
|
244
|
+
* process exit, so delivery reliability is ~99%+ for responsive servers.
|
|
245
|
+
* Occasional loss on slow/unreachable servers is acceptable for non-critical
|
|
246
|
+
* telemetry.
|
|
247
|
+
*/
|
|
248
|
+
export function sendTelemetry(config, payload) {
|
|
249
|
+
const url = `${config.serverUrl}/api/v1/telemetry/match`;
|
|
250
|
+
|
|
251
|
+
fetch(url, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: {
|
|
254
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
255
|
+
"Content-Type": "application/json",
|
|
256
|
+
},
|
|
257
|
+
body: JSON.stringify(payload),
|
|
258
|
+
}).catch(() => { /* telemetry errors are non-critical */ });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Main entry point
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
export async function main(input) {
|
|
266
|
+
const indexPath = join(homedir(), ".claude", "skillrepo", "index.json");
|
|
267
|
+
|
|
268
|
+
// ── Read index ──────────────────────────────────────────────
|
|
269
|
+
let index;
|
|
270
|
+
try {
|
|
271
|
+
index = JSON.parse(readFileSync(indexPath, "utf-8"));
|
|
272
|
+
} catch {
|
|
273
|
+
// No index → nothing to match. Exit cleanly.
|
|
274
|
+
process.stdout.write("{}");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!index.skills?.length) {
|
|
279
|
+
process.stdout.write("{}");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Read config (needed for telemetry POST) ─────────────────
|
|
284
|
+
const config = readConfig();
|
|
285
|
+
if (!config) {
|
|
286
|
+
process.stdout.write("{}");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Match prompt against skills ─────────────────────────────
|
|
291
|
+
const prompt = input.prompt ?? "";
|
|
292
|
+
const matches = matchSkills(prompt, index.skills);
|
|
293
|
+
|
|
294
|
+
if (matches.length === 0) {
|
|
295
|
+
process.stdout.write("{}");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── Session dedup ───────────────────────────────────────────
|
|
300
|
+
const sessionId = input.session_id ?? "default";
|
|
301
|
+
const { path: statePath, state } = readSessionState(sessionId);
|
|
302
|
+
const newMatches = deduplicateMatches(matches, state);
|
|
303
|
+
|
|
304
|
+
if (newMatches.length === 0) {
|
|
305
|
+
process.stdout.write("{}");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Build and send telemetry ────────────────────────────────
|
|
310
|
+
const sessionHash = createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
|
|
311
|
+
const githubUsername = resolveGithubUsername();
|
|
312
|
+
|
|
313
|
+
const payload = buildTelemetryPayload(newMatches, {
|
|
314
|
+
ide: "claude-code",
|
|
315
|
+
sessionHash,
|
|
316
|
+
githubUsername,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
sendTelemetry(config, payload); // fire-and-forget — do NOT await
|
|
320
|
+
|
|
321
|
+
// ── Update session state ────────────────────────────────────
|
|
322
|
+
updateSessionState(statePath, state, newMatches);
|
|
323
|
+
|
|
324
|
+
// ── Output: NO additionalContext ────────────────────────────
|
|
325
|
+
process.stdout.write("{}");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Run ───────────────────────────────────────────────────────
|
|
329
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
330
|
+
const isMainModule = process.argv[1] === __filename;
|
|
331
|
+
|
|
332
|
+
if (isMainModule) {
|
|
333
|
+
let inputBuf = "";
|
|
334
|
+
for await (const chunk of process.stdin) inputBuf += chunk;
|
|
335
|
+
|
|
336
|
+
let input;
|
|
337
|
+
try { input = JSON.parse(inputBuf); }
|
|
338
|
+
catch { process.stdout.write("{}"); process.exit(0); }
|
|
339
|
+
|
|
340
|
+
main(input).catch(() => {
|
|
341
|
+
process.stdout.write("{}");
|
|
342
|
+
process.exit(0);
|
|
343
|
+
});
|
|
344
|
+
}
|