skills-atlas-cli 0.6.1 โ 0.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/README.md +9 -0
- package/bin/skills.js +3 -1
- package/package.json +1 -1
- package/src/commands/gaps.js +61 -0
- package/src/commands/hook.js +21 -4
- package/src/commands/suggest.js +37 -7
- package/src/gapstate.js +25 -0
- package/src/registry.js +14 -0
- package/src/transcripts.js +73 -0
package/README.md
CHANGED
|
@@ -179,6 +179,15 @@ unless one truly fits, or searches further itself). It's:
|
|
|
179
179
|
- **safe** โ never auto-installs (always your call), and fails open (a hook
|
|
180
180
|
error never blocks your prompt).
|
|
181
181
|
|
|
182
|
+
**๐ญ Capability gaps.** `skills-atlas gaps` shows Claude your *recent activity* and
|
|
183
|
+
lets **Claude** spot the recurring kinds of work you keep doing that no installed
|
|
184
|
+
skill covers yet โ then recommend one, with the pattern as evidence. We don't guess
|
|
185
|
+
with heuristics; we just give Claude the memory it lacks (your recent prompts, read
|
|
186
|
+
from Claude Code's own local transcripts โ **nothing is stored or sent**) plus the
|
|
187
|
+
catalog. With the hook on, it also nudges in-conversation now and then. The two
|
|
188
|
+
layers are independent: `skills-atlas hook suggest on|off` (per-prompt) and
|
|
189
|
+
`skills-atlas hook gaps on|off` (the proactive nudge).
|
|
190
|
+
|
|
182
191
|
## License
|
|
183
192
|
|
|
184
193
|
MIT. Each installed skill keeps its own source repository's license.
|
package/bin/skills.js
CHANGED
|
@@ -14,13 +14,14 @@ const sync = require('../src/commands/sync');
|
|
|
14
14
|
const registry = require('../src/commands/registry');
|
|
15
15
|
const suggest = require('../src/commands/suggest');
|
|
16
16
|
const hook = require('../src/commands/hook');
|
|
17
|
+
const gaps = require('../src/commands/gaps');
|
|
17
18
|
const update = require('../src/commands/update');
|
|
18
19
|
const { categories, list } = require('../src/commands/categories');
|
|
19
20
|
|
|
20
21
|
const VERSION = require('../package.json').version;
|
|
21
22
|
// `use` = install + activate inline (emit the SKILL.md so an agent follows it now).
|
|
22
23
|
const use = argv => install([...argv, '--inline']);
|
|
23
|
-
const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, update, categories, list, registry };
|
|
24
|
+
const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, gaps, update, categories, list, registry };
|
|
24
25
|
|
|
25
26
|
const HELP = `skills-atlas โ search, install & manage AI agent skills
|
|
26
27
|
|
|
@@ -43,6 +44,7 @@ manage what you've installed:
|
|
|
43
44
|
|
|
44
45
|
autopilot (opt-in):
|
|
45
46
|
hook on|off|status proactively suggest a skill in Claude when your prompt fits one
|
|
47
|
+
gaps kinds of work you keep doing without a skill (run: skills-atlas hook on)
|
|
46
48
|
|
|
47
49
|
catalog:
|
|
48
50
|
update refresh the catalog from the public data feed
|
package/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// `skills-atlas gaps` โ surface the user's recent activity + a judging instruction
|
|
2
|
+
// so CLAUDE (running this in Claude Code) spots recurring needs no skill covers yet.
|
|
3
|
+
// We provide memory (recent prompts) + the ask; Claude does the judgment. No network.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const { parse } = require('../args');
|
|
7
|
+
const transcripts = require('../transcripts');
|
|
8
|
+
const gapstate = require('../gapstate');
|
|
9
|
+
const { dim, green } = require('../format');
|
|
10
|
+
|
|
11
|
+
const HELP = `usage: skills-atlas gaps [dismiss <skill> | clear]
|
|
12
|
+
|
|
13
|
+
Surface your recent Claude Code activity so Claude can spot recurring kinds of work
|
|
14
|
+
no installed skill covers yet. Run it in Claude Code (or ask Claude "any skills I
|
|
15
|
+
should get?"). Reads your local transcripts; nothing is stored or sent.
|
|
16
|
+
|
|
17
|
+
gaps review recent activity โ Claude recommends
|
|
18
|
+
gaps dismiss <skill> stop recommending one
|
|
19
|
+
gaps clear reset dismissals
|
|
20
|
+
--json`;
|
|
21
|
+
|
|
22
|
+
const INSTRUCTION = dismissed =>
|
|
23
|
+
`Identify any recurring KIND of work the user keeps doing that an installable catalog skill ` +
|
|
24
|
+
`is built for and they haven't installed. For each real recurring need (ignore one-offs and ` +
|
|
25
|
+
`anything already covered): state the pattern + rough frequency as evidence, then recommend the ` +
|
|
26
|
+
`skill โ verify it exists with \`skills-atlas search "<intent>"\` or \`skills-atlas info <skill>\`, ` +
|
|
27
|
+
`and install with \`skills-atlas use <skill> --yes\`.` +
|
|
28
|
+
(dismissed.length ? ` Already dismissed (skip these): ${dismissed.join(', ')}.` : '') +
|
|
29
|
+
` If nothing clearly recurs, say there are no gaps.`;
|
|
30
|
+
|
|
31
|
+
module.exports = async function gapsCmd(argv) {
|
|
32
|
+
const { values, positionals } = parse(argv, ['json', 'yes']);
|
|
33
|
+
if (values.help) { console.log(HELP); return; }
|
|
34
|
+
const sub = positionals[0];
|
|
35
|
+
|
|
36
|
+
if (sub === 'clear') {
|
|
37
|
+
gapstate.clear();
|
|
38
|
+
console.log(values.json ? JSON.stringify({ cleared: true }) : `${green('โ')} dismissals reset.`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (sub === 'dismiss') {
|
|
42
|
+
const x = positionals.slice(1).join(' ');
|
|
43
|
+
if (!x) { console.error('usage: skills-atlas gaps dismiss <skill>'); process.exitCode = 1; return; }
|
|
44
|
+
gapstate.dismiss(x);
|
|
45
|
+
console.log(values.json ? JSON.stringify({ dismissed: x }) : `${green('โ')} dismissed: ${x}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const recent = transcripts.recentPrompts({ max: 60 });
|
|
50
|
+
const dismissed = (gapstate.read().dismissed) || [];
|
|
51
|
+
if (values.json) { console.log(JSON.stringify({ recent, dismissed }, null, 2)); return; }
|
|
52
|
+
|
|
53
|
+
if (!recent.length) {
|
|
54
|
+
console.log(dim('no recent activity found to analyze.'));
|
|
55
|
+
console.log(dim('gaps are spotted from your recent Claude Code prompts โ use it for a while, then run this in a session.'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const days = Math.max(1, Math.round((Date.now() - recent[recent.length - 1].ts) / 86400000));
|
|
59
|
+
const lines = recent.slice(0, 40).map(r => ` - ${r.text.replace(/\s+/g, ' ').slice(0, 100)}`).join('\n');
|
|
60
|
+
console.log(`\n[Skills Atlas โ capability gaps] The user's recent requests across sessions (${recent.length} over ~${days} day(s), newest first):\n${lines}\n\n${INSTRUCTION(dismissed)}`);
|
|
61
|
+
};
|
package/src/commands/hook.js
CHANGED
|
@@ -9,6 +9,7 @@ const os = require('os');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { parse } = require('../args');
|
|
11
11
|
const { green, dim } = require('../format');
|
|
12
|
+
const registry = require('../registry');
|
|
12
13
|
|
|
13
14
|
const HOOK_CMD = 'skills-atlas suggest';
|
|
14
15
|
const settingsPath = () => path.join(os.homedir(), '.claude', 'settings.json');
|
|
@@ -22,21 +23,37 @@ const isOurs = e => e && Array.isArray(e.hooks)
|
|
|
22
23
|
|
|
23
24
|
module.exports = async function hook(argv) {
|
|
24
25
|
const { values, positionals } = parse(argv, ['json']);
|
|
25
|
-
if (values.help) { console.log('usage: skills-atlas hook <on|off|status>'); return; }
|
|
26
|
+
if (values.help) { console.log('usage: skills-atlas hook <on|off|status|suggest on|off|gaps on|off>'); return; }
|
|
26
27
|
const sub = positionals[0] || 'status';
|
|
27
28
|
const p = settingsPath();
|
|
28
29
|
|
|
30
|
+
if (sub === 'suggest' || sub === 'gaps') {
|
|
31
|
+
const onoff = positionals[1];
|
|
32
|
+
if (onoff !== 'on' && onoff !== 'off') {
|
|
33
|
+
console.error(`usage: skills-atlas hook ${sub} <on|off>`); process.exitCode = 1; return;
|
|
34
|
+
}
|
|
35
|
+
const key = sub === 'suggest' ? 'suggest' : 'gapAlerts';
|
|
36
|
+
registry.setAutopilot({ [key]: onoff === 'on' });
|
|
37
|
+
const label = sub === 'suggest' ? 'per-prompt autopilot' : 'gap alerts';
|
|
38
|
+
console.log(`${green('โ')} ${label}: ${onoff === 'on' ? green('on') : dim('off')}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
if (sub === 'status') {
|
|
30
43
|
let on = false;
|
|
31
44
|
try { on = ((readSettings(p).hooks || {}).UserPromptSubmit || []).some(isOurs); } catch { /* invalid โ off */ }
|
|
32
|
-
|
|
33
|
-
console.log(
|
|
45
|
+
const ap = registry.getAutopilot();
|
|
46
|
+
if (values.json) { console.log(JSON.stringify({ enabled: on, suggest: ap.suggest, gapAlerts: ap.gapAlerts, settings: p })); return; }
|
|
47
|
+
console.log(`autopilot hook: ${on ? green('on') : dim('off')} ${dim(p)}`);
|
|
48
|
+
console.log(` per-prompt suggest: ${ap.suggest ? green('on') : dim('off')} ${dim('(skills-atlas hook suggest on|off)')}`);
|
|
49
|
+
console.log(` gap alerts: ${ap.gapAlerts ? green('on') : dim('off')} ${dim('(skills-atlas hook gaps on|off)')}`);
|
|
50
|
+
if (on) console.log(dim(' review gaps: skills-atlas gaps'));
|
|
34
51
|
if (!on) console.log(dim('enable: skills-atlas hook on'));
|
|
35
52
|
return;
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
if (sub !== 'on' && sub !== 'off') {
|
|
39
|
-
console.error('usage: skills-atlas hook <on|off|status>');
|
|
56
|
+
console.error('usage: skills-atlas hook <on|off|status|suggest on|off|gaps on|off>');
|
|
40
57
|
process.exitCode = 1;
|
|
41
58
|
return;
|
|
42
59
|
}
|
package/src/commands/suggest.js
CHANGED
|
@@ -15,8 +15,13 @@ const { buildIndices } = require('../index-build');
|
|
|
15
15
|
const { suggestCandidates } = require('../search-core');
|
|
16
16
|
const manifest = require('../manifest');
|
|
17
17
|
const fsu = require('../fsutil');
|
|
18
|
+
const registry = require('../registry');
|
|
19
|
+
const transcripts = require('../transcripts');
|
|
20
|
+
const gapstate = require('../gapstate');
|
|
18
21
|
|
|
19
22
|
const COOLDOWN = 3; // min prompts between suggestions
|
|
23
|
+
const GAP_EVERY = 12; // earliest a gap nudge may fire (per session)
|
|
24
|
+
const NUDGE_COOLDOWN_MS = 24 * 3600000; // and at most one gap nudge per day (across sessions)
|
|
20
25
|
|
|
21
26
|
function stateFile(sessionId) {
|
|
22
27
|
const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
@@ -31,6 +36,10 @@ function writeState(f, s) {
|
|
|
31
36
|
try { fs.mkdirSync(path.dirname(f), { recursive: true }); fs.writeFileSync(f, JSON.stringify(s)); } catch { /* ignore */ }
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
function emit(ctx) {
|
|
40
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: ctx } }));
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
module.exports = async function suggest() {
|
|
35
44
|
try {
|
|
36
45
|
if (process.stdin.isTTY) {
|
|
@@ -47,25 +56,48 @@ module.exports = async function suggest() {
|
|
|
47
56
|
const file = stateFile(event.session_id || event.sessionId);
|
|
48
57
|
const state = readState(file);
|
|
49
58
|
state.count = (state.count || 0) + 1;
|
|
59
|
+
const ap = registry.getAutopilot();
|
|
60
|
+
|
|
61
|
+
// --- Proactive gap nudge: periodic + throttled to once/day; Claude judges ---
|
|
62
|
+
if (ap.gapAlerts && state.count % GAP_EVERY === 0) {
|
|
63
|
+
const gs = gapstate.read();
|
|
64
|
+
if (Date.now() - (gs.lastNudge || 0) >= NUDGE_COOLDOWN_MS) {
|
|
65
|
+
const recent = transcripts.recentPrompts({ max: 20 });
|
|
66
|
+
if (recent.length >= 8) {
|
|
67
|
+
const dismissed = gs.dismissed || [];
|
|
68
|
+
const lines = recent.map(r => `- ${r.text.replace(/\s+/g, ' ').slice(0, 100)}`).join('\n');
|
|
69
|
+
const days = Math.max(1, Math.round((Date.now() - recent[recent.length - 1].ts) / 86400000));
|
|
70
|
+
emit(`[Skills Atlas โ capability gaps] The user's recent requests (${recent.length} over ~${days} day(s), newest first):\n${lines}\n` +
|
|
71
|
+
`If a recurring KIND of work stands out that an installable catalog skill is built for and they haven't ` +
|
|
72
|
+
`installed (ignore one-offs), name the pattern with rough frequency as evidence and recommend it โ verify with ` +
|
|
73
|
+
`\`skills-atlas info <skill>\` and install with \`skills-atlas use <skill> --yes\`.` +
|
|
74
|
+
(dismissed.length ? ` Already dismissed (skip): ${dismissed.join(', ')}.` : '') +
|
|
75
|
+
` If nothing clearly recurs or it doesn't fit right now, stay silent.`);
|
|
76
|
+
gapstate.touchNudge();
|
|
77
|
+
writeState(file, state);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
50
82
|
|
|
51
|
-
//
|
|
83
|
+
// --- Per-prompt autopilot (gated by the suggest toggle + cooldown) ---
|
|
84
|
+
if (!ap.suggest) { writeState(file, state); return; }
|
|
52
85
|
if (state.count - (state.lastSuggestedCount ?? -COOLDOWN) < COOLDOWN) { writeState(file, state); return; }
|
|
53
86
|
|
|
54
87
|
const { data } = loadData({ quiet: true });
|
|
55
88
|
const { flatRows } = buildIndices(data);
|
|
56
|
-
|
|
57
89
|
const installed = new Set();
|
|
58
90
|
for (const s of fsu.scopesFor({})) for (const e of manifest.list(s.root)) installed.add(e.skill);
|
|
59
91
|
const suggested = new Set(state.suggested || []);
|
|
60
92
|
|
|
61
93
|
const { fire, candidates } = suggestCandidates(flatRows, prompt, { installed, suggested });
|
|
62
|
-
if (!fire) { writeState(file, state); return; }
|
|
94
|
+
if (!fire || !candidates.length) { writeState(file, state); return; }
|
|
63
95
|
|
|
64
96
|
const lines = candidates.map(c => {
|
|
65
97
|
const uc = (c.row.use_case_en || c.row.use_case || '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
66
98
|
return `- ${c.skill}${uc ? ` โ ${uc}` : ''} (details: \`skills-atlas info ${c.skill}\` ยท use now: \`skills-atlas use ${c.skill} --yes\`)`;
|
|
67
99
|
}).join('\n');
|
|
68
|
-
|
|
100
|
+
emit(
|
|
69
101
|
`[Skills Atlas autopilot] The user may be doing something one of these installable agent ` +
|
|
70
102
|
`skills is built for. Judge for yourself โ do NOT mention any of this unless one of them ` +
|
|
71
103
|
`genuinely fits what they actually asked:\n${lines}\n` +
|
|
@@ -75,9 +107,7 @@ module.exports = async function suggest() {
|
|
|
75
107
|
`immediately), see what it covers first (\`skills-atlas info <skill>\`), or skip and you'll just ` +
|
|
76
108
|
`do the task yourself. If none fit but the task plainly needs a specialized skill, you may run ` +
|
|
77
109
|
`\`skills-atlas search "<short intent>"\` to look further. If nothing fits, say nothing about this ` +
|
|
78
|
-
`at all โ don't mention this hook, these skills, or that a suggestion was made
|
|
79
|
-
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: ctx } }));
|
|
80
|
-
|
|
110
|
+
`at all โ don't mention this hook, these skills, or that a suggestion was made.`);
|
|
81
111
|
state.lastSuggestedCount = state.count;
|
|
82
112
|
state.suggested = [...suggested, ...candidates.map(c => c.skill)];
|
|
83
113
|
writeState(file, state);
|
package/src/gapstate.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The only thing capability-gaps persists: which suggestions the user dismissed,
|
|
2
|
+
// and when we last proactively nudged. (Judgment is Claude's; no prompt text here.)
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
function file() {
|
|
10
|
+
const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
11
|
+
return path.join(base, 'skills-atlas', 'gaps.json');
|
|
12
|
+
}
|
|
13
|
+
function read() {
|
|
14
|
+
try { const s = JSON.parse(fs.readFileSync(file(), 'utf8')); if (!Array.isArray(s.dismissed)) s.dismissed = []; return s; }
|
|
15
|
+
catch { return { dismissed: [], lastNudge: 0 }; }
|
|
16
|
+
}
|
|
17
|
+
function write(s) {
|
|
18
|
+
try { fs.mkdirSync(path.dirname(file()), { recursive: true }); fs.writeFileSync(file(), JSON.stringify(s)); } catch { /* ignore */ }
|
|
19
|
+
}
|
|
20
|
+
function dismiss(x) { const s = read(); if (x && !s.dismissed.includes(x)) s.dismissed.push(x); write(s); return s; }
|
|
21
|
+
function isDismissed(x) { return read().dismissed.includes(x); }
|
|
22
|
+
function touchNudge() { const s = read(); s.lastNudge = Date.now(); write(s); }
|
|
23
|
+
function clear() { write({ dismissed: [], lastNudge: 0 }); }
|
|
24
|
+
|
|
25
|
+
module.exports = { file, read, write, dismiss, isDismissed, touchNudge, clear };
|
package/src/registry.js
CHANGED
|
@@ -72,7 +72,21 @@ function removeCachedSource(url) {
|
|
|
72
72
|
try { fs.rmSync(sourceCachePath(url), { force: true }); } catch { /* ignore */ }
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Autopilot toggles (per-prompt suggest + proactive gap alerts) share this
|
|
76
|
+
// config.json; read-modify-write the full object so registry `sources` is preserved.
|
|
77
|
+
function getAutopilot() {
|
|
78
|
+
const c = readConfig();
|
|
79
|
+
return { suggest: true, gapAlerts: true, ...(c.autopilot || {}) };
|
|
80
|
+
}
|
|
81
|
+
function setAutopilot(patch) {
|
|
82
|
+
const c = readConfig();
|
|
83
|
+
c.autopilot = { ...getAutopilot(), ...patch };
|
|
84
|
+
writeConfig(c);
|
|
85
|
+
return c.autopilot;
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
module.exports = {
|
|
76
89
|
configDir, configFile, readConfig, writeConfig, addSource, removeSource, listSources,
|
|
77
90
|
effectiveSources, sourceCachePath, cacheSource, readCachedSource, removeCachedSource,
|
|
91
|
+
getAutopilot, setAutopilot,
|
|
78
92
|
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Read the user's recent TYPED prompts from Claude Code's local session transcripts
|
|
2
|
+
// (JSONL under ~/.claude/projects/<proj>/<session>.jsonl). We store nothing new โ
|
|
3
|
+
// this is the user's own data, read locally, only ever shown to the local Claude.
|
|
4
|
+
// Validated against real transcripts: a typed prompt has type:"user",
|
|
5
|
+
// message.role:"user", and a STRING content (tool results have array content).
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const DAY = 86400000;
|
|
13
|
+
const MAX_BYTES = 1_500_000; // cap per-file read (hook runs under a 5s timeout); recent prompts are at the file's tail
|
|
14
|
+
const projectsDir = () => path.join(os.homedir(), '.claude', 'projects');
|
|
15
|
+
|
|
16
|
+
function fromFile(file, sinceMs) {
|
|
17
|
+
const out = [];
|
|
18
|
+
let text;
|
|
19
|
+
try {
|
|
20
|
+
const st = fs.statSync(file);
|
|
21
|
+
if (st.size <= MAX_BYTES) {
|
|
22
|
+
text = fs.readFileSync(file, 'utf8');
|
|
23
|
+
} else {
|
|
24
|
+
// tail-read only the last MAX_BYTES, then drop the partial first line
|
|
25
|
+
const fd = fs.openSync(file, 'r');
|
|
26
|
+
try {
|
|
27
|
+
const b = Buffer.alloc(MAX_BYTES);
|
|
28
|
+
fs.readSync(fd, b, 0, MAX_BYTES, st.size - MAX_BYTES);
|
|
29
|
+
text = b.toString('utf8');
|
|
30
|
+
} finally { fs.closeSync(fd); }
|
|
31
|
+
const nl = text.indexOf('\n');
|
|
32
|
+
text = nl >= 0 ? text.slice(nl + 1) : text;
|
|
33
|
+
}
|
|
34
|
+
} catch { return out; }
|
|
35
|
+
for (const line of text.split('\n')) {
|
|
36
|
+
if (!line) continue;
|
|
37
|
+
let o; try { o = JSON.parse(line); } catch { continue; }
|
|
38
|
+
if (o.type !== 'user' || o.isSidechain) continue;
|
|
39
|
+
const m = o.message;
|
|
40
|
+
if (!m || m.role !== 'user' || typeof m.content !== 'string') continue;
|
|
41
|
+
const t = m.content.trim();
|
|
42
|
+
if (!t || t.startsWith('<') || t.includes('<command-name>') || t.includes('<local-command')) continue;
|
|
43
|
+
const ts = Date.parse(o.timestamp);
|
|
44
|
+
if (!Number.isFinite(ts) || (sinceMs && ts < sinceMs)) continue; // drop missing/unparseable/out-of-window
|
|
45
|
+
out.push({ text: t, ts, cwd: o.cwd || null });
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function recentPrompts({ windowDays = 21, max = 60, now = Date.now() } = {}) {
|
|
51
|
+
const sinceMs = now - windowDays * DAY;
|
|
52
|
+
const dir = projectsDir();
|
|
53
|
+
const files = [];
|
|
54
|
+
try {
|
|
55
|
+
for (const proj of fs.readdirSync(dir)) {
|
|
56
|
+
const pdir = path.join(dir, proj);
|
|
57
|
+
let entries; try { entries = fs.readdirSync(pdir); } catch { continue; }
|
|
58
|
+
for (const f of entries) {
|
|
59
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
60
|
+
const fp = path.join(pdir, f);
|
|
61
|
+
let mt = 0; try { mt = fs.statSync(fp).mtimeMs; } catch { /* ignore */ }
|
|
62
|
+
if (mt >= sinceMs) files.push({ fp, mt });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch { return []; }
|
|
66
|
+
files.sort((a, b) => b.mt - a.mt);
|
|
67
|
+
let all = [];
|
|
68
|
+
for (const { fp } of files.slice(0, 40)) all = all.concat(fromFile(fp, sinceMs));
|
|
69
|
+
all.sort((a, b) => b.ts - a.ts);
|
|
70
|
+
return all.slice(0, max);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { recentPrompts, fromFile, projectsDir };
|