elliot-stack 1.0.17 → 1.0.18
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/README.md +6 -0
- package/bin/install.cjs +203 -9
- package/hooks/repo-search-nudge.js +31 -0
- package/package.json +3 -2
- package/skills/estack-repo-search/SKILL.md +0 -65
package/README.md
CHANGED
|
@@ -52,6 +52,12 @@ npx elliot-stack@latest
|
|
|
52
52
|
- [Claude Code](https://claude.ai/code) CLI installed
|
|
53
53
|
- Node.js 18+
|
|
54
54
|
|
|
55
|
+
## Contributing
|
|
56
|
+
|
|
57
|
+
External contributions are welcome via pull request. Direct pushes to `main` are blocked — fork the repo, push your changes to a branch, and open a PR. Only the maintainer (Elliot) can merge to `main` and cut releases. This is intentional: `elliot-stack` is a security-sensitive npm package and the release tag can only be pushed by the maintainer.
|
|
58
|
+
|
|
59
|
+
See [`docs/publishing.md`](docs/publishing.md) for the release flow and security model.
|
|
60
|
+
|
|
55
61
|
## License
|
|
56
62
|
|
|
57
63
|
MIT
|
package/bin/install.cjs
CHANGED
|
@@ -15,6 +15,8 @@ const BACKUP_DIR = path.join(CLAUDE_DIR, '.estack-backup');
|
|
|
15
15
|
const CHECKSUMS_FILE = path.join(CLAUDE_DIR, '.estack-checksums.json');
|
|
16
16
|
const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
17
17
|
const PACKAGE_SKILLS_DIR = path.join(__dirname, '..', 'skills');
|
|
18
|
+
const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
|
|
19
|
+
const PACKAGE_HOOKS_DIR = path.join(__dirname, '..', 'hooks');
|
|
18
20
|
|
|
19
21
|
// ── Flags ──────────────────────────────────────────────────────────────────
|
|
20
22
|
const SILENT = process.argv.includes('--silent');
|
|
@@ -39,6 +41,13 @@ function walkDir(dir, base) {
|
|
|
39
41
|
return files;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
function computeFileHash(filePath) {
|
|
45
|
+
if (!fs.existsSync(filePath)) return null;
|
|
46
|
+
const hash = crypto.createHash('sha256');
|
|
47
|
+
hash.update(fs.readFileSync(filePath));
|
|
48
|
+
return hash.digest('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
function computeSkillHash(skillDir) {
|
|
43
52
|
if (!fs.existsSync(skillDir)) return null;
|
|
44
53
|
const hash = crypto.createHash('sha256');
|
|
@@ -66,6 +75,14 @@ function backupSkill(name) {
|
|
|
66
75
|
copyDir(installedDir, path.join(BACKUP_DIR, name));
|
|
67
76
|
}
|
|
68
77
|
|
|
78
|
+
function backupHook(filename) {
|
|
79
|
+
const installedFile = path.join(HOOKS_DIR, filename);
|
|
80
|
+
if (!fs.existsSync(installedFile)) return;
|
|
81
|
+
const dest = path.join(BACKUP_DIR, 'hooks', filename);
|
|
82
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
83
|
+
fs.copyFileSync(installedFile, dest);
|
|
84
|
+
}
|
|
85
|
+
|
|
69
86
|
function promptChar(question) {
|
|
70
87
|
if (!process.stdin.isTTY) {
|
|
71
88
|
// Non-interactive environment — read a line from piped stdin
|
|
@@ -118,7 +135,7 @@ function getSkillDescription(skillDir) {
|
|
|
118
135
|
return '';
|
|
119
136
|
}
|
|
120
137
|
|
|
121
|
-
// ──
|
|
138
|
+
// ── Hook setup ─────────────────────────────────────────────────────────────
|
|
122
139
|
|
|
123
140
|
function setupStartupHook() {
|
|
124
141
|
let settings = {};
|
|
@@ -164,6 +181,45 @@ function setupStartupHook() {
|
|
|
164
181
|
return true;
|
|
165
182
|
}
|
|
166
183
|
|
|
184
|
+
function setupRepoSearchNudgeHook() {
|
|
185
|
+
let settings = {};
|
|
186
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
187
|
+
try {
|
|
188
|
+
settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
|
189
|
+
} catch (_) {
|
|
190
|
+
settings = {};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (settings.hooks && settings.hooks.PostToolUse) {
|
|
195
|
+
for (const group of settings.hooks.PostToolUse) {
|
|
196
|
+
if (group.matcher === 'WebFetch|WebSearch' && group.hooks) {
|
|
197
|
+
for (const hook of group.hooks) {
|
|
198
|
+
if (hook.command && hook.command.includes('repo-search-nudge.js')) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!settings.hooks) settings.hooks = {};
|
|
207
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
208
|
+
|
|
209
|
+
settings.hooks.PostToolUse.push({
|
|
210
|
+
matcher: 'WebFetch|WebSearch',
|
|
211
|
+
hooks: [{
|
|
212
|
+
type: 'command',
|
|
213
|
+
command: `node "${path.join(HOOKS_DIR, 'repo-search-nudge.js').replace(/\\/g, '/')}"`,
|
|
214
|
+
timeout: 5,
|
|
215
|
+
}],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
219
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
167
223
|
// ── Main ────────────────────────────────────────────────────────────────────
|
|
168
224
|
|
|
169
225
|
async function main() {
|
|
@@ -191,6 +247,16 @@ async function main() {
|
|
|
191
247
|
packageHashes[name] = computeSkillHash(path.join(PACKAGE_SKILLS_DIR, name));
|
|
192
248
|
}
|
|
193
249
|
|
|
250
|
+
// 2b. Scan package hooks
|
|
251
|
+
const hookFilenames = fs.existsSync(PACKAGE_HOOKS_DIR)
|
|
252
|
+
? fs.readdirSync(PACKAGE_HOOKS_DIR).filter((f) => f.endsWith('.js')).sort()
|
|
253
|
+
: [];
|
|
254
|
+
|
|
255
|
+
const packageHookHashes = {};
|
|
256
|
+
for (const filename of hookFilenames) {
|
|
257
|
+
packageHookHashes[filename] = computeFileHash(path.join(PACKAGE_HOOKS_DIR, filename));
|
|
258
|
+
}
|
|
259
|
+
|
|
194
260
|
// 3. Load existing checksums
|
|
195
261
|
let storedChecksums = {};
|
|
196
262
|
if (fs.existsSync(CHECKSUMS_FILE)) {
|
|
@@ -230,9 +296,36 @@ async function main() {
|
|
|
230
296
|
}
|
|
231
297
|
}
|
|
232
298
|
|
|
299
|
+
// 4b. Detect local modifications and needed updates for hooks
|
|
300
|
+
const modifiedHooks = [];
|
|
301
|
+
const hooksNeedingUpdate = [];
|
|
302
|
+
for (const filename of hookFilenames) {
|
|
303
|
+
const installedFile = path.join(HOOKS_DIR, filename);
|
|
304
|
+
const key = 'hook:' + filename;
|
|
305
|
+
if (!fs.existsSync(installedFile)) {
|
|
306
|
+
hooksNeedingUpdate.push(filename);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const currentHash = computeFileHash(installedFile);
|
|
310
|
+
if (!storedChecksums[key]) {
|
|
311
|
+
if (currentHash !== packageHookHashes[filename]) {
|
|
312
|
+
modifiedHooks.push(filename);
|
|
313
|
+
hooksNeedingUpdate.push(filename);
|
|
314
|
+
}
|
|
315
|
+
} else if (currentHash !== storedChecksums[key]) {
|
|
316
|
+
modifiedHooks.push(filename);
|
|
317
|
+
if (storedChecksums[key] !== packageHookHashes[filename]) {
|
|
318
|
+
hooksNeedingUpdate.push(filename);
|
|
319
|
+
}
|
|
320
|
+
} else if (currentHash !== packageHookHashes[filename]) {
|
|
321
|
+
hooksNeedingUpdate.push(filename);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
233
325
|
// 5. Silent mode — no output at all
|
|
234
326
|
if (SILENT) {
|
|
235
|
-
if (needsUpdate.length === 0 && modifiedSkills.length === 0
|
|
327
|
+
if (needsUpdate.length === 0 && modifiedSkills.length === 0 &&
|
|
328
|
+
hooksNeedingUpdate.length === 0 && modifiedHooks.length === 0) {
|
|
236
329
|
process.exit(0);
|
|
237
330
|
}
|
|
238
331
|
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
@@ -243,13 +336,21 @@ async function main() {
|
|
|
243
336
|
copyDir(path.join(PACKAGE_SKILLS_DIR, name), path.join(SKILLS_DIR, name));
|
|
244
337
|
newChecksums[name] = packageHashes[name];
|
|
245
338
|
}
|
|
339
|
+
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
340
|
+
for (const filename of hookFilenames) {
|
|
341
|
+
if (modifiedHooks.includes(filename)) continue;
|
|
342
|
+
if (!hooksNeedingUpdate.includes(filename) && fs.existsSync(path.join(HOOKS_DIR, filename))) continue;
|
|
343
|
+
fs.copyFileSync(path.join(PACKAGE_HOOKS_DIR, filename), path.join(HOOKS_DIR, filename));
|
|
344
|
+
newChecksums['hook:' + filename] = packageHookHashes[filename];
|
|
345
|
+
}
|
|
246
346
|
fs.writeFileSync(CHECKSUMS_FILE, JSON.stringify(newChecksums, null, 2));
|
|
247
347
|
process.exit(0);
|
|
248
348
|
}
|
|
249
349
|
|
|
250
350
|
// 6. Startup mode — non-interactive, backup + merge context for Claude Code
|
|
251
351
|
if (STARTUP) {
|
|
252
|
-
if (needsUpdate.length === 0 && modifiedSkills.length === 0
|
|
352
|
+
if (needsUpdate.length === 0 && modifiedSkills.length === 0 &&
|
|
353
|
+
hooksNeedingUpdate.length === 0 && modifiedHooks.length === 0) {
|
|
253
354
|
process.exit(0);
|
|
254
355
|
}
|
|
255
356
|
|
|
@@ -273,6 +374,27 @@ async function main() {
|
|
|
273
374
|
updated.push(name);
|
|
274
375
|
}
|
|
275
376
|
|
|
377
|
+
// Install hooks
|
|
378
|
+
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
379
|
+
const updatedHooks = [];
|
|
380
|
+
const mergeNeededHooks = [];
|
|
381
|
+
|
|
382
|
+
for (const filename of hookFilenames) {
|
|
383
|
+
if (modifiedHooks.includes(filename)) {
|
|
384
|
+
backupHook(filename);
|
|
385
|
+
fs.copyFileSync(path.join(PACKAGE_HOOKS_DIR, filename), path.join(HOOKS_DIR, filename));
|
|
386
|
+
newChecksums['hook:' + filename] = packageHookHashes[filename];
|
|
387
|
+
mergeNeededHooks.push(filename);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (!hooksNeedingUpdate.includes(filename) && fs.existsSync(path.join(HOOKS_DIR, filename))) continue;
|
|
391
|
+
fs.copyFileSync(path.join(PACKAGE_HOOKS_DIR, filename), path.join(HOOKS_DIR, filename));
|
|
392
|
+
newChecksums['hook:' + filename] = packageHookHashes[filename];
|
|
393
|
+
updatedHooks.push(filename);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
setupRepoSearchNudgeHook();
|
|
397
|
+
|
|
276
398
|
fs.writeFileSync(CHECKSUMS_FILE, JSON.stringify(newChecksums, null, 2));
|
|
277
399
|
|
|
278
400
|
// Build output for Claude Code
|
|
@@ -283,6 +405,10 @@ async function main() {
|
|
|
283
405
|
msgParts.push('estack: updated ' + updated.join(', '));
|
|
284
406
|
}
|
|
285
407
|
|
|
408
|
+
if (updatedHooks.length > 0) {
|
|
409
|
+
msgParts.push('estack: updated hooks ' + updatedHooks.join(', '));
|
|
410
|
+
}
|
|
411
|
+
|
|
286
412
|
if (mergeNeeded.length > 0) {
|
|
287
413
|
const backupPath = BACKUP_DIR.replace(HOME, '~');
|
|
288
414
|
msgParts.push(
|
|
@@ -299,6 +425,21 @@ async function main() {
|
|
|
299
425
|
'identify the user\'s changes, and apply them to the new version where compatible.';
|
|
300
426
|
}
|
|
301
427
|
|
|
428
|
+
if (mergeNeededHooks.length > 0) {
|
|
429
|
+
const backupPath = BACKUP_DIR.replace(HOME, '~');
|
|
430
|
+
msgParts.push(
|
|
431
|
+
'estack: updated hooks ' + mergeNeededHooks.join(', ') +
|
|
432
|
+
' (local changes backed up to ' + backupPath + '/hooks/)'
|
|
433
|
+
);
|
|
434
|
+
const existingContext = output.additionalContext ? output.additionalContext + ' ' : '';
|
|
435
|
+
output.additionalContext =
|
|
436
|
+
existingContext +
|
|
437
|
+
'estack hooks were updated but the user had local modifications to: ' +
|
|
438
|
+
mergeNeededHooks.join(', ') + '. ' +
|
|
439
|
+
'Their previous versions are saved at ' + path.join(BACKUP_DIR, 'hooks') + '. ' +
|
|
440
|
+
'The new upstream versions are now installed at ' + HOOKS_DIR + '.';
|
|
441
|
+
}
|
|
442
|
+
|
|
302
443
|
if (msgParts.length > 0) {
|
|
303
444
|
output.systemMessage = msgParts.join('\n');
|
|
304
445
|
}
|
|
@@ -312,10 +453,19 @@ async function main() {
|
|
|
312
453
|
// 7. Interactive mode — prompt if modifications detected
|
|
313
454
|
let modifiedAction = null; // 'overwrite', 'skip', or 'merge'
|
|
314
455
|
|
|
315
|
-
if (modifiedSkills.length > 0) {
|
|
316
|
-
console.log('\nThe following
|
|
317
|
-
|
|
318
|
-
console.log('
|
|
456
|
+
if (modifiedSkills.length > 0 || modifiedHooks.length > 0) {
|
|
457
|
+
console.log('\nThe following items have been modified locally:');
|
|
458
|
+
if (modifiedSkills.length > 0) {
|
|
459
|
+
console.log(' Skills:');
|
|
460
|
+
for (const name of modifiedSkills) {
|
|
461
|
+
console.log(' - ' + name);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (modifiedHooks.length > 0) {
|
|
465
|
+
console.log(' Hooks:');
|
|
466
|
+
for (const filename of modifiedHooks) {
|
|
467
|
+
console.log(' - ' + filename);
|
|
468
|
+
}
|
|
319
469
|
}
|
|
320
470
|
console.log('\nChoose an action:');
|
|
321
471
|
console.log(' [o] Overwrite all (replace with latest)');
|
|
@@ -371,15 +521,49 @@ async function main() {
|
|
|
371
521
|
console.log(' Installed ' + name);
|
|
372
522
|
}
|
|
373
523
|
|
|
524
|
+
// 8b. Install hooks
|
|
525
|
+
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
526
|
+
let installedHookCount = 0;
|
|
527
|
+
const mergedHooks = [];
|
|
528
|
+
|
|
529
|
+
for (const filename of hookFilenames) {
|
|
530
|
+
if (modifiedHooks.includes(filename)) {
|
|
531
|
+
if (modifiedAction === 'skip') {
|
|
532
|
+
console.log(' Skipped hook ' + filename + ' (local modifications preserved)');
|
|
533
|
+
const currentHash = computeFileHash(path.join(HOOKS_DIR, filename));
|
|
534
|
+
if (currentHash) newChecksums['hook:' + filename] = currentHash;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (modifiedAction === 'merge') {
|
|
538
|
+
backupHook(filename);
|
|
539
|
+
mergedHooks.push(filename);
|
|
540
|
+
console.log(' Backed up hook ' + filename + ' → ~/.claude/.estack-backup/hooks/' + filename);
|
|
541
|
+
}
|
|
542
|
+
// overwrite or merge — fall through to install
|
|
543
|
+
} else if (!hooksNeedingUpdate.includes(filename) && fs.existsSync(path.join(HOOKS_DIR, filename))) {
|
|
544
|
+
// Already installed and up-to-date
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
fs.copyFileSync(path.join(PACKAGE_HOOKS_DIR, filename), path.join(HOOKS_DIR, filename));
|
|
548
|
+
newChecksums['hook:' + filename] = packageHookHashes[filename];
|
|
549
|
+
installedHookCount++;
|
|
550
|
+
console.log(' Installed hook ' + filename);
|
|
551
|
+
}
|
|
552
|
+
|
|
374
553
|
// 9. Write checksums
|
|
375
554
|
fs.writeFileSync(CHECKSUMS_FILE, JSON.stringify(newChecksums, null, 2));
|
|
376
555
|
|
|
377
|
-
// 10. Setup startup hook
|
|
556
|
+
// 10. Setup startup hook and repo-search nudge hook
|
|
378
557
|
const hookInstalled = setupStartupHook();
|
|
558
|
+
const nudgeHookInstalled = setupRepoSearchNudgeHook();
|
|
379
559
|
|
|
380
560
|
// 11. Summary output
|
|
381
561
|
console.log('\nestack installed successfully!\n');
|
|
382
|
-
console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' installed to ~/.claude/skills
|
|
562
|
+
console.log(' ' + installedCount + ' skill' + (installedCount !== 1 ? 's' : '') + ' installed to ~/.claude/skills/');
|
|
563
|
+
if (installedHookCount > 0) {
|
|
564
|
+
console.log(' ' + installedHookCount + ' hook' + (installedHookCount !== 1 ? 's' : '') + ' installed to ~/.claude/hooks/');
|
|
565
|
+
}
|
|
566
|
+
console.log('');
|
|
383
567
|
console.log('Skills available:');
|
|
384
568
|
|
|
385
569
|
for (const name of skillNames) {
|
|
@@ -393,12 +577,22 @@ async function main() {
|
|
|
393
577
|
console.log(' "Merge my estack changes from ~/.claude/.estack-backup/"');
|
|
394
578
|
}
|
|
395
579
|
|
|
580
|
+
if (mergedHooks.length > 0) {
|
|
581
|
+
console.log('\nLocal hook changes backed up for: ' + mergedHooks.join(', '));
|
|
582
|
+
console.log('Backed up to ~/.claude/.estack-backup/hooks/');
|
|
583
|
+
}
|
|
584
|
+
|
|
396
585
|
if (hookInstalled) {
|
|
397
586
|
console.log('\nAuto-update hook added to ~/.claude/settings.json');
|
|
398
587
|
console.log('Skills will update automatically when you start Claude Code.');
|
|
399
588
|
} else {
|
|
400
589
|
console.log('\nAuto-update hook already configured.');
|
|
401
590
|
}
|
|
591
|
+
|
|
592
|
+
if (nudgeHookInstalled) {
|
|
593
|
+
console.log('Repo-search nudge hook registered in settings.json.');
|
|
594
|
+
}
|
|
595
|
+
|
|
402
596
|
console.log('');
|
|
403
597
|
}
|
|
404
598
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PostToolUse hook: nudges toward the repo-search skill when GitHub is involved.
|
|
3
|
+
// Fires on every WebFetch or WebSearch that touches a github.com URL.
|
|
4
|
+
|
|
5
|
+
const SKILL_NAME = "estack-repo-search";
|
|
6
|
+
|
|
7
|
+
let input = "";
|
|
8
|
+
process.stdin.on("data", (c) => (input += c));
|
|
9
|
+
process.stdin.on("end", () => {
|
|
10
|
+
try { run(input); } catch { /* never break the tool */ }
|
|
11
|
+
process.exit(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function emitNudge() {
|
|
15
|
+
process.stdout.write(JSON.stringify({
|
|
16
|
+
hookSpecificOutput: {
|
|
17
|
+
hookEventName: "PostToolUse",
|
|
18
|
+
additionalContext: `GitHub repo detected. For deeper code questions, consider the ${SKILL_NAME} skill — it clones and greps the repo locally, which is usually faster and more accurate than web fetching multiple GitHub pages.`
|
|
19
|
+
}
|
|
20
|
+
}) + "\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function run(raw) {
|
|
24
|
+
let payload;
|
|
25
|
+
try { payload = JSON.parse(raw); } catch { return; }
|
|
26
|
+
|
|
27
|
+
const tool = payload.tool_name;
|
|
28
|
+
if (tool !== "WebFetch" && tool !== "WebSearch") return;
|
|
29
|
+
|
|
30
|
+
if (/github\.com/i.test(raw)) emitNudge();
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elliot-stack",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"description": "Elliot's skill stack for Claude Code — install via npx elliot-stack@latest",
|
|
5
5
|
"bin": {
|
|
6
6
|
"elliot-stack": "bin/install.cjs"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/",
|
|
10
|
-
"skills/"
|
|
10
|
+
"skills/",
|
|
11
|
+
"hooks/"
|
|
11
12
|
],
|
|
12
13
|
"keywords": [
|
|
13
14
|
"claude-code",
|
|
@@ -90,68 +90,3 @@ print(base + '?title=' + urllib.parse.quote(title) + '&body=' + urllib.parse.quo
|
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
Share the printed URL with the user. They click it, review the pre-filled title and body, then click **Submit new issue**.
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
## Skill Feedback
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Skill Feedback
|
|
100
|
-
|
|
101
|
-
If the user shares feedback about this skill — a bug, something confusing, a missing feature, or a suggestion — ask them to describe it in a bit more detail (what they expected, what happened, and any relevant context). Then file the issue using whichever method is available:
|
|
102
|
-
|
|
103
|
-
**If `gh` is installed** (`gh --version` succeeds), create the issue directly:
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
gh issue create \
|
|
107
|
-
--repo ElliotDrel/e-stack \
|
|
108
|
-
--title "estack-repo-search: <concise summary>" \
|
|
109
|
-
--body "<description from user feedback — expected vs. actual behavior and context>"
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
**If `gh` is not installed**, build a pre-filled URL:
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
python3 -c "
|
|
116
|
-
import urllib.parse
|
|
117
|
-
title = 'estack-repo-search: <concise summary>'
|
|
118
|
-
body = '<description from user feedback — expected vs. actual behavior and context>'
|
|
119
|
-
base = 'https://github.com/ElliotDrel/e-stack/issues/new'
|
|
120
|
-
print(base + '?title=' + urllib.parse.quote(title) + '&body=' + urllib.parse.quote(body))
|
|
121
|
-
"
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Share the printed URL with the user and offer to open it in their browser.
|
|
125
|
-
|
|
126
|
-
They can also click it directly, review the pre-filled title and body, and click **Submit new issue**.
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## Skill Feedback
|
|
131
|
-
|
|
132
|
-
If the user shares feedback about this skill — a bug, something confusing, a missing feature, or a suggestion — ask them to describe it in a bit more detail (what they expected, what happened, and any relevant context). Then file the issue using whichever method is available:
|
|
133
|
-
|
|
134
|
-
**If `gh` is installed** (`gh --version` succeeds), create the issue directly:
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
gh issue create \
|
|
138
|
-
--repo ElliotDrel/e-stack \
|
|
139
|
-
--title "estack-repo-search: <concise summary>" \
|
|
140
|
-
--body "<description from user feedback — expected vs. actual behavior and context>"
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
**If `gh` is not installed**, build a pre-filled URL:
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
python3 -c "
|
|
147
|
-
import urllib.parse
|
|
148
|
-
title = 'estack-repo-search: <concise summary>'
|
|
149
|
-
body = '<description from user feedback — expected vs. actual behavior and context>'
|
|
150
|
-
base = 'https://github.com/ElliotDrel/e-stack/issues/new'
|
|
151
|
-
print(base + '?title=' + urllib.parse.quote(title) + '&body=' + urllib.parse.quote(body))
|
|
152
|
-
"
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Share the printed URL with the user and offer to open it in their browser.
|
|
156
|
-
|
|
157
|
-
They can also click it directly, review the pre-filled title and body, and click **Submit new issue**.
|