sshlg-skills 1.77.0 → 1.78.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/CHANGELOG.md +20 -0
- package/README.md +13 -1
- package/bin/sshlg-skills.js +75 -12
- package/lib/visibility-state.js +2 -0
- package/lib/visibility.js +56 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## v1.78.0 (2026-10-24): quiet core skills listed by name only
|
|
2
|
+
|
|
3
|
+
- **B-254:** `visibility` lists quiet core skills by name only on Claude Code. Measured
|
|
4
|
+
2026-10-24 on Claude Code 2.1.299: `Skill listing over budget: 132 skills, 61725 chars > 30000
|
|
5
|
+
budget`; over budget the host keeps bundled skills whole, gives the rest their description
|
|
6
|
+
most-used first and lists the remainder by name. A personal skill in the core that was not
|
|
7
|
+
used within 14 days and is not named in `~/.claude/CLAUDE.md` now gets
|
|
8
|
+
`skillOverrides: "name-only"` (on this machine 22 skills, the launcher's estimate 46,402 to
|
|
9
|
+
35,853 characters). `--full-days N|off`; ownership of each value is recorded
|
|
10
|
+
(`claudeNameOnly`), `--revert` removes only values still as written, and `update` re-applies
|
|
11
|
+
the tier only after an `--apply` that recorded `fullDays`. The listing estimate now counts
|
|
12
|
+
Claude's `- name: description` form, and the preview prints Claude Code's own last count
|
|
13
|
+
from its debug logs. `test/visibility_test.js` 22 to 29 checks,
|
|
14
|
+
`test/visibility_e2e_test.js` 11 to 15. Release impact card:
|
|
15
|
+
`docs/evidence/specs/2026-10-24-v1780-release-impact.md`.
|
|
16
|
+
- **Context audit, the same request:** B-255 (the listing still over budget: family and foreign
|
|
17
|
+
plugin descriptions) and B-256 (the 15,410-character routing block in every host file) are
|
|
18
|
+
open rows with their measurements; oversized project instruction files are tracked in the
|
|
19
|
+
HANDOFF entry.
|
|
20
|
+
|
|
1
21
|
## v1.77.0 (2026-10-24): every release with users carries its impact card
|
|
2
22
|
|
|
3
23
|
- **Pins:** task-pipeline 1.102.0, the release impact card (operator, 2026-10-24): a release in a
|
package/README.md
CHANGED
|
@@ -686,6 +686,17 @@ hides every other installed pack; an existing file is replaced only with `--repl
|
|
|
686
686
|
so a plugin's skills stay listed (switch a foreign plugin off with `enabledPlugins`).
|
|
687
687
|
The pack marks and the overrides below do not change them; `--keep <skill>` keeps a
|
|
688
688
|
skill listed there.
|
|
689
|
+
- **Claude Code name-only tier** (B-254, since v1.78.0): a personal skill in the core that
|
|
690
|
+
was not used within 14 days and is not named in `~/.claude/CLAUDE.md` stays listed by name
|
|
691
|
+
without its description (`skillOverrides: "name-only"`). Claude Code 2.1.299 does the same
|
|
692
|
+
on its own once the listing is over budget (bundled skills whole, the others' descriptions
|
|
693
|
+
most-used first, the rest by name), so this picks the quiet ones by the operator's usage
|
|
694
|
+
instead of the host's order. `--full-days N` sets the window, `--full-days off` turns the
|
|
695
|
+
tier off. `update` re-applies it only after one `visibility --apply` that recorded
|
|
696
|
+
`fullDays`; an older record re-applies without it. The preview prints the launcher's
|
|
697
|
+
estimate of the listing and Claude Code's own last count from its debug logs (the one
|
|
698
|
+
`Skill listing over budget` line); the estimate leaves out bundled skills, plugin commands
|
|
699
|
+
and plugins loaded from other roots, so the logged count is the one to trust.
|
|
689
700
|
- **Gemini, Pi, OpenCode and Hermes** hide the skills of marked packs, except the usage
|
|
690
701
|
core. Strongest first: a family pack is never hidden; the usage core is never hidden
|
|
691
702
|
(family skills, skills used in the last 60 days by Claude Code usage, skills
|
|
@@ -698,12 +709,13 @@ hides every other installed pack; an existing file is replaced only with `--repl
|
|
|
698
709
|
npx sshlg-skills config set visibility.pack.anthropics/financial-services show # or hide, or default
|
|
699
710
|
npx sshlg-skills visibility # preview: per-host counts, nothing written
|
|
700
711
|
npx sshlg-skills visibility --apply # write each host's own switch, backups first; records consent
|
|
712
|
+
npx sshlg-skills visibility --apply --full-days off # Claude: keep every core description
|
|
701
713
|
npx sshlg-skills visibility --revert # remove exactly what --apply wrote; withdraws consent
|
|
702
714
|
```
|
|
703
715
|
|
|
704
716
|
| Host | The switch `visibility` writes | Source | A hidden skill can still |
|
|
705
717
|
|---|---|---|---|
|
|
706
|
-
| Claude Code | `skillOverrides: "user-invocable-only"` in `~/.claude/settings.json`; plugin skills are out of its reach | since v1.55.0; Claude Code settings reference: overrides do not apply to plugin skills | be run from the `/` menu by a person |
|
|
718
|
+
| Claude Code | `skillOverrides: "user-invocable-only"` (hidden) or `"name-only"` (quiet core) in `~/.claude/settings.json`; plugin skills are out of its reach | since v1.55.0; Claude Code settings reference: overrides do not apply to plugin skills | be run from the `/` menu by a person |
|
|
707
719
|
| Codex | `enabled = false` entries in `~/.codex/config.toml` `[skills] config` | since v1.55.0 | be read from its SKILL.md path when asked |
|
|
708
720
|
| Gemini CLI 0.63.0 | `skills.disabled` in `~/.gemini/settings.json`, the list `gemini skills disable <name> --scope user` writes; read back with `gemini skills list` (`[Disabled]`) | `packages/cli/src/utils/skillSettings.ts`; `SkillManager.getSkills()` drops disabled skills | come back with `gemini skills enable <name>` |
|
|
709
721
|
| Pi 1.1.0 | `-<path>` exact exclusions in the `skills` array of `~/.pi/agent/settings.json`, one per folder holding the skill (`~/.agents/skills`, `~/.pi/agent/skills`); the file is created if absent and removed again by `--revert` | Pi's own docs, settings → Resources; `isEnabledByOverrides` in its `dist/core/package-manager.js` | be loaded for one session with `pi --skill <path>` |
|
package/bin/sshlg-skills.js
CHANGED
|
@@ -314,8 +314,9 @@ Usage:
|
|
|
314
314
|
# report header + footer, links looked up
|
|
315
315
|
npx sshlg-skills toolkit [--for "<task>"] [--find "<concept>, …"] [--expand <provider>]
|
|
316
316
|
# every skill this machine can reach
|
|
317
|
-
npx sshlg-skills visibility [--apply | --revert] [--days 60] [--keep a,b]
|
|
317
|
+
npx sshlg-skills visibility [--apply | --revert] [--days 60] [--full-days 14|off] [--keep a,b]
|
|
318
318
|
# Claude/Codex: hide all outside the core;
|
|
319
|
+
# Claude: quiet core skills name-only;
|
|
319
320
|
# Gemini/Pi/OpenCode/Hermes: hide marked
|
|
320
321
|
# packs; Cursor/Kimi: cannot hide
|
|
321
322
|
npx sshlg-skills visibility marks [--init --show <owner/repo,…> [--replace] [--apply]]
|
|
@@ -773,6 +774,9 @@ function reapplyVisibility(f) {
|
|
|
773
774
|
log(`\n== Skill visibility (re-applied: this machine ran \`visibility --apply\`; hosts: ${[...only].join(', ')}) ==`);
|
|
774
775
|
return runVisibility({
|
|
775
776
|
mode: 'apply', days: Number(rec.days) || undefined, keep: Array.isArray(rec.keep) ? rec.keep : [], only, reapply: true,
|
|
777
|
+
// A record from before the name-only tier carries no fullDays: re-apply keeps the old
|
|
778
|
+
// behaviour until the operator runs `visibility --apply` once (B-254, explicit consent).
|
|
779
|
+
fullDays: typeof rec.fullDays === 'number' ? rec.fullDays : null,
|
|
776
780
|
}) === 0;
|
|
777
781
|
}
|
|
778
782
|
|
|
@@ -2139,9 +2143,19 @@ function cmdVisibility(argv) {
|
|
|
2139
2143
|
mode: flag('--revert') ? 'revert' : flag('--apply') ? 'apply' : 'preview',
|
|
2140
2144
|
days: Number(value('--days')) || V.DEFAULT_DAYS,
|
|
2141
2145
|
keep: (value('--keep') || '').split(',').map((s) => s.trim()).filter(Boolean),
|
|
2146
|
+
fullDays: fullDaysArg(value('--full-days'), V.DEFAULT_FULL_DAYS),
|
|
2142
2147
|
});
|
|
2143
2148
|
}
|
|
2144
2149
|
|
|
2150
|
+
/** `--full-days N` (a whole number of days), `--full-days off` (no name-only tier), or the default. */
|
|
2151
|
+
function fullDaysArg(raw, dflt) {
|
|
2152
|
+
if (raw === null) return dflt;
|
|
2153
|
+
if (raw === 'off') return null;
|
|
2154
|
+
const n = Number(raw);
|
|
2155
|
+
if (!Number.isInteger(n) || n < 0) { log(`visibility: --full-days takes a whole number of days or "off", got "${raw}"; using ${dflt}`); return dflt; }
|
|
2156
|
+
return n;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2145
2159
|
/**
|
|
2146
2160
|
* The work of `visibility`, callable from `update` (which re-applies it on a machine whose
|
|
2147
2161
|
* record shows the operator ran `--apply`). `opts.only` limits the run to a set of host
|
|
@@ -2163,6 +2177,29 @@ function runVisibility(opts) {
|
|
|
2163
2177
|
finally { lock.release(home, got.token); }
|
|
2164
2178
|
}
|
|
2165
2179
|
|
|
2180
|
+
/**
|
|
2181
|
+
* Claude Code's own last listing count, read from its debug logs (newest first). Only the
|
|
2182
|
+
* one warning line is parsed; nothing else in a log is read into the output.
|
|
2183
|
+
*/
|
|
2184
|
+
function lastListingMeasure(claudeRoot) {
|
|
2185
|
+
const dir = path.join(claudeRoot, 'debug');
|
|
2186
|
+
let files = [];
|
|
2187
|
+
try {
|
|
2188
|
+
files = fs.readdirSync(dir).filter((n) => n.endsWith('.txt'))
|
|
2189
|
+
.map((n) => ({ n, m: fs.statSync(path.join(dir, n)).mtimeMs })).sort((a, b) => b.m - a.m).slice(0, 40);
|
|
2190
|
+
} catch (e) { return null; }
|
|
2191
|
+
const re = /^(\S+) \[WARN\] Skill listing over budget: (\d+) skills, (\d+) chars > (\d+) budget/m;
|
|
2192
|
+
for (const f of files) {
|
|
2193
|
+
let text;
|
|
2194
|
+
try { text = fs.readFileSync(path.join(dir, f.n), 'utf8'); } catch (e) { continue; }
|
|
2195
|
+
const all = text.match(new RegExp(re.source, 'gm'));
|
|
2196
|
+
if (!all) continue;
|
|
2197
|
+
const m = re.exec(all[all.length - 1]);
|
|
2198
|
+
return { at: m[1], skills: Number(m[2]), chars: Number(m[3]), budget: Number(m[4]) };
|
|
2199
|
+
}
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2166
2203
|
function runVisibilityUnlocked(opts) {
|
|
2167
2204
|
const V = require(path.join(ROOT, 'lib', 'visibility.js'));
|
|
2168
2205
|
const HV = require(path.join(ROOT, 'lib', 'hostvis.js'));
|
|
@@ -2179,6 +2216,8 @@ function runVisibilityUnlocked(opts) {
|
|
|
2179
2216
|
const doRevert = opts.mode === 'revert';
|
|
2180
2217
|
const days = opts.days || V.DEFAULT_DAYS;
|
|
2181
2218
|
const keep = opts.keep || [];
|
|
2219
|
+
// null: no name-only tier (a re-apply of a record that predates it, or `--full-days off`).
|
|
2220
|
+
const fullDays = typeof opts.fullDays === 'number' ? opts.fullDays : null;
|
|
2182
2221
|
const inScope = (id) => !opts.only || opts.only.has(id);
|
|
2183
2222
|
|
|
2184
2223
|
const recordPath = path.join(home, '.sshlg-skills', 'visibility.json');
|
|
@@ -2226,11 +2265,24 @@ function runVisibilityUnlocked(opts) {
|
|
|
2226
2265
|
try { settingsData = JSON.parse(settingsText || '{}'); } catch (e) { settingsData = {}; }
|
|
2227
2266
|
const overrides = settingsData.skillOverrides || {};
|
|
2228
2267
|
const mine = new Set(owned.claude.filter((n) => overrides[n] === V.HIDE));
|
|
2268
|
+
const ownedNameOnly = Array.isArray(owned.claudeNameOnly) ? owned.claudeNameOnly : [];
|
|
2269
|
+
const mineNameOnly = new Set(ownedNameOnly.filter((n) => overrides[n] === V.NAME_ONLY));
|
|
2229
2270
|
const foreign = {};
|
|
2230
|
-
for (const [k, v] of Object.entries(overrides)) if (!mine.has(k)) foreign[k] = v;
|
|
2231
|
-
const
|
|
2232
|
-
|
|
2233
|
-
const
|
|
2271
|
+
for (const [k, v] of Object.entries(overrides)) if (!mine.has(k) && !mineNameOnly.has(k)) foreign[k] = v;
|
|
2272
|
+
const full = doRevert || fullDays === null ? null
|
|
2273
|
+
: V.fullSet({ personal, usage, now, fullDays, instructions });
|
|
2274
|
+
const plan = doRevert ? { hide: [], nameOnly: [], respected: [] }
|
|
2275
|
+
: V.claudePlan({ personal, core: coreSet, full, overrides: foreign, keep });
|
|
2276
|
+
// Values this run writes, and values it removes (only while they still hold what it wrote).
|
|
2277
|
+
const claudeSetMap = {};
|
|
2278
|
+
for (const n of plan.hide) if (overrides[n] !== V.HIDE) claudeSetMap[n] = V.HIDE;
|
|
2279
|
+
for (const n of plan.nameOnly) if (overrides[n] !== V.NAME_ONLY) claudeSetMap[n] = V.NAME_ONLY;
|
|
2280
|
+
const planned = new Set(plan.hide.concat(plan.nameOnly));
|
|
2281
|
+
const claudeUnsetMap = {};
|
|
2282
|
+
for (const n of mine) if (!planned.has(n)) claudeUnsetMap[n] = V.HIDE;
|
|
2283
|
+
for (const n of mineNameOnly) if (!planned.has(n)) claudeUnsetMap[n] = V.NAME_ONLY;
|
|
2284
|
+
const claudeSet = Object.keys(claudeSetMap);
|
|
2285
|
+
const claudeUnset = Object.keys(claudeUnsetMap);
|
|
2234
2286
|
const stateOf = (s, ov) => {
|
|
2235
2287
|
if (s.loaded === false) return 'hidden'; // installed, not loaded here (B-185): not in the listing
|
|
2236
2288
|
if (s.namespace !== 'claude:plain') return 'on';
|
|
@@ -2239,7 +2291,7 @@ function runVisibilityUnlocked(opts) {
|
|
|
2239
2291
|
return v === 'name-only' ? 'name-only' : 'on';
|
|
2240
2292
|
};
|
|
2241
2293
|
const after = Object.assign({}, overrides);
|
|
2242
|
-
for (const n of claudeSet) after[n] =
|
|
2294
|
+
for (const n of claudeSet) after[n] = claudeSetMap[n];
|
|
2243
2295
|
for (const n of claudeUnset) delete after[n];
|
|
2244
2296
|
const charsBefore = V.listingChars(skills.map((s) => ({ name: s.plugin && s.namespace === 'claude:plugin' ? `${s.plugin.split('@')[0]}:${s.id}` : s.id, description: s.description, state: stateOf(s, overrides) })));
|
|
2245
2297
|
const charsAfter = V.listingChars(skills.map((s) => ({ name: s.plugin && s.namespace === 'claude:plugin' ? `${s.plugin.split('@')[0]}:${s.id}` : s.id, description: s.description, state: stateOf(s, after) })));
|
|
@@ -2336,11 +2388,21 @@ function runVisibilityUnlocked(opts) {
|
|
|
2336
2388
|
log(` overrides (config.json): hidePacks ${(ov.hidePacks || []).length}, showPacks ${(ov.showPacks || []).length} — Gemini, Pi, OpenCode and Hermes only`);
|
|
2337
2389
|
}
|
|
2338
2390
|
if (inScope('claude')) {
|
|
2339
|
-
|
|
2391
|
+
const newHides = claudeSet.filter((n) => claudeSetMap[n] === V.HIDE).length;
|
|
2392
|
+
log(` Claude: ${personal.length} personal skills; hide ${newHides} more, name-only ${claudeSet.length - newHides} more, restore ${claudeUnset.length}; ` +
|
|
2340
2393
|
`${Object.keys(foreign).length} operator-set override(s) left alone`);
|
|
2341
|
-
log(
|
|
2342
|
-
|
|
2343
|
-
|
|
2394
|
+
log(fullDays === null
|
|
2395
|
+
? ' name-only tier: off (`--full-days N` turns it on: core skills unused for N days keep their name, lose their description)'
|
|
2396
|
+
: ` name-only tier: core skills not used within ${fullDays} days and not named in the instructions are listed by name only (${plan.nameOnly.length})`);
|
|
2397
|
+
log(` Claude listing, launcher's estimate ≈ ${charsBefore} → ${charsAfter} characters`);
|
|
2398
|
+
log(' (personal and registered plugin skills in Claude\'s `- name: description` form; bundled skills,');
|
|
2399
|
+
log(' plugin commands and plugins loaded from other roots are not counted).');
|
|
2400
|
+
const measured = lastListingMeasure(claudeRoot);
|
|
2401
|
+
log(measured
|
|
2402
|
+
? ` Claude Code's own count (debug log ${measured.at}): ${measured.skills} entries, ${measured.chars} characters > ${measured.budget} budget.`
|
|
2403
|
+
: ' Claude Code\'s own count: none logged yet. `claude -p x --debug`, then look for "Skill listing over budget".');
|
|
2404
|
+
log(' Over budget, Claude Code 2.1.299 keeps bundled skills whole, gives the others their description');
|
|
2405
|
+
log(' most-used first and lists the rest by name only.');
|
|
2344
2406
|
}
|
|
2345
2407
|
if (inScope('codex')) {
|
|
2346
2408
|
log(` Codex: ${shared.length} shared skills; disable up to ${codexAdd.length} more (entries already present are left as they are), re-enable ${codexDrop.length}` +
|
|
@@ -2404,7 +2466,7 @@ function runVisibilityUnlocked(opts) {
|
|
|
2404
2466
|
const degraded = [];
|
|
2405
2467
|
let record = Object.assign({}, owned, {
|
|
2406
2468
|
note: 'Written by `npx sshlg-skills visibility`; --revert removes these entries. Pending holds a write-ahead receipt; applied is consent for update on hosts only.',
|
|
2407
|
-
days, keep, marksRevision: marksData && !markProblems.length ? marksData.revision : null,
|
|
2469
|
+
days, keep, fullDays, marksRevision: marksData && !markProblems.length ? marksData.revision : null,
|
|
2408
2470
|
updatedAt: new Date().toISOString(), created: Object.assign({}, owned.created || {}),
|
|
2409
2471
|
files: Object.assign({}, owned.files || {}, Object.fromEntries(Object.entries(files).filter(([id]) => inScope(id)))),
|
|
2410
2472
|
});
|
|
@@ -2442,13 +2504,14 @@ function runVisibilityUnlocked(opts) {
|
|
|
2442
2504
|
if (id === 'claude') {
|
|
2443
2505
|
if (claudeSet.length && !('skillOverrides' in settingsData)) next.created.claude = ['skillOverrides'];
|
|
2444
2506
|
if (doRevert) delete next.created.claude;
|
|
2507
|
+
next.claudeNameOnly = plan.nameOnly.slice().sort();
|
|
2445
2508
|
}
|
|
2446
2509
|
if (r.text === before) { journal.save(next); log(` ${id}: already as planned`); return; }
|
|
2447
2510
|
transact(id, file, before, r.text, next, () => S.write(file, r.text, home));
|
|
2448
2511
|
};
|
|
2449
2512
|
const claudeCreated = createdOf('claude').includes('skillOverrides') || !Object.prototype.hasOwnProperty.call(owned, 'applied');
|
|
2450
2513
|
writeSimple('claude', settingsPath, settingsText,
|
|
2451
|
-
(t) => V.editClaudeSettings(t, { set:
|
|
2514
|
+
(t) => V.editClaudeSettings(t, { set: claudeSetMap, unset: claudeUnsetMap, prune: claudeCreated }), () => plan.hide.slice().sort());
|
|
2452
2515
|
writeSimple('codex', codexPath, codexText,
|
|
2453
2516
|
(t) => V.editCodexConfig(t, { disable: codexAdd, enable: codexDrop }),
|
|
2454
2517
|
(r) => [...new Set(owned.codex.filter((p) => codexWant.includes(p)).concat(r.added || []))].sort());
|
package/lib/visibility-state.js
CHANGED
|
@@ -39,6 +39,8 @@ function validate(r) {
|
|
|
39
39
|
if (!r || typeof r !== 'object' || Array.isArray(r)) throw new Error('visibility record is not an object');
|
|
40
40
|
for (const h of HOSTS) if (r[h] !== undefined && (!Array.isArray(r[h]) || r[h].some((x) => typeof x !== 'string')))
|
|
41
41
|
throw new Error(`visibility record ${h} is not a list of strings`);
|
|
42
|
+
if (r.claudeNameOnly !== undefined && (!Array.isArray(r.claudeNameOnly) || r.claudeNameOnly.some((x) => typeof x !== 'string')))
|
|
43
|
+
throw new Error('visibility record claudeNameOnly is not a list of strings');
|
|
42
44
|
if (r.files !== undefined && (!r.files || typeof r.files !== 'object' || Array.isArray(r.files)
|
|
43
45
|
|| Object.entries(r.files).some(([h, f]) => !HOSTS.includes(h) || typeof f !== 'string' || !path.isAbsolute(f))))
|
|
44
46
|
throw new Error('visibility record files is not a host-to-absolute-path map');
|
package/lib/visibility.js
CHANGED
|
@@ -23,8 +23,15 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
const HIDE = 'user-invocable-only';
|
|
26
|
+
const NAME_ONLY = 'name-only';
|
|
26
27
|
const DESC_CAP = 1536;
|
|
27
28
|
const DEFAULT_DAYS = 60;
|
|
29
|
+
// B-254. Measured 2026-10-24: Claude Code 2.1.299 logged `Skill listing over budget:
|
|
30
|
+
// 132 skills, 61725 chars > 30000 budget`. Over budget it keeps bundled skills whole and
|
|
31
|
+
// gives the rest their description most-used first; the remainder are listed by name
|
|
32
|
+
// only. So a quiet core skill loses its description anyway. Within the core, only what
|
|
33
|
+
// was used within DEFAULT_FULL_DAYS or is named in the instructions keeps it.
|
|
34
|
+
const DEFAULT_FULL_DAYS = 14;
|
|
28
35
|
|
|
29
36
|
/** Skill names the instruction text references deliberately: inside backticks. */
|
|
30
37
|
function namedIn(text, names) {
|
|
@@ -53,16 +60,39 @@ function core({ personal, family, usage, now, days, instructions }) {
|
|
|
53
60
|
return set;
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The core members that keep their description: used within `fullDays`, or named in
|
|
65
|
+
* the instructions (an agent told to use a skill by name is already told when).
|
|
66
|
+
*/
|
|
67
|
+
function fullSet({ personal, usage, now, fullDays, instructions }) {
|
|
68
|
+
const window = (fullDays == null ? DEFAULT_FULL_DAYS : fullDays) * 86400e3;
|
|
69
|
+
const set = new Set();
|
|
70
|
+
for (const n of personal || []) {
|
|
71
|
+
const u = (usage || {})[n];
|
|
72
|
+
if (u && typeof u.lastUsedAt === 'number' && now - u.lastUsedAt <= window) set.add(n);
|
|
73
|
+
}
|
|
74
|
+
for (const n of namedIn(instructions, personal)) set.add(n);
|
|
75
|
+
return set;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `hide`: outside the core. `nameOnly`: in the core but outside `full`, so listed by name
|
|
80
|
+
* without its description. Without `full` nothing goes name-only. A key the operator set
|
|
81
|
+
* is never planned over, in either list.
|
|
82
|
+
*/
|
|
83
|
+
function claudePlan({ personal, core: coreSet, full, overrides, keep }) {
|
|
57
84
|
const kept = new Set(keep || []);
|
|
58
85
|
const hide = [];
|
|
86
|
+
const nameOnly = [];
|
|
59
87
|
const respected = [];
|
|
60
88
|
for (const n of personal || []) {
|
|
61
|
-
if (
|
|
89
|
+
if (kept.has(n)) continue;
|
|
90
|
+
const inCore = coreSet.has(n);
|
|
91
|
+
if (inCore && (!full || full.has(n))) continue;
|
|
62
92
|
if (overrides && Object.prototype.hasOwnProperty.call(overrides, n)) { respected.push(n); continue; }
|
|
63
|
-
hide.push(n);
|
|
93
|
+
(inCore ? nameOnly : hide).push(n);
|
|
64
94
|
}
|
|
65
|
-
return { hide: hide.sort(), respected: respected.sort() };
|
|
95
|
+
return { hide: hide.sort(), nameOnly: nameOnly.sort(), respected: respected.sort() };
|
|
66
96
|
}
|
|
67
97
|
|
|
68
98
|
/**
|
|
@@ -77,12 +107,19 @@ function editClaudeSettings(text, { set, unset, prune }) {
|
|
|
77
107
|
if (JSON.stringify(data, null, 2) + '\n' !== text) {
|
|
78
108
|
return { ok: false, reason: 'settings.json is not in the 2-space JSON format Claude Code writes; refusing to reformat it' };
|
|
79
109
|
}
|
|
110
|
+
// `set` and `unset` map a name to its value; a bare list means the hidden value, the
|
|
111
|
+
// form callers used before the name-only tier. `unset` removes a key only while it
|
|
112
|
+
// still holds the value the launcher wrote.
|
|
113
|
+
const asMap = (x) => (Array.isArray(x) ? Object.fromEntries(x.map((n) => [n, HIDE])) : (x || {}));
|
|
114
|
+
const setMap = asMap(set);
|
|
115
|
+
const unsetMap = asMap(unset);
|
|
80
116
|
const ov = Object.assign({}, data.skillOverrides || {});
|
|
81
117
|
let changed = false;
|
|
82
|
-
for (const n of
|
|
118
|
+
for (const [n, v] of Object.entries(setMap)) if (ov[n] !== v) { ov[n] = v; changed = true; }
|
|
83
119
|
const kept = [];
|
|
84
|
-
for (const n of
|
|
85
|
-
if (
|
|
120
|
+
for (const [n, v] of Object.entries(unsetMap)) {
|
|
121
|
+
if (Object.prototype.hasOwnProperty.call(setMap, n)) continue;
|
|
122
|
+
if (ov[n] === v) { delete ov[n]; changed = true; }
|
|
86
123
|
else if (n in ov) kept.push(n);
|
|
87
124
|
}
|
|
88
125
|
// Nothing to do leaves the file exactly as it was — including an empty map the
|
|
@@ -177,15 +214,21 @@ function editCodexConfig(text, { disable, enable }) {
|
|
|
177
214
|
return { ok: true, text: lines.join('\n'), added };
|
|
178
215
|
}
|
|
179
216
|
|
|
180
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* Characters a listing spends, in the form Claude Code 2.1.299 prints it: `- name: description`
|
|
219
|
+
* while `on` (description capped at 1536), `- name` when name-only, one newline between
|
|
220
|
+
* entries; hidden costs nothing.
|
|
221
|
+
*/
|
|
181
222
|
function listingChars(rows) {
|
|
182
223
|
let n = 0;
|
|
224
|
+
let entries = 0;
|
|
183
225
|
for (const r of rows || []) {
|
|
184
226
|
if (r.state === 'hidden') continue;
|
|
185
|
-
|
|
186
|
-
|
|
227
|
+
entries += 1;
|
|
228
|
+
n += 2 + String(r.name).length;
|
|
229
|
+
if (r.state === 'on') n += 2 + Math.min(DESC_CAP, String(r.description || '').length);
|
|
187
230
|
}
|
|
188
|
-
return n;
|
|
231
|
+
return entries ? n + entries - 1 : 0;
|
|
189
232
|
}
|
|
190
233
|
|
|
191
234
|
// ── Packs: the operator's marks (pack-marks.json) and per-machine overrides ──
|
|
@@ -453,7 +496,8 @@ function jsonAt(text, keys) {
|
|
|
453
496
|
}
|
|
454
497
|
|
|
455
498
|
module.exports = {
|
|
456
|
-
core, claudePlan, editClaudeSettings, editCodexConfig, listingChars, namedIn,
|
|
499
|
+
core, fullSet, claudePlan, editClaudeSettings, editCodexConfig, listingChars, namedIn,
|
|
500
|
+
HIDE, NAME_ONLY, DEFAULT_DAYS, DEFAULT_FULL_DAYS,
|
|
457
501
|
MARKS_SCHEMA, marksProblems, marksIndex, marksFromLock, packHidden, hostHide, setPackOverride,
|
|
458
502
|
editJsonList, editJsonMap, jsonAt,
|
|
459
503
|
};
|
package/package.json
CHANGED