scoops 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.
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const ASCR_HOOK_MARKER = 'ascr/hooks';
4
+
5
+ const ASCR_HOOKS = {
6
+ SessionStart: {
7
+ matcher: '',
8
+ hooks: [{ type: 'command', command: 'node .ascr/hooks/session-start.js', timeout: 10 }],
9
+ },
10
+ UserPromptSubmit: {
11
+ matcher: '',
12
+ hooks: [{ type: 'command', command: 'node .ascr/hooks/prompt-retriever.js', timeout: 10 }],
13
+ },
14
+ Stop: {
15
+ matcher: '',
16
+ hooks: [{ type: 'command', command: 'node .ascr/hooks/stop-gate.js', timeout: 15, blocking: true }],
17
+ },
18
+ };
19
+
20
+ // Returns true if a hook entry looks like an ASCR hook
21
+ function isAscrHook(entry) {
22
+ return (entry.hooks || []).some(h => h.command && h.command.includes(ASCR_HOOK_MARKER));
23
+ }
24
+
25
+ // Deep-merge ASCR hooks into an existing settings object without clobbering anything
26
+ function mergeHooksIntoSettings(existing = {}) {
27
+ const result = JSON.parse(JSON.stringify(existing)); // deep clone
28
+ if (!result.hooks) result.hooks = {};
29
+
30
+ for (const [event, hookEntry] of Object.entries(ASCR_HOOKS)) {
31
+ if (!result.hooks[event]) {
32
+ // Event not present at all — add it
33
+ result.hooks[event] = [hookEntry];
34
+ } else {
35
+ // Event exists — check if ASCR hook is already there
36
+ const existingIdx = result.hooks[event].findIndex(isAscrHook);
37
+ if (existingIdx === -1) {
38
+ // Not present — append
39
+ result.hooks[event].push(hookEntry);
40
+ } else {
41
+ // Already present — update in place (idempotent re-init)
42
+ result.hooks[event][existingIdx] = hookEntry;
43
+ }
44
+ }
45
+ }
46
+
47
+ return result;
48
+ }
49
+
50
+ // Returns the list of events ASCR hooks are registered under (for status checks)
51
+ function getAscrHookEvents(settings = {}) {
52
+ const registered = [];
53
+ for (const [event, entries] of Object.entries(settings.hooks || {})) {
54
+ if ((entries || []).some(isAscrHook)) registered.push(event);
55
+ }
56
+ return registered;
57
+ }
58
+
59
+ module.exports = { mergeHooksIntoSettings, getAscrHookEvents, ASCR_HOOKS };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const { join } = require('node:path');
4
+
5
+ const ascrDir = (cwd) => join(cwd, '.ascr');
6
+ const hooksDir = (cwd) => join(cwd, '.ascr', 'hooks');
7
+ const decisionsFile = (cwd) => join(cwd, '.ascr', 'decisions.json');
8
+ const architectureFile = (cwd) => join(cwd, '.ascr', 'architecture.json');
9
+ const threadsFile = (cwd) => join(cwd, '.ascr', 'threads.json');
10
+ const warningsFile = (cwd) => join(cwd, '.ascr', 'warnings.json');
11
+ const indexFile = (cwd) => join(cwd, '.ascr', 'index.json');
12
+ const gitignoreFile = (cwd) => join(cwd, '.ascr', '.gitignore');
13
+ const sessionHashFile = (cwd, sessionId) => join(cwd, '.ascr', `.session-hash-${sessionId}`);
14
+ const claudeDir = (cwd) => join(cwd, '.claude');
15
+ const settingsFile = (cwd) => join(cwd, '.claude', 'settings.json');
16
+
17
+ const KNOWLEDGE_FILES = (cwd) => ({
18
+ decisions: decisionsFile(cwd),
19
+ architecture: architectureFile(cwd),
20
+ threads: threadsFile(cwd),
21
+ warnings: warningsFile(cwd),
22
+ index: indexFile(cwd),
23
+ });
24
+
25
+ module.exports = {
26
+ ascrDir,
27
+ hooksDir,
28
+ decisionsFile,
29
+ architectureFile,
30
+ threadsFile,
31
+ warningsFile,
32
+ indexFile,
33
+ gitignoreFile,
34
+ sessionHashFile,
35
+ claudeDir,
36
+ settingsFile,
37
+ KNOWLEDGE_FILES,
38
+ };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ // Common English stop words to exclude from keyword extraction
4
+ const STOP_WORDS = new Set([
5
+ 'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
6
+ 'of', 'with', 'by', 'from', 'is', 'it', 'its', 'be', 'as', 'are',
7
+ 'was', 'were', 'been', 'has', 'have', 'had', 'do', 'does', 'did',
8
+ 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'i', 'we',
9
+ 'you', 'he', 'she', 'they', 'this', 'that', 'these', 'those', 'what',
10
+ 'how', 'why', 'when', 'where', 'which', 'who', 'not', 'no', 'so',
11
+ 'if', 'then', 'else', 'my', 'our', 'your', 'their', 'me', 'us',
12
+ 'add', 'make', 'create', 'update', 'get', 'set', 'use', 'using',
13
+ 'want', 'need', 'please', 'help', 'let', 'just', 'also', 'now',
14
+ 'like', 'new', 'old', 'all', 'some', 'any', 'into', 'out', 'up',
15
+ ]);
16
+
17
+ // Extract meaningful keywords from a prompt string
18
+ function extractKeywords(prompt) {
19
+ return prompt
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9\s\-_]/g, ' ') // strip punctuation
22
+ .split(/[\s\-_]+/) // split on whitespace and separators
23
+ .map(w => w.trim())
24
+ .filter(w => w.length >= 3 && !STOP_WORDS.has(w))
25
+ .filter((w, i, arr) => arr.indexOf(w) === i); // dedupe
26
+ }
27
+
28
+ // Format a retrieved knowledge entry as concise injected text
29
+ function formatEntry(type, entry) {
30
+ const lines = [];
31
+ switch (type) {
32
+ case 'decisions':
33
+ lines.push(`[Decision] ${entry.decision}`);
34
+ if (entry.why) lines.push(` Why: ${entry.why}`);
35
+ if (entry.alternatives_rejected?.length) lines.push(` Rejected: ${entry.alternatives_rejected.join(', ')}`);
36
+ if (entry.affects?.length) lines.push(` Affects: ${entry.affects.join(', ')}`);
37
+ break;
38
+ case 'architecture':
39
+ lines.push(`[Architecture] ${entry.component} (${entry.location || 'location unknown'})`);
40
+ if (entry.does) lines.push(` ${entry.does}`);
41
+ if (entry.depends_on?.length) lines.push(` Depends on: ${entry.depends_on.join(', ')}`);
42
+ if (entry.depended_by?.length) lines.push(` Used by: ${entry.depended_by.join(', ')}`);
43
+ if (entry.gotchas?.length) lines.push(` Gotcha: ${entry.gotchas.join(' | ')}`);
44
+ break;
45
+ case 'threads':
46
+ lines.push(`[Thread] ${entry.thread} — ${(entry.status || 'unknown').toUpperCase()}`);
47
+ if (entry.done?.length) lines.push(` Done: ${entry.done.join(', ')}`);
48
+ if (entry.remaining?.length) lines.push(` Remaining: ${entry.remaining.join(', ')}`);
49
+ if (entry.blocked_by) lines.push(` Blocked by: ${entry.blocked_by}`);
50
+ break;
51
+ case 'warnings':
52
+ lines.push(`[Warning] ${entry.warning}`);
53
+ if (entry.context) lines.push(` Context: ${entry.context}`);
54
+ if (entry.avoid) lines.push(` Avoid: ${entry.avoid}`);
55
+ break;
56
+ }
57
+ return lines.join('\n');
58
+ }
59
+
60
+ module.exports = { extractKeywords, formatEntry };