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.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/bin/arivu.js +2 -0
- package/bin/ascr.js +2 -0
- package/bin/scoops.js +2 -0
- package/bin/zute.js +2 -0
- package/package.json +31 -0
- package/src/cli.js +70 -0
- package/src/commands/export.js +115 -0
- package/src/commands/init.js +110 -0
- package/src/commands/list.js +81 -0
- package/src/commands/prune.js +107 -0
- package/src/commands/status.js +62 -0
- package/src/hooks/prompt-retriever.js +147 -0
- package/src/hooks/session-start.js +82 -0
- package/src/hooks/stop-gate.js +124 -0
- package/src/lib/index.js +65 -0
- package/src/lib/memory.js +49 -0
- package/src/lib/merge-settings.js +59 -0
- package/src/lib/paths.js +38 -0
- package/src/lib/scoring.js +60 -0
|
@@ -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 };
|
package/src/lib/paths.js
ADDED
|
@@ -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 };
|