skills-atlas-cli 0.8.4 → 0.8.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-atlas-cli",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "Search, install and learn AI agent skills from the terminal — powered by the Skills Atlas catalog.",
5
5
  "bin": {
6
6
  "skills-atlas": "bin/skills.js",
@@ -20,8 +20,7 @@ const transcripts = require('../transcripts');
20
20
  const gapstate = require('../gapstate');
21
21
 
22
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)
23
+ const GAP_EVERY = 12; // a gap nudge is only considered at every Nth prompt (per session)
25
24
 
26
25
  function stateFile(sessionId) {
27
26
  const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
@@ -58,12 +57,14 @@ module.exports = async function suggest() {
58
57
  state.count = (state.count || 0) + 1;
59
58
  const ap = registry.getAutopilot();
60
59
 
61
- // --- Proactive gap nudge: periodic + throttled to once/day; Claude judges ---
60
+ // --- Proactive gap nudge: periodic, and refired when the recurring work shifts
61
+ // to something new (anti-spam floor + activity fingerprint, not a daily clock) ---
62
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) {
63
+ const recent = transcripts.recentPrompts({ max: 20 });
64
+ if (recent.length >= 8) {
65
+ const gs = gapstate.read();
66
+ const { fire, sig } = gapstate.shouldNudge(gs, recent, Date.now());
67
+ if (fire) {
67
68
  const dismissed = gs.dismissed || [];
68
69
  const lines = recent.map(r => `- ${r.text.replace(/\s+/g, ' ').slice(0, 100)}`).join('\n');
69
70
  const days = Math.max(1, Math.round((Date.now() - recent[recent.length - 1].ts) / 86400000));
@@ -73,7 +74,7 @@ module.exports = async function suggest() {
73
74
  `\`skills-atlas info <skill>\` and install with \`skills-atlas use <skill> --yes\`.` +
74
75
  (dismissed.length ? ` Already dismissed (skip): ${dismissed.join(', ')}.` : '') +
75
76
  ` If nothing clearly recurs or it doesn't fit right now, stay silent.`);
76
- gapstate.touchNudge();
77
+ gapstate.touchNudge(sig);
77
78
  writeState(file, state);
78
79
  return;
79
80
  }
package/src/gapstate.js CHANGED
@@ -1,10 +1,18 @@
1
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.)
2
+ // when we last proactively nudged, and a fingerprint of what the user was doing at
3
+ // that nudge. (Judgment is Claude's; no prompt text is stored — only token stems.)
3
4
  'use strict';
4
5
 
5
6
  const fs = require('fs');
6
7
  const os = require('os');
7
8
  const path = require('path');
9
+ const { tokenize } = require('./search-core');
10
+
11
+ // Gap nudges are gated by activity, not a wall clock: a short anti-spam floor, then
12
+ // a refire whenever the recurring work shifts to something new (so it can catch the
13
+ // user on their NEXT task), plus a long fallback so a persistent gap can resurface.
14
+ const MIN_INTERVAL_MS = 90 * 60 * 1000; // ~90 min: never two nudges in a burst
15
+ const REFRESH_INTERVAL_MS = 12 * 60 * 60 * 1000; // ~12 h: an unchanged gap may resurface
8
16
 
9
17
  function file() {
10
18
  const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
@@ -19,7 +27,45 @@ function write(s) {
19
27
  }
20
28
  function dismiss(x) { const s = read(); if (x && !s.dismissed.includes(x)) s.dismissed.push(x); write(s); return s; }
21
29
  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 }); }
30
+ function touchNudge(sig) { const s = read(); s.lastNudge = Date.now(); if (sig) s.lastSig = sig; write(s); }
31
+ function clear() { write({ dismissed: [], lastNudge: 0, lastSig: [] }); }
32
+
33
+ // A coarse fingerprint of recent work: the most frequent contentful tokens across
34
+ // recent prompts. When this set shifts, the user has moved to a new kind of work.
35
+ function activitySignature(prompts, topN = 8) {
36
+ const freq = new Map();
37
+ for (const p of prompts || []) {
38
+ const text = typeof p === 'string' ? p : (p && p.text) || '';
39
+ for (const t of tokenize(String(text).toLowerCase())) freq.set(t, (freq.get(t) || 0) + 1);
40
+ }
41
+ return [...freq.entries()]
42
+ .sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))
43
+ .slice(0, topN).map(e => e[0]).sort();
44
+ }
45
+
46
+ // Did the dominant activity shift by more than half (Jaccard < 0.5)? No prior → yes.
47
+ function signatureShifted(sig, prev) {
48
+ if (!prev || !prev.length) return true;
49
+ const a = new Set(sig);
50
+ if (!a.size) return false;
51
+ const b = new Set(prev);
52
+ let inter = 0; for (const x of a) if (b.has(x)) inter++;
53
+ const union = new Set([...a, ...b]).size || 1;
54
+ return inter / union < 0.5;
55
+ }
56
+
57
+ // Smart-refire decision (pure). Fire when past the anti-spam floor AND either the
58
+ // activity shifted to something new, or the long fallback has elapsed.
59
+ function shouldNudge(state, recent, now) {
60
+ const sig = activitySignature(recent);
61
+ const since = now - ((state && state.lastNudge) || 0);
62
+ if (since < MIN_INTERVAL_MS) return { fire: false, sig };
63
+ const fire = signatureShifted(sig, state && state.lastSig) || since >= REFRESH_INTERVAL_MS;
64
+ return { fire, sig };
65
+ }
24
66
 
25
- module.exports = { file, read, write, dismiss, isDismissed, touchNudge, clear };
67
+ module.exports = {
68
+ file, read, write, dismiss, isDismissed, touchNudge, clear,
69
+ activitySignature, signatureShifted, shouldNudge,
70
+ MIN_INTERVAL_MS, REFRESH_INTERVAL_MS,
71
+ };