@shrkcrft/cli 0.1.0-alpha.11 → 0.1.0-alpha.12
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/commands/ask.command.d.ts.map +1 -1
- package/dist/commands/ask.command.js +10 -9
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +100 -1
- package/dist/commands/deps-audit.command.d.ts +23 -0
- package/dist/commands/deps-audit.command.d.ts.map +1 -0
- package/dist/commands/deps-audit.command.js +266 -0
- package/dist/commands/doctor.command.d.ts.map +1 -1
- package/dist/commands/doctor.command.js +60 -1
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +144 -26
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +3 -2
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +22 -1
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +3 -2
- package/dist/commands/move-plan.command.d.ts +23 -0
- package/dist/commands/move-plan.command.d.ts.map +1 -0
- package/dist/commands/move-plan.command.js +360 -0
- package/dist/commands/scaffold-validate.command.d.ts +22 -0
- package/dist/commands/scaffold-validate.command.d.ts.map +1 -0
- package/dist/commands/scaffold-validate.command.js +215 -0
- package/dist/commands/smart-context.command.d.ts +30 -0
- package/dist/commands/smart-context.command.d.ts.map +1 -0
- package/dist/commands/smart-context.command.js +3763 -0
- package/dist/commands/spike.command.d.ts +22 -0
- package/dist/commands/spike.command.d.ts.map +1 -0
- package/dist/commands/spike.command.js +235 -0
- package/dist/commands/watch.command.d.ts +26 -0
- package/dist/commands/watch.command.d.ts.map +1 -0
- package/dist/commands/watch.command.js +456 -0
- package/dist/env/load-dotenv.d.ts +15 -0
- package/dist/env/load-dotenv.d.ts.map +1 -0
- package/dist/env/load-dotenv.js +70 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +83 -2
- package/dist/schemas/json-schemas.d.ts +384 -36
- package/dist/schemas/json-schemas.d.ts.map +1 -1
- package/dist/schemas/json-schemas.js +247 -36
- package/package.json +33 -31
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, watch as fsWatch, writeFileSync, } from 'node:fs';
|
|
3
|
+
import * as nodePath from 'node:path';
|
|
4
|
+
import { buildTaskPacket, inspectSharkcraft } from '@shrkcrft/inspector';
|
|
5
|
+
import { SemanticIndex, buildFocusedContext, classifyTask, parseTaskTypeOverride, } from '@shrkcrft/embeddings';
|
|
6
|
+
import { flagBool, flagNumber, flagString, resolveCwd, } from "../command-registry.js";
|
|
7
|
+
import { asJson } from "../output/format-output.js";
|
|
8
|
+
const FEED_DIR = nodePath.join('.sharkcraft', 'feed');
|
|
9
|
+
const DEFAULT_DEBOUNCE_MS = 750;
|
|
10
|
+
const DEFAULT_INTERVAL_MS = 0; // off by default; --interval to enable
|
|
11
|
+
const MANIFEST_SCHEMA = 'sharkcraft.shrk-watch-manifest/v1';
|
|
12
|
+
function manifestPath(cwd, slug) {
|
|
13
|
+
return nodePath.join(cwd, FEED_DIR, `${slug}.pid.json`);
|
|
14
|
+
}
|
|
15
|
+
function readManifest(path) {
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const m = JSON.parse(readFileSync(path, 'utf8'));
|
|
20
|
+
if (m.schema !== MANIFEST_SCHEMA || typeof m.pid !== 'number')
|
|
21
|
+
return null;
|
|
22
|
+
return m;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isProcessAlive(pid) {
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, 0);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `shrk watch "<task>"` — emit a fresh task-focused context bundle on
|
|
39
|
+
* stdout JSONL each time the workspace changes.
|
|
40
|
+
*
|
|
41
|
+
* Designed to run in a terminal *next to* Claude Code so the agent has
|
|
42
|
+
* a continuously-refreshed "feed" of the most relevant code for the
|
|
43
|
+
* current task — no extra LLM calls, just BGE-ranked deltas.
|
|
44
|
+
*
|
|
45
|
+
* Surfaces:
|
|
46
|
+
* - stdout: one JSON line per packet, NDJSON-style.
|
|
47
|
+
* - filesystem: same packet appended to
|
|
48
|
+
* `.sharkcraft/feed/<slug>.jsonl` so other tools (or the user)
|
|
49
|
+
* can tail it from elsewhere.
|
|
50
|
+
*
|
|
51
|
+
* Cost: every emission is O(BGE-search + re-rank) — typically
|
|
52
|
+
* 100–300 ms for ~10 candidate files. We never call a generative LLM.
|
|
53
|
+
*/
|
|
54
|
+
export const watchCommand = {
|
|
55
|
+
name: 'watch',
|
|
56
|
+
description: 'Emit a focused-context packet on stdout JSONL each time the workspace changes (or every --interval seconds). No LLM calls.',
|
|
57
|
+
usage: 'shrk watch "<task>" [--interval N] [--debounce ms] [--task-type <type>] [--once] [--quiet] [--json]',
|
|
58
|
+
async run(args) {
|
|
59
|
+
const task = args.positional.join(' ').trim();
|
|
60
|
+
if (!task) {
|
|
61
|
+
process.stderr.write('Usage: shrk watch "<task>" [--interval N] [--once] [--debounce ms]\n');
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
const cwd = resolveCwd(args);
|
|
65
|
+
const once = flagBool(args, 'once');
|
|
66
|
+
const quiet = flagBool(args, 'quiet') || flagBool(args, 'json');
|
|
67
|
+
const debounceMs = flagNumber(args, 'debounce') ?? DEFAULT_DEBOUNCE_MS;
|
|
68
|
+
const intervalSec = flagNumber(args, 'interval') ?? 0;
|
|
69
|
+
const intervalMs = intervalSec > 0 ? intervalSec * 1000 : DEFAULT_INTERVAL_MS;
|
|
70
|
+
const overrideRaw = flagString(args, 'task-type');
|
|
71
|
+
const taskTypeOverride = parseTaskTypeOverride(overrideRaw);
|
|
72
|
+
const index = await SemanticIndex.tryLoad(cwd);
|
|
73
|
+
if (!index) {
|
|
74
|
+
process.stderr.write('[shrk watch] no semantic index — run `shrk smart-context embeddings-build` first.\n');
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
const slug = slugify(task);
|
|
78
|
+
const feedDir = nodePath.join(cwd, FEED_DIR);
|
|
79
|
+
mkdirSync(feedDir, { recursive: true });
|
|
80
|
+
const feedPath = nodePath.join(feedDir, `${slug}.jsonl`);
|
|
81
|
+
const manifestFilePath = manifestPath(cwd, slug);
|
|
82
|
+
// Daemon manifest check — one watcher per (cwd, slug). Allows the agent
|
|
83
|
+
// and the human to know which watcher owns which feed, and prevents two
|
|
84
|
+
// processes from racing on the same JSONL.
|
|
85
|
+
if (!once) {
|
|
86
|
+
const existing = readManifest(manifestFilePath);
|
|
87
|
+
if (existing && isProcessAlive(existing.pid)) {
|
|
88
|
+
if (flagBool(args, 'replace')) {
|
|
89
|
+
try {
|
|
90
|
+
process.kill(existing.pid, 'SIGTERM');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// best effort
|
|
94
|
+
}
|
|
95
|
+
// Give the old process a moment to clean up.
|
|
96
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
process.stderr.write(`[shrk watch] another watcher is already running for slug "${slug}" (pid ${existing.pid}).\n` +
|
|
100
|
+
` → Use \`shrk watch list\` to see active feeds.\n` +
|
|
101
|
+
` → Use \`shrk watch stop ${slug}\` to stop it.\n` +
|
|
102
|
+
` → Pass --replace to take over.\n`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (existing) {
|
|
107
|
+
// Stale manifest (pid not alive) — clean up.
|
|
108
|
+
try {
|
|
109
|
+
rmSync(manifestFilePath, { force: true });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* ignore */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const manifest = {
|
|
116
|
+
schema: MANIFEST_SCHEMA,
|
|
117
|
+
pid: process.pid,
|
|
118
|
+
slug,
|
|
119
|
+
task,
|
|
120
|
+
startedAt: new Date().toISOString(),
|
|
121
|
+
feedPath,
|
|
122
|
+
intervalMs,
|
|
123
|
+
debounceMs,
|
|
124
|
+
};
|
|
125
|
+
writeFileSync(manifestFilePath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
126
|
+
}
|
|
127
|
+
if (!quiet) {
|
|
128
|
+
process.stderr.write(`[shrk watch] task: "${task}"\n[shrk watch] feed: ${feedPath}\n[shrk watch] press Ctrl+C to stop.\n`);
|
|
129
|
+
}
|
|
130
|
+
let lastHash = '';
|
|
131
|
+
let lastEmittedAt = 0;
|
|
132
|
+
const emit = async (reason) => {
|
|
133
|
+
const packet = await buildPacket({
|
|
134
|
+
cwd,
|
|
135
|
+
task,
|
|
136
|
+
index,
|
|
137
|
+
taskTypeOverride,
|
|
138
|
+
reason,
|
|
139
|
+
});
|
|
140
|
+
const fingerprint = packet.fingerprint;
|
|
141
|
+
if (fingerprint === lastHash) {
|
|
142
|
+
// Same content — skip noisy duplicate emissions.
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
lastHash = fingerprint;
|
|
146
|
+
lastEmittedAt = Date.now();
|
|
147
|
+
const line = asJson(packet);
|
|
148
|
+
process.stdout.write(line + '\n');
|
|
149
|
+
try {
|
|
150
|
+
appendFileSync(feedPath, line + '\n', 'utf8');
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
process.stderr.write(`[shrk watch] feed write failed: ${e.message}\n`);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// Initial emission.
|
|
157
|
+
await emit('start');
|
|
158
|
+
if (once)
|
|
159
|
+
return 0;
|
|
160
|
+
let debounceTimer = null;
|
|
161
|
+
const scheduleEmit = (reason) => {
|
|
162
|
+
if (debounceTimer)
|
|
163
|
+
clearTimeout(debounceTimer);
|
|
164
|
+
debounceTimer = setTimeout(() => {
|
|
165
|
+
debounceTimer = null;
|
|
166
|
+
void emit(reason);
|
|
167
|
+
}, debounceMs);
|
|
168
|
+
};
|
|
169
|
+
const watchers = [];
|
|
170
|
+
const roots = ['packages', 'examples', 'sharkcraft', 'docs', 'libs']
|
|
171
|
+
.map((p) => nodePath.join(cwd, p))
|
|
172
|
+
.filter((abs) => existsSync(abs));
|
|
173
|
+
for (const root of roots) {
|
|
174
|
+
try {
|
|
175
|
+
const w = fsWatch(root, { recursive: true }, (_event, filename) => {
|
|
176
|
+
if (!filename)
|
|
177
|
+
return;
|
|
178
|
+
if (!shouldReactTo(String(filename)))
|
|
179
|
+
return;
|
|
180
|
+
scheduleEmit(`change:${filename}`);
|
|
181
|
+
});
|
|
182
|
+
watchers.push(w);
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
process.stderr.write(`[shrk watch] could not watch ${root}: ${e.message}\n`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Optional interval-based emission so an idle workspace still
|
|
189
|
+
// produces fresh packets (handy for "every N seconds, refresh").
|
|
190
|
+
const intervalHandle = intervalMs > 0
|
|
191
|
+
? setInterval(() => {
|
|
192
|
+
if (Date.now() - lastEmittedAt < intervalMs / 2)
|
|
193
|
+
return;
|
|
194
|
+
scheduleEmit('interval');
|
|
195
|
+
}, intervalMs)
|
|
196
|
+
: null;
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
const shutdown = (code) => {
|
|
199
|
+
if (debounceTimer)
|
|
200
|
+
clearTimeout(debounceTimer);
|
|
201
|
+
if (intervalHandle)
|
|
202
|
+
clearInterval(intervalHandle);
|
|
203
|
+
for (const w of watchers) {
|
|
204
|
+
try {
|
|
205
|
+
w.close();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
/* ignore */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
if (existsSync(manifestFilePath)) {
|
|
213
|
+
const cur = readManifest(manifestFilePath);
|
|
214
|
+
if (cur && cur.pid === process.pid)
|
|
215
|
+
rmSync(manifestFilePath, { force: true });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
/* ignore */
|
|
220
|
+
}
|
|
221
|
+
if (!quiet)
|
|
222
|
+
process.stderr.write('\n[shrk watch] stopped.\n');
|
|
223
|
+
resolve(code);
|
|
224
|
+
};
|
|
225
|
+
const onSignal = () => shutdown(0);
|
|
226
|
+
process.once('SIGINT', onSignal);
|
|
227
|
+
process.once('SIGTERM', onSignal);
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
/** `shrk watch list` — show all active and stale watch manifests. */
|
|
232
|
+
export const watchListCommand = {
|
|
233
|
+
name: 'list',
|
|
234
|
+
description: 'List active shrk-watch daemons by reading manifests in .sharkcraft/feed/.',
|
|
235
|
+
usage: 'shrk watch list [--json]',
|
|
236
|
+
async run(args) {
|
|
237
|
+
const cwd = resolveCwd(args);
|
|
238
|
+
const json = flagBool(args, 'json');
|
|
239
|
+
const dir = nodePath.join(cwd, FEED_DIR);
|
|
240
|
+
if (!existsSync(dir)) {
|
|
241
|
+
if (json)
|
|
242
|
+
process.stdout.write(asJson({ active: [], stale: [] }) + '\n');
|
|
243
|
+
else
|
|
244
|
+
process.stdout.write('No watch feeds yet.\n');
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
const entries = readdirSync(dir).filter((n) => n.endsWith('.pid.json'));
|
|
248
|
+
const active = [];
|
|
249
|
+
const stale = [];
|
|
250
|
+
for (const name of entries) {
|
|
251
|
+
const m = readManifest(nodePath.join(dir, name));
|
|
252
|
+
if (!m)
|
|
253
|
+
continue;
|
|
254
|
+
if (isProcessAlive(m.pid)) {
|
|
255
|
+
let feedSize;
|
|
256
|
+
try {
|
|
257
|
+
feedSize = statSync(m.feedPath).size;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
/* ignore */
|
|
261
|
+
}
|
|
262
|
+
active.push({ ...m, ...(feedSize !== undefined ? { feedSize } : {}) });
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
stale.push({ slug: m.slug, pid: m.pid, startedAt: m.startedAt });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (json) {
|
|
269
|
+
process.stdout.write(asJson({ active, stale }) + '\n');
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
if (active.length === 0 && stale.length === 0) {
|
|
273
|
+
process.stdout.write('No watch feeds.\n');
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
if (active.length > 0) {
|
|
277
|
+
process.stdout.write(`Active watchers (${active.length}):\n`);
|
|
278
|
+
for (const w of active) {
|
|
279
|
+
process.stdout.write(` ${w.slug.padEnd(50)} pid ${String(w.pid).padEnd(8)} ${w.startedAt}\n → ${w.feedPath}${w.feedSize !== undefined ? ` (${w.feedSize} bytes)` : ''}\n`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (stale.length > 0) {
|
|
283
|
+
process.stdout.write(`\nStale manifests (process not alive):\n`);
|
|
284
|
+
for (const w of stale) {
|
|
285
|
+
process.stdout.write(` ${w.slug} (pid ${w.pid}, started ${w.startedAt})\n`);
|
|
286
|
+
}
|
|
287
|
+
process.stdout.write(' → Run `shrk watch prune` to clean them up.\n');
|
|
288
|
+
}
|
|
289
|
+
return 0;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
/** `shrk watch stop <slug>` — SIGTERM the matching watcher. */
|
|
293
|
+
export const watchStopCommand = {
|
|
294
|
+
name: 'stop',
|
|
295
|
+
description: 'Stop a running shrk-watch daemon by slug (sends SIGTERM and waits for it to exit).',
|
|
296
|
+
usage: 'shrk watch stop <slug> [--json]',
|
|
297
|
+
async run(args) {
|
|
298
|
+
const slug = args.positional[0]?.trim();
|
|
299
|
+
if (!slug) {
|
|
300
|
+
process.stderr.write('Usage: shrk watch stop <slug>\n');
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
303
|
+
const cwd = resolveCwd(args);
|
|
304
|
+
const json = flagBool(args, 'json');
|
|
305
|
+
const path = manifestPath(cwd, slug);
|
|
306
|
+
const m = readManifest(path);
|
|
307
|
+
if (!m) {
|
|
308
|
+
if (json)
|
|
309
|
+
process.stdout.write(asJson({ status: 'not-found', slug }) + '\n');
|
|
310
|
+
else
|
|
311
|
+
process.stderr.write(`No watcher manifest found for slug "${slug}".\n`);
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
if (!isProcessAlive(m.pid)) {
|
|
315
|
+
try {
|
|
316
|
+
rmSync(path, { force: true });
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
/* ignore */
|
|
320
|
+
}
|
|
321
|
+
if (json)
|
|
322
|
+
process.stdout.write(asJson({ status: 'stale', slug, pid: m.pid }) + '\n');
|
|
323
|
+
else
|
|
324
|
+
process.stdout.write(`Watcher for "${slug}" was stale (pid ${m.pid}); cleaned up manifest.\n`);
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
process.kill(m.pid, 'SIGTERM');
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
process.stderr.write(`Failed to signal pid ${m.pid}: ${e.message}\n`);
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
334
|
+
// Wait briefly for the process to exit and clean its own manifest.
|
|
335
|
+
for (let i = 0; i < 20; i += 1) {
|
|
336
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
337
|
+
if (!isProcessAlive(m.pid))
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
if (json)
|
|
341
|
+
process.stdout.write(asJson({ status: 'stopped', slug, pid: m.pid }) + '\n');
|
|
342
|
+
else
|
|
343
|
+
process.stdout.write(`Stopped watcher "${slug}" (pid ${m.pid}).\n`);
|
|
344
|
+
return 0;
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
/** `shrk watch prune` — remove stale manifests. */
|
|
348
|
+
export const watchPruneCommand = {
|
|
349
|
+
name: 'prune',
|
|
350
|
+
description: 'Remove stale shrk-watch manifests (pid not alive). Safe to run anytime.',
|
|
351
|
+
usage: 'shrk watch prune [--json]',
|
|
352
|
+
async run(args) {
|
|
353
|
+
const cwd = resolveCwd(args);
|
|
354
|
+
const json = flagBool(args, 'json');
|
|
355
|
+
const dir = nodePath.join(cwd, FEED_DIR);
|
|
356
|
+
if (!existsSync(dir)) {
|
|
357
|
+
if (json)
|
|
358
|
+
process.stdout.write(asJson({ removed: [] }) + '\n');
|
|
359
|
+
else
|
|
360
|
+
process.stdout.write('No feed directory; nothing to prune.\n');
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
const removed = [];
|
|
364
|
+
for (const name of readdirSync(dir).filter((n) => n.endsWith('.pid.json'))) {
|
|
365
|
+
const path = nodePath.join(dir, name);
|
|
366
|
+
const m = readManifest(path);
|
|
367
|
+
if (!m || !isProcessAlive(m.pid)) {
|
|
368
|
+
try {
|
|
369
|
+
rmSync(path, { force: true });
|
|
370
|
+
removed.push(m?.slug ?? name);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
/* ignore */
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (json)
|
|
378
|
+
process.stdout.write(asJson({ removed }) + '\n');
|
|
379
|
+
else
|
|
380
|
+
process.stdout.write(removed.length === 0 ? 'No stale manifests.\n' : `Pruned: ${removed.join(', ')}\n`);
|
|
381
|
+
return 0;
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
async function buildPacket(input) {
|
|
385
|
+
const inspection = await inspectSharkcraft({ cwd: input.cwd });
|
|
386
|
+
const packet = buildTaskPacket(inspection, input.task, { maxTokens: 3500 });
|
|
387
|
+
const focused = await buildFocusedContext({
|
|
388
|
+
cwd: input.cwd,
|
|
389
|
+
task: input.task,
|
|
390
|
+
index: input.index,
|
|
391
|
+
rules: packet.relevantRules,
|
|
392
|
+
verificationCommands: packet.verificationCommands,
|
|
393
|
+
});
|
|
394
|
+
const taskType = input.taskTypeOverride ?? classifyTask(input.task).type;
|
|
395
|
+
const summarisedFiles = focused.files.map((f) => ({
|
|
396
|
+
path: f.path,
|
|
397
|
+
fileSimilarity: f.fileSimilarity,
|
|
398
|
+
summary: f.summary,
|
|
399
|
+
blocks: f.blocks.map((b) => ({
|
|
400
|
+
name: b.name,
|
|
401
|
+
kind: b.kind,
|
|
402
|
+
startLine: b.startLine,
|
|
403
|
+
similarity: b.similarity,
|
|
404
|
+
})),
|
|
405
|
+
}));
|
|
406
|
+
// Fingerprint = which files + their similarities + which blocks ranked.
|
|
407
|
+
// Same diff round-trip = same fingerprint = no re-emission.
|
|
408
|
+
const fp = createHash('sha1')
|
|
409
|
+
.update(JSON.stringify({ taskType, files: summarisedFiles }))
|
|
410
|
+
.digest('hex')
|
|
411
|
+
.slice(0, 16);
|
|
412
|
+
return {
|
|
413
|
+
schema: 'sharkcraft.shrk-watch-packet/v1',
|
|
414
|
+
task: input.task,
|
|
415
|
+
taskSlug: slugify(input.task),
|
|
416
|
+
taskType,
|
|
417
|
+
emittedAt: new Date().toISOString(),
|
|
418
|
+
reason: input.reason,
|
|
419
|
+
fingerprint: fp,
|
|
420
|
+
focused: {
|
|
421
|
+
model: focused.model,
|
|
422
|
+
approxTokens: focused.approxTokens,
|
|
423
|
+
files: focused.files,
|
|
424
|
+
rules: focused.rules,
|
|
425
|
+
docHits: focused.docHits,
|
|
426
|
+
verificationCommands: focused.verificationCommands,
|
|
427
|
+
},
|
|
428
|
+
hints: {
|
|
429
|
+
pull: 'shrk smart-context "<task>" --focused --tiny-only --json',
|
|
430
|
+
plan: 'shrk smart-context "<task>" --focused --plan --save',
|
|
431
|
+
spike: 'shrk spike <slug>',
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function shouldReactTo(filename) {
|
|
436
|
+
if (filename.startsWith('.git/'))
|
|
437
|
+
return false;
|
|
438
|
+
if (filename.includes('node_modules/'))
|
|
439
|
+
return false;
|
|
440
|
+
if (filename.includes('/dist/') || filename.endsWith('/dist'))
|
|
441
|
+
return false;
|
|
442
|
+
if (filename.includes('.sharkcraft/'))
|
|
443
|
+
return false; // never feed our own writes back in
|
|
444
|
+
if (filename.includes('.next/'))
|
|
445
|
+
return false;
|
|
446
|
+
if (/\.(ts|tsx|js|jsx|mjs|cjs|md|json|yml|yaml)$/.test(filename))
|
|
447
|
+
return true;
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
function slugify(s) {
|
|
451
|
+
return (s
|
|
452
|
+
.toLowerCase()
|
|
453
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
454
|
+
.replace(/(^-+|-+$)/g, '')
|
|
455
|
+
.slice(0, 60) || 'task');
|
|
456
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal `.env` loader for the Node-based `shrk` binary.
|
|
3
|
+
*
|
|
4
|
+
* Bun auto-loads `.env`; Node does not. This walks from `cwd` up to the
|
|
5
|
+
* filesystem root looking for a `.env` file and merges any KEY=VALUE
|
|
6
|
+
* pairs into `process.env` — but only when the key is not already set,
|
|
7
|
+
* so an actual shell export always wins.
|
|
8
|
+
*
|
|
9
|
+
* No dependency on `dotenv`. Parser is intentionally small: lines that
|
|
10
|
+
* start with `#` are comments, blank lines are skipped, surrounding
|
|
11
|
+
* single/double quotes on the value are stripped, and escaped `\n`
|
|
12
|
+
* sequences inside double-quoted values become real newlines.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadDotenv(startDir: string): void;
|
|
15
|
+
//# sourceMappingURL=load-dotenv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-dotenv.d.ts","sourceRoot":"","sources":["../../src/env/load-dotenv.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAmBjD"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Minimal `.env` loader for the Node-based `shrk` binary.
|
|
5
|
+
*
|
|
6
|
+
* Bun auto-loads `.env`; Node does not. This walks from `cwd` up to the
|
|
7
|
+
* filesystem root looking for a `.env` file and merges any KEY=VALUE
|
|
8
|
+
* pairs into `process.env` — but only when the key is not already set,
|
|
9
|
+
* so an actual shell export always wins.
|
|
10
|
+
*
|
|
11
|
+
* No dependency on `dotenv`. Parser is intentionally small: lines that
|
|
12
|
+
* start with `#` are comments, blank lines are skipped, surrounding
|
|
13
|
+
* single/double quotes on the value are stripped, and escaped `\n`
|
|
14
|
+
* sequences inside double-quoted values become real newlines.
|
|
15
|
+
*/
|
|
16
|
+
export function loadDotenv(startDir) {
|
|
17
|
+
const envPath = findEnvFile(startDir);
|
|
18
|
+
if (!envPath)
|
|
19
|
+
return;
|
|
20
|
+
let body;
|
|
21
|
+
try {
|
|
22
|
+
body = readFileSync(envPath, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const rawLine of body.split(/\r?\n/)) {
|
|
28
|
+
const line = rawLine.trim();
|
|
29
|
+
if (line.length === 0 || line.startsWith('#'))
|
|
30
|
+
continue;
|
|
31
|
+
const eq = line.indexOf('=');
|
|
32
|
+
if (eq <= 0)
|
|
33
|
+
continue;
|
|
34
|
+
const key = line.slice(0, eq).trim();
|
|
35
|
+
if (!isValidKey(key))
|
|
36
|
+
continue;
|
|
37
|
+
if (process.env[key] !== undefined)
|
|
38
|
+
continue;
|
|
39
|
+
process.env[key] = unquote(line.slice(eq + 1).trim());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isValidKey(key) {
|
|
43
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
|
|
44
|
+
}
|
|
45
|
+
function unquote(value) {
|
|
46
|
+
if (value.length >= 2) {
|
|
47
|
+
const first = value[0];
|
|
48
|
+
const last = value[value.length - 1];
|
|
49
|
+
if (first === '"' && last === '"') {
|
|
50
|
+
return value.slice(1, -1).replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\t/g, '\t');
|
|
51
|
+
}
|
|
52
|
+
if (first === "'" && last === "'") {
|
|
53
|
+
return value.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const hash = value.indexOf(' #');
|
|
57
|
+
return hash === -1 ? value : value.slice(0, hash).trimEnd();
|
|
58
|
+
}
|
|
59
|
+
function findEnvFile(startDir) {
|
|
60
|
+
let dir = nodePath.resolve(startDir);
|
|
61
|
+
while (true) {
|
|
62
|
+
const candidate = nodePath.join(dir, '.env');
|
|
63
|
+
if (existsSync(candidate))
|
|
64
|
+
return candidate;
|
|
65
|
+
const parent = nodePath.dirname(dir);
|
|
66
|
+
if (parent === dir)
|
|
67
|
+
return null;
|
|
68
|
+
dir = parent;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/dist/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAIhB,MAAM,uBAAuB,CAAC;AAgX/B,wBAAgB,aAAa,IAAI,eAAe,CAmX/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAkGD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA4CxE"}
|
package/dist/main.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { loadDotenv } from "./env/load-dotenv.js";
|
|
2
3
|
import { CommandRegistry, extractGlobalCwd, parseArgs, } from "./command-registry.js";
|
|
3
4
|
import { initCommand } from "./commands/init.command.js";
|
|
4
5
|
import { inspectCommand } from "./commands/inspect.command.js";
|
|
@@ -55,6 +56,12 @@ import { dashboardCommand } from "./commands/dashboard.command.js";
|
|
|
55
56
|
import { dashboardDiffCommand, dashboardExportCommand, } from "./commands/dashboard-export.command.js";
|
|
56
57
|
import { importCommand } from "./commands/import.command.js";
|
|
57
58
|
import { askCommand } from "./commands/ask.command.js";
|
|
59
|
+
import { smartContextCommand, smartContextEmbeddingsBuildCommand, smartContextEmbeddingsStatusCommand, smartContextListCommand, smartContextPlanAheadCommand, smartContextShowCommand, } from "./commands/smart-context.command.js";
|
|
60
|
+
import { spikeCommand } from "./commands/spike.command.js";
|
|
61
|
+
import { depsAuditCommand } from "./commands/deps-audit.command.js";
|
|
62
|
+
import { scaffoldValidateCommand } from "./commands/scaffold-validate.command.js";
|
|
63
|
+
import { movePlanCommand } from "./commands/move-plan.command.js";
|
|
64
|
+
import { watchCommand, watchListCommand, watchPruneCommand, watchStopCommand } from "./commands/watch.command.js";
|
|
58
65
|
import { mcpCommand } from "./commands/mcp.command.js";
|
|
59
66
|
import { versionCommand } from "./commands/version.command.js";
|
|
60
67
|
import { makeHelpCommand } from "./commands/help.command.js";
|
|
@@ -204,6 +211,20 @@ export function buildRegistry() {
|
|
|
204
211
|
registry.register(planParentCommand);
|
|
205
212
|
registry.register(devCommand);
|
|
206
213
|
registry.register(askCommand);
|
|
214
|
+
registry.register(smartContextCommand);
|
|
215
|
+
registry.registerSubcommand('smart-context', smartContextPlanAheadCommand);
|
|
216
|
+
registry.registerSubcommand('smart-context', smartContextListCommand);
|
|
217
|
+
registry.registerSubcommand('smart-context', smartContextShowCommand);
|
|
218
|
+
registry.registerSubcommand('smart-context', smartContextEmbeddingsBuildCommand);
|
|
219
|
+
registry.registerSubcommand('smart-context', smartContextEmbeddingsStatusCommand);
|
|
220
|
+
registry.register(spikeCommand);
|
|
221
|
+
registry.register(depsAuditCommand);
|
|
222
|
+
registry.register(scaffoldValidateCommand);
|
|
223
|
+
registry.register(movePlanCommand);
|
|
224
|
+
registry.register(watchCommand);
|
|
225
|
+
registry.registerSubcommand('watch', watchListCommand);
|
|
226
|
+
registry.registerSubcommand('watch', watchStopCommand);
|
|
227
|
+
registry.registerSubcommand('watch', watchPruneCommand);
|
|
207
228
|
registry.register(mcpCommand);
|
|
208
229
|
registry.register(versionCommand);
|
|
209
230
|
registry.register(qualityCommand);
|
|
@@ -804,9 +825,69 @@ if (isMain ||
|
|
|
804
825
|
entryPath.endsWith('shrk') ||
|
|
805
826
|
entryPath.endsWith('shrk.js') ||
|
|
806
827
|
entryPath.endsWith('shrk.cmd')) {
|
|
828
|
+
loadDotenv(process.cwd());
|
|
807
829
|
const argv = process.argv.slice(2);
|
|
808
|
-
|
|
830
|
+
const cleanShutdown = async (code) => {
|
|
831
|
+
// Best-effort teardown of shared native runtimes. Without this,
|
|
832
|
+
// commands that loaded native libs (ONNX via embeddings; Metal
|
|
833
|
+
// via node-llama-cpp) abort during `process.exit` AFTER the
|
|
834
|
+
// work completed — the user sees their result then `zsh: abort`.
|
|
835
|
+
// Dynamic imports keep these off the hot path for commands that
|
|
836
|
+
// never touched them.
|
|
837
|
+
try {
|
|
838
|
+
const mod = (await import('@shrkcrft/embeddings'));
|
|
839
|
+
if (typeof mod.disposeSemanticIndexPipeline === 'function') {
|
|
840
|
+
await mod.disposeSemanticIndexPipeline();
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
// Best-effort; never block the exit on teardown failure.
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
const mod = (await import('@shrkcrft/ai'));
|
|
848
|
+
if (typeof mod.disposeLlamaCppRuntime === 'function') {
|
|
849
|
+
await mod.disposeLlamaCppRuntime();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
// Best-effort.
|
|
854
|
+
}
|
|
855
|
+
// Flush stdio synchronously before bypassing C++ destructors.
|
|
856
|
+
// `_exit` skips all libc finalizers, which is exactly what we
|
|
857
|
+
// need (see below), but it also doesn't wait for buffered
|
|
858
|
+
// writes to drain. Two synchronous write callbacks force the
|
|
859
|
+
// current buffers through.
|
|
860
|
+
try {
|
|
861
|
+
await new Promise((resolve) => process.stdout.write('', () => resolve()));
|
|
862
|
+
await new Promise((resolve) => process.stderr.write('', () => resolve()));
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
// ignore flush failures
|
|
866
|
+
}
|
|
867
|
+
// Prefer `_exit` over `exit` on Node. Even after our explicit
|
|
868
|
+
// disposes above, node-llama-cpp's libggml-metal destructor
|
|
869
|
+
// still aborts in `__cxa_finalize_ranges` because the Metal
|
|
870
|
+
// device list isn't drained by the current dispose API
|
|
871
|
+
// (`GGML_ASSERT([rsets->data count] == 0)` fires from
|
|
872
|
+
// `ggml_metal_device_free`, surfacing as `zsh: abort` AFTER
|
|
873
|
+
// the user's result has already printed). `process._exit`
|
|
874
|
+
// skips the libc++ static-destructor pass entirely, which is
|
|
875
|
+
// safe here because we have already torn down the runtimes
|
|
876
|
+
// we care about above and the OS will reclaim the rest.
|
|
877
|
+
//
|
|
878
|
+
// Bun's process object doesn't expose `_exit`, but Bun also
|
|
879
|
+
// doesn't drive shutdown through libuv + libc++ static
|
|
880
|
+
// destructors the same way Node does, so the Metal crash
|
|
881
|
+
// doesn't reproduce there. Fall back to `process.exit` when
|
|
882
|
+
// `_exit` is unavailable.
|
|
883
|
+
const lowLevelExit = process._exit;
|
|
884
|
+
if (typeof lowLevelExit === 'function') {
|
|
885
|
+
lowLevelExit(code);
|
|
886
|
+
}
|
|
887
|
+
process.exit(code);
|
|
888
|
+
};
|
|
889
|
+
runCli(argv).then((code) => cleanShutdown(code), (err) => {
|
|
809
890
|
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
810
|
-
|
|
891
|
+
return cleanShutdown(1);
|
|
811
892
|
});
|
|
812
893
|
}
|