yourskills 0.1.4 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +6 -4
  2. package/dist/main.js +710 -638
  3. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -143,9 +143,10 @@ function configValue(config, key) {
143
143
  case "server":
144
144
  return config.serverUrl ?? UNSET;
145
145
  case "track":
146
- return config.track === undefined ? UNSET : String(config.track);
147
- case "autoUpdate":
148
- return String(config.autoUpdate !== false);
146
+ case "autoUpdate": {
147
+ const value = key === "track" ? config.track : config.autoUpdate;
148
+ return String(value !== false);
149
+ }
149
150
  }
150
151
  }
151
152
  function setConfigValue(config, key, value) {
@@ -2866,8 +2867,8 @@ Options:
2866
2867
  --fix doctor: repair what it can, and say what it did
2867
2868
  --explain config get: say where each value came from
2868
2869
  --method <tool> upgrade: npm, bun or brew, when the guess is wrong
2869
- --track hooks install: opt in to counting skill invocations
2870
- --no-track hooks install: opt out again
2870
+ --track hooks install: count skill invocations (the default)
2871
+ --no-track hooks install: stop counting skill invocations
2871
2872
  --server <url> Registry base URL for this invocation
2872
2873
  --plain Plain, greppable output; no frames, no colour
2873
2874
  --json Machine-readable output
@@ -2929,8 +2930,8 @@ var OPTIONS = [
2929
2930
  ["--fix", "doctor: repair what it can"],
2930
2931
  ["--explain", "config get: where each value came from"],
2931
2932
  ["--method", "upgrade: npm, bun or brew"],
2932
- ["--track", "hooks install: opt in to usage counting"],
2933
- ["--no-track", "hooks install: opt out"],
2933
+ ["--track", "hooks install: count skill invocations (the default)"],
2934
+ ["--no-track", "hooks install: stop counting skill invocations"],
2934
2935
  ["--server", "registry base URL"],
2935
2936
  ["--plain", "plain output"],
2936
2937
  ["--json", "machine-readable output"],
@@ -3098,6 +3099,9 @@ function agentForEvent(kind, event, adapters = HOOK_ADAPTERS) {
3098
3099
  return owner?.agent ?? "unknown";
3099
3100
  }
3100
3101
  // src/hooks/audit.ts
3102
+ import { existsSync as existsSync3 } from "node:fs";
3103
+
3104
+ // src/hooks/install.ts
3101
3105
  import { existsSync as existsSync2 } from "node:fs";
3102
3106
 
3103
3107
  // src/usage/queue.ts
@@ -4710,7 +4714,7 @@ function trackingDisabledByEnv() {
4710
4714
  async function trackingEnabled() {
4711
4715
  if (trackingDisabledByEnv())
4712
4716
  return false;
4713
- return (await readConfig()).track === true;
4717
+ return (await readConfig()).track !== false;
4714
4718
  }
4715
4719
  async function flushQuietly(server) {
4716
4720
  try {
@@ -4864,474 +4868,116 @@ async function applyTo(path, change) {
4864
4868
  return { changed: true };
4865
4869
  }
4866
4870
 
4867
- // src/hooks/audit.ts
4868
- function wanted(entries, optedIn) {
4869
- return entries.filter((entry) => entry.kind === "update-check" || optedIn);
4870
- }
4871
- async function hookGaps() {
4872
- const config = await readConfig();
4873
- const optedIn = !trackingDisabledByEnv() && config.track === true;
4874
- const gaps = [];
4875
- for (const adapter of HOOK_ADAPTERS) {
4876
- const path = adapter.settingsPath();
4877
- if (!path || !existsSync2(adapter.home()))
4878
- continue;
4879
- const expected = wanted(adapter.entries, optedIn);
4880
- if (expected.length === 0)
4881
- continue;
4882
- const file = await readSettingsFile(path);
4883
- if (!file.ok) {
4884
- gaps.push({
4885
- agent: adapter.agent,
4886
- path,
4887
- registered: [],
4888
- missing: [],
4889
- error: file.error
4890
- });
4891
- continue;
4892
- }
4893
- const registered = registeredEntries(file.settings, expected).map((entry) => entry.kind);
4894
- gaps.push({
4895
- agent: adapter.agent,
4896
- path,
4897
- registered,
4898
- missing: expected.map((entry) => entry.kind).filter((kind) => !registered.includes(kind))
4899
- });
4871
+ // src/hooks/install.ts
4872
+ var PAYLOAD_DISCLOSURE = [
4873
+ { mark: "none", text: "Usage tracking sends, once per skill invocation:" },
4874
+ { mark: "none", text: " skillExternalId", note: "the skill's registry id" },
4875
+ { mark: "none", text: " ref", note: "the commit you installed it at" },
4876
+ { mark: "none", text: " agent", note: "which agent ran it" },
4877
+ { mark: "none", text: " occurredAt", note: "when it ran" },
4878
+ {
4879
+ mark: "none",
4880
+ text: " (nothing else)",
4881
+ note: "the server adds a keyed hash of you; the CLI never sends a user id"
4900
4882
  }
4901
- return gaps;
4902
- }
4903
- async function registerMissing(gap) {
4904
- const adapter = HOOK_ADAPTERS.find((a) => a.agent === gap.agent);
4905
- const entries = adapter?.entries.filter((entry) => gap.missing.includes(entry.kind));
4906
- if (!adapter || !entries || entries.length === 0)
4907
- throw new Error(`nothing to register for ${gap.agent}`);
4908
- const applied = await applyTo(gap.path, (settings) => addHooks(settings, entries));
4909
- if (applied.error)
4910
- throw new Error(applied.error);
4911
- return `registered ${gap.missing.join(" + ")} in ${gap.path}`;
4912
- }
4913
- // ../../packages/bundles/src/drift.ts
4914
- var SHORT_SHA = 7;
4915
- function namesSameCommit(prefix, full) {
4916
- const short = prefix.trim().toLowerCase();
4917
- const long = full.trim().toLowerCase();
4918
- if (short === long)
4919
- return true;
4920
- return short.length >= SHORT_SHA && long.startsWith(short);
4921
- }
4922
-
4923
- // src/installed/installed.ts
4924
- var shortRef = (ref) => ref.slice(0, SHORT_SHA);
4925
- function refColumn(row) {
4926
- return row.unpinned ? "unpinned" : shortRef(row.ref);
4927
- }
4928
- function installedRows(nearest) {
4929
- return Object.values(nearest.manifest.skills).map((entry) => ({
4930
- name: entry.name,
4931
- ref: entry.ref,
4932
- bundle: entry.bundle ?? null,
4933
- scope: nearest.manifest.scope,
4934
- unpinned: entry.unpinned === true
4935
- })).sort((a, b) => a.name.localeCompare(b.name));
4883
+ ];
4884
+ function trackNote(track) {
4885
+ if (trackingDisabledByEnv())
4886
+ return "off (YOURSKILLS_NO_TRACK is set)";
4887
+ return track ? "on; turn off with `yourskills config set track false`" : "off; turn on with `yourskills config set track true`";
4936
4888
  }
4937
- function columns(rows) {
4938
- const names = Math.max(...rows.map((r) => r.name.length));
4939
- const bundles = Math.max(...rows.map((r) => (r.bundle ?? "-").length));
4940
- return rows.map((row) => ({
4941
- mark: "none",
4942
- text: `${row.name.padEnd(names)} ${refColumn(row)}`,
4943
- note: `${(row.bundle ?? "-").padEnd(bundles)} ${row.scope}`
4944
- }));
4889
+ function trackingOn(config) {
4890
+ return !trackingDisabledByEnv() && config.track !== false;
4945
4891
  }
4946
- function installedReport(nearest) {
4947
- const rows = installedRows(nearest);
4948
- const lines = rows.length === 0 ? [
4949
- {
4950
- mark: "none",
4951
- text: "Nothing installed. Run `yourskills add <name>`.",
4952
- note: nearest.path
4953
- }
4954
- ] : columns(rows);
4892
+ function selectAdapters(agent) {
4893
+ if (agent) {
4894
+ const found = adapterFor(agent);
4895
+ if (!found)
4896
+ return { adapters: [], unknown: [agent], absent: [] };
4897
+ return existsSync2(found.home()) ? { adapters: [found], unknown: [], absent: [] } : { adapters: [], unknown: [], absent: [found] };
4898
+ }
4955
4899
  return {
4956
- title: "installed",
4957
- lines,
4958
- json: {
4959
- manifest: nearest.path,
4960
- scope: nearest.manifest.scope,
4961
- skills: rows
4962
- },
4963
- ...rows.length === 0 ? {
4964
- next: {
4965
- command: "yourskills browse",
4966
- why: "pick something to install"
4967
- }
4968
- } : {}
4900
+ adapters: HOOK_ADAPTERS.filter((a) => existsSync2(a.home())),
4901
+ unknown: [],
4902
+ absent: []
4969
4903
  };
4970
4904
  }
4971
- // src/installed/outdated.ts
4972
- function driftTag(row) {
4973
- return lifecycleTag(row.deprecated ? "deprecated" : undefined);
4974
- }
4975
- function hasMoved(installed, available) {
4976
- return !(namesSameCommit(available, installed) || namesSameCommit(installed, available));
4905
+ async function pinnedAdapters() {
4906
+ const pinned = await resolveAgents();
4907
+ if (pinned.length === 0)
4908
+ return null;
4909
+ const adapters = pinned.map((name) => adapterFor(name)).filter((a) => a !== undefined).filter((a) => existsSync2(a.home()));
4910
+ return adapters.length > 0 ? adapters : null;
4977
4911
  }
4978
- function drift(manifest, pins, catalog) {
4979
- const byExternalId = new Map(catalog.map((row) => [row.externalId, row]));
4980
- const byName = new Map(catalog.map((row) => [row.name, row]));
4981
- const moved = [];
4982
- const unknown = [];
4983
- const unpinned = [];
4984
- for (const entry of Object.values(manifest.skills)) {
4985
- if (entry.unpinned) {
4986
- unpinned.push(entry.name);
4987
- continue;
4912
+ async function installInto(adapter, tracking) {
4913
+ const path = adapter.settingsPath();
4914
+ const result = {
4915
+ agent: adapter.agent,
4916
+ path,
4917
+ installed: [],
4918
+ already: [],
4919
+ skipped: []
4920
+ };
4921
+ if (!path) {
4922
+ for (const kind of adapter.supports) {
4923
+ result.skipped.push({
4924
+ kind,
4925
+ why: adapter.limitation ?? "not supported yet"
4926
+ });
4988
4927
  }
4989
- const row = byExternalId.get(entry.externalId) ?? byName.get(entry.name);
4990
- if (!row) {
4991
- unknown.push(entry.name);
4928
+ return result;
4929
+ }
4930
+ const wanted = [];
4931
+ for (const entry of adapter.entries) {
4932
+ if (entry.kind === "usage" && !tracking) {
4933
+ result.skipped.push({ kind: entry.kind, why: trackNote(false) });
4992
4934
  continue;
4993
4935
  }
4994
- const pin = entry.bundle ? pins.find((p) => p.bundle === entry.bundle && p.skillId === row.id) : undefined;
4995
- const available = pin?.pinnedSha ?? row.currentSha;
4996
- if (!hasMoved(entry.ref, available))
4997
- continue;
4998
- moved.push({
4999
- name: entry.name,
5000
- bundle: entry.bundle ?? null,
5001
- installed: entry.ref,
5002
- available,
5003
- deprecated: pin?.deprecated ?? row.status === "deprecated"
5004
- });
4936
+ wanted.push(entry);
5005
4937
  }
5006
- moved.sort((a, b) => a.name.localeCompare(b.name));
5007
- return { moved, unknown, unpinned };
5008
- }
5009
- async function driftNow(client, nearest) {
5010
- const [pins, catalog] = await Promise.all([
5011
- client.bundles.pins.query(),
5012
- client.skills.list.query({})
5013
- ]);
5014
- return drift(nearest.manifest, pins, catalog);
5015
- }
5016
- async function outdatedReport(client, nearest, quiet = false) {
5017
- let report2;
5018
- try {
5019
- report2 = await driftNow(client, nearest);
5020
- } catch (error) {
5021
- const message = error instanceof Error ? error.message : String(error);
5022
- return {
5023
- title: "outdated",
5024
- lines: [
5025
- { mark: "fail", text: "Could not reach the registry.", note: message }
5026
- ],
5027
- json: { manifest: nearest.path, error: message },
5028
- silent: quiet
5029
- };
4938
+ let present = new Set;
4939
+ const applied = await applyTo(path, (settings) => {
4940
+ present = new Set(registeredEntries(settings, wanted).map((entry) => entry.kind));
4941
+ return addHooks(settings, wanted);
4942
+ });
4943
+ if (applied.error) {
4944
+ result.error = applied.error;
4945
+ return result;
5030
4946
  }
5031
- const lines = report2.moved.map((row) => ({
5032
- mark: "pending",
5033
- text: `${row.name} ${shortRef(row.installed)} -> ${shortRef(row.available)}`,
5034
- note: [row.bundle ?? "-", driftTag(row)].filter(Boolean).join(" ")
5035
- }));
5036
- for (const name of report2.unknown) {
4947
+ for (const entry of wanted) {
4948
+ if (present.has(entry.kind))
4949
+ result.already.push(entry.kind);
4950
+ else
4951
+ result.installed.push(entry.kind);
4952
+ }
4953
+ if (applied.changed && result.already.length > 0) {
4954
+ result.repaired = result.already;
4955
+ result.already = [];
4956
+ }
4957
+ return result;
4958
+ }
4959
+ function linesFor(result) {
4960
+ const lines = [];
4961
+ if (result.error) {
5037
4962
  lines.push({
5038
4963
  mark: "fail",
5039
- text: name,
5040
- note: "no longer in your registry"
4964
+ text: `${result.agent}: left untouched`,
4965
+ note: result.error
5041
4966
  });
4967
+ return lines;
5042
4968
  }
5043
- for (const name of report2.unpinned) {
4969
+ for (const kind of result.installed) {
5044
4970
  lines.push({
5045
- mark: "pending",
5046
- text: name,
5047
- note: "not in your registry; imported from disk, nothing to compare"
4971
+ mark: "ok",
4972
+ text: `${result.agent} ${kind}`,
4973
+ note: result.path ?? ""
5048
4974
  });
5049
4975
  }
5050
- const current = report2.moved.length === 0 && report2.unknown.length === 0;
5051
- if (current && !quiet) {
4976
+ for (const kind of result.repaired ?? []) {
5052
4977
  lines.push({
5053
4978
  mark: "ok",
5054
- text: "Everything is at the ref your registry pins."
5055
- });
5056
- }
5057
- return {
5058
- title: "outdated",
5059
- lines,
5060
- json: {
5061
- manifest: nearest.path,
5062
- outdated: report2.moved,
5063
- unknown: report2.unknown,
5064
- unpinned: report2.unpinned
5065
- },
5066
- silent: quiet && current,
5067
- ...report2.moved.length > 0 ? {
5068
- next: {
5069
- command: "yourskills update --all",
5070
- why: "install what moved"
5071
- }
5072
- } : {}
5073
- };
5074
- }
5075
- // src/hooks/emit.ts
5076
- function describe(outcome) {
5077
- switch (outcome.kind) {
5078
- case "quiet":
5079
- return [];
5080
- case "signed-out":
5081
- return [
5082
- "yourskills: your sign-in has expired, so your skills are no longer kept up to date.",
5083
- " run: yourskills login"
5084
- ];
5085
- case "behind": {
5086
- const lines = [
5087
- `yourskills: ${count(outcome.moved.length, "skill")} ${outcome.moved.length === 1 ? "has" : "have"} a newer version in your registry (autoUpdate is off)`
5088
- ];
5089
- for (const row of outcome.moved) {
5090
- lines.push(` ${row.name} ${shortRef(row.installed)} -> ${shortRef(row.available)}${column(driftTag(row))}`);
5091
- }
5092
- lines.push(" run: yourskills update --all");
5093
- return lines;
5094
- }
5095
- case "updated": {
5096
- const { applied, failed } = outcome;
5097
- const lines = [
5098
- applied.length > 0 ? `yourskills: updated ${count(applied.length, "skill")} to what your registry pins; ${applied.length === 1 ? "it is" : "they are"} live in this session.` : `yourskills: could not update ${count(failed.length, "skill")}`
5099
- ];
5100
- for (const row of applied) {
5101
- lines.push(` ${row.name} ${row.from} -> ${row.to}`);
5102
- }
5103
- for (const row of failed) {
5104
- lines.push(` ${row.name} failed: ${row.message}`);
5105
- }
5106
- if (applied.length > 0)
5107
- lines.push(` recorded in ${outcome.manifest}`);
5108
- if (failed.length > 0)
5109
- lines.push(" run: yourskills doctor");
5110
- return lines;
5111
- }
5112
- }
5113
- }
5114
- function describeForModel(outcome) {
5115
- switch (outcome.kind) {
5116
- case "quiet":
5117
- return [];
5118
- case "signed-out":
5119
- return [
5120
- "yourskills: the user's session with the skills registry has expired, so installed skills are no longer being updated. The user has been told to run `yourskills login`; it is their call."
5121
- ];
5122
- case "behind": {
5123
- const lines = [
5124
- `yourskills: ${count(outcome.moved.length, "installed skill")} ${outcome.moved.length === 1 ? "is" : "are"} behind the registry. Automatic updates are off; the user has been told and it is their call whether to run \`yourskills update --all\`.`
5125
- ];
5126
- for (const row of outcome.moved) {
5127
- lines.push(` ${row.name} installed ${shortRef(row.installed)}, registry pins ${shortRef(row.available)}${column(driftTag(row))}`);
5128
- }
5129
- return lines;
5130
- }
5131
- case "updated": {
5132
- const { applied, failed } = outcome;
5133
- const lines = [];
5134
- if (applied.length > 0) {
5135
- lines.push(`yourskills updated ${count(applied.length, "installed skill")} on disk at the start of this session. The contents changed: re-read a skill before relying on what you remember of it.`);
5136
- for (const row of applied) {
5137
- lines.push(` ${row.name} ${row.from} -> ${row.to}`);
5138
- }
5139
- lines.push(` recorded in ${outcome.manifest}`);
5140
- }
5141
- if (failed.length > 0) {
5142
- lines.push(`yourskills could not update ${count(failed.length, "installed skill")}; the version on disk is unchanged. The user has been told.`);
5143
- for (const row of failed) {
5144
- lines.push(` ${row.name} ${row.message}`);
5145
- }
5146
- }
5147
- return lines;
5148
- }
5149
- }
5150
- }
5151
- function count(n, noun) {
5152
- return `${n} ${noun}${n === 1 ? "" : "s"}`;
5153
- }
5154
- function column(text) {
5155
- return text ? ` ${text}` : "";
5156
- }
5157
- function emit(outcome) {
5158
- const lines = describe(outcome);
5159
- if (lines.length === 0)
5160
- return;
5161
- const installed = outcome.kind === "updated" && outcome.applied.length > 0;
5162
- console.log(JSON.stringify({
5163
- systemMessage: lines.join(`
5164
- `),
5165
- hookSpecificOutput: {
5166
- hookEventName: "SessionStart",
5167
- additionalContext: describeForModel(outcome).join(`
5168
- `),
5169
- ...installed ? { reloadSkills: true } : {}
5170
- }
5171
- }));
5172
- }
5173
- // src/hooks/ensure.ts
5174
- import { existsSync as existsSync3 } from "node:fs";
5175
- async function registerInto(adapter) {
5176
- const wanted2 = adapter.entries.filter((entry) => entry.kind === "update-check");
5177
- if (wanted2.length === 0)
5178
- return null;
5179
- const path = adapter.settingsPath();
5180
- if (!path)
5181
- return null;
5182
- if (!existsSync3(adapter.home()))
5183
- return null;
5184
- const applied = await applyTo(path, (settings) => addHooks(settings, wanted2));
5185
- if (!applied.changed)
5186
- return null;
5187
- return { agent: adapter.agent, path, entries: wanted2 };
5188
- }
5189
- function linesFor(registered) {
5190
- const agents = registered.map((r) => r.agent).join(", ");
5191
- const lines = [
5192
- `+ yourskills will check your skills once a day, when ${agents} starts, and update what moved`
5193
- ];
5194
- for (const { agent, path } of registered) {
5195
- lines.push(` ${agent} ${path}`);
5196
- }
5197
- lines.push(" undo with: yourskills hooks uninstall");
5198
- if (!trackingDisabledByEnv()) {
5199
- lines.push(" usage counting stays off; turn it on with: yourskills hooks install --track");
5200
- }
5201
- return lines;
5202
- }
5203
- async function ensureHooksRegistered() {
5204
- try {
5205
- const config = await readConfig();
5206
- if (config.hooksOffered)
5207
- return [];
5208
- if (!config.token)
5209
- return [];
5210
- const registered = [];
5211
- for (const adapter of HOOK_ADAPTERS) {
5212
- try {
5213
- const result = await registerInto(adapter);
5214
- if (result)
5215
- registered.push(result);
5216
- } catch {}
5217
- }
5218
- await updateConfig((stored) => ({
5219
- ...stored,
5220
- hooksOffered: new Date().toISOString()
5221
- }));
5222
- return registered.length > 0 ? linesFor(registered) : [];
5223
- } catch {
5224
- return [];
5225
- }
5226
- }
5227
- // src/hooks/install.ts
5228
- import { existsSync as existsSync4 } from "node:fs";
5229
- var PAYLOAD_DISCLOSURE = [
5230
- { mark: "none", text: "Usage tracking sends, once per skill invocation:" },
5231
- { mark: "none", text: " skillExternalId", note: "the skill's registry id" },
5232
- { mark: "none", text: " ref", note: "the commit you installed it at" },
5233
- { mark: "none", text: " agent", note: "which agent ran it" },
5234
- { mark: "none", text: " occurredAt", note: "when it ran" },
5235
- {
5236
- mark: "none",
5237
- text: " (nothing else)",
5238
- note: "the server adds a keyed hash of you; the CLI never sends a user id"
5239
- }
5240
- ];
5241
- function trackNote(track) {
5242
- if (trackingDisabledByEnv())
5243
- return "off (YOURSKILLS_NO_TRACK is set)";
5244
- return track ? "on" : "off — enable with `yourskills hooks install --track`";
5245
- }
5246
- function selectAdapters(agent) {
5247
- if (agent) {
5248
- const found = adapterFor(agent);
5249
- if (!found)
5250
- return { adapters: [], unknown: [agent], absent: [] };
5251
- return existsSync4(found.home()) ? { adapters: [found], unknown: [], absent: [] } : { adapters: [], unknown: [], absent: [found] };
5252
- }
5253
- return {
5254
- adapters: HOOK_ADAPTERS.filter((a) => existsSync4(a.home())),
5255
- unknown: [],
5256
- absent: []
5257
- };
5258
- }
5259
- async function pinnedAdapters() {
5260
- const pinned = await resolveAgents();
5261
- if (pinned.length === 0)
5262
- return null;
5263
- const adapters = pinned.map((name) => adapterFor(name)).filter((a) => a !== undefined).filter((a) => existsSync4(a.home()));
5264
- return adapters.length > 0 ? adapters : null;
5265
- }
5266
- async function installInto(adapter, optedIn) {
5267
- const path = adapter.settingsPath();
5268
- const result = {
5269
- agent: adapter.agent,
5270
- path,
5271
- installed: [],
5272
- already: [],
5273
- skipped: []
5274
- };
5275
- if (!path) {
5276
- for (const kind of adapter.supports) {
5277
- result.skipped.push({
5278
- kind,
5279
- why: adapter.limitation ?? "not supported yet"
5280
- });
5281
- }
5282
- return result;
5283
- }
5284
- const wanted2 = [];
5285
- for (const entry of adapter.entries) {
5286
- if (entry.kind === "usage" && !optedIn) {
5287
- result.skipped.push({ kind: entry.kind, why: trackNote(false) });
5288
- continue;
5289
- }
5290
- wanted2.push(entry);
5291
- }
5292
- let present = new Set;
5293
- const applied = await applyTo(path, (settings) => {
5294
- present = new Set(registeredEntries(settings, wanted2).map((entry) => entry.kind));
5295
- return addHooks(settings, wanted2);
5296
- });
5297
- if (applied.error) {
5298
- result.error = applied.error;
5299
- return result;
5300
- }
5301
- for (const entry of wanted2) {
5302
- if (present.has(entry.kind))
5303
- result.already.push(entry.kind);
5304
- else
5305
- result.installed.push(entry.kind);
5306
- }
5307
- if (applied.changed && result.already.length > 0) {
5308
- result.repaired = result.already;
5309
- result.already = [];
5310
- }
5311
- return result;
5312
- }
5313
- function linesFor2(result) {
5314
- const lines = [];
5315
- if (result.error) {
5316
- lines.push({
5317
- mark: "fail",
5318
- text: `${result.agent}: left untouched`,
5319
- note: result.error
5320
- });
5321
- return lines;
5322
- }
5323
- for (const kind of result.installed) {
5324
- lines.push({
5325
- mark: "ok",
5326
- text: `${result.agent} ${kind}`,
5327
- note: result.path ?? ""
5328
- });
5329
- }
5330
- for (const kind of result.repaired ?? []) {
5331
- lines.push({
5332
- mark: "ok",
5333
- text: `${result.agent} ${kind}`,
5334
- note: `updated ${result.path ?? ""}`.trim()
4979
+ text: `${result.agent} ${kind}`,
4980
+ note: `updated ${result.path ?? ""}`.trim()
5335
4981
  });
5336
4982
  }
5337
4983
  for (const kind of result.already) {
@@ -5356,11 +5002,11 @@ async function hooksInstall(opts = {}) {
5356
5002
  await writeConfig({ ...config2, track: opts.track });
5357
5003
  }
5358
5004
  const config = await readConfig();
5359
- const optedIn = !trackingDisabledByEnv() && config.track === true;
5005
+ const tracking = trackingOn(config);
5360
5006
  const { adapters: selected, unknown, absent } = selectAdapters(opts.agent);
5361
5007
  const adapters = opts.agent === undefined ? await pinnedAdapters() ?? selected : selected;
5362
5008
  const lines = [...PAYLOAD_DISCLOSURE];
5363
- lines.push({ mark: "none", text: "tracking", note: trackNote(optedIn) });
5009
+ lines.push({ mark: "none", text: "tracking", note: trackNote(tracking) });
5364
5010
  for (const agent of unknown) {
5365
5011
  lines.push({
5366
5012
  mark: "fail",
@@ -5385,9 +5031,9 @@ async function hooksInstall(opts = {}) {
5385
5031
  }
5386
5032
  const results = [];
5387
5033
  for (const adapter of adapters) {
5388
- const result = await installInto(adapter, optedIn);
5034
+ const result = await installInto(adapter, tracking);
5389
5035
  results.push(result);
5390
- lines.push(...linesFor2(result));
5036
+ lines.push(...linesFor(result));
5391
5037
  }
5392
5038
  if (adapters.length === 0 && unknown.length === 0 && absent.length === 0) {
5393
5039
  lines.push({
@@ -5400,7 +5046,7 @@ async function hooksInstall(opts = {}) {
5400
5046
  title: "hooks install",
5401
5047
  lines,
5402
5048
  json: {
5403
- track: optedIn,
5049
+ track: tracking,
5404
5050
  agents: results,
5405
5051
  unknown,
5406
5052
  absent: absent.map((a) => a.agent)
@@ -5451,9 +5097,9 @@ async function hooksUninstall(opts = {}) {
5451
5097
  }
5452
5098
  async function hooksStatus() {
5453
5099
  const config = await readConfig();
5454
- const optedIn = !trackingDisabledByEnv() && config.track === true;
5100
+ const tracking = trackingOn(config);
5455
5101
  const lines = [
5456
- { mark: "none", text: "tracking", note: trackNote(optedIn) }
5102
+ { mark: "none", text: "tracking", note: trackNote(tracking) }
5457
5103
  ];
5458
5104
  const agents = [];
5459
5105
  for (const agent of agentsToReport(config.agents ?? [])) {
@@ -5508,194 +5154,392 @@ async function hooksStatus() {
5508
5154
  return {
5509
5155
  title: "hooks status",
5510
5156
  lines,
5511
- json: { track: optedIn, agents }
5157
+ json: { track: tracking, agents }
5512
5158
  };
5513
5159
  }
5514
- // src/hooks/session.ts
5515
- var CHECK_BUDGET_MS = 5000;
5516
- var UPDATE_BUDGET_MS = 90000;
5517
- var DAY_MS = 86400000;
5518
- var REAL = {
5519
- now: () => Date.now(),
5520
- readConfig,
5521
- updateConfig,
5522
- connect: () => createClient(),
5523
- nearestManifest: () => nearestManifest(),
5524
- driftNow,
5525
- add,
5526
- checkBudgetMs: CHECK_BUDGET_MS,
5527
- resolveAgents: () => resolveAgents(),
5528
- landed: listInstalled
5529
- };
5530
- function updateCheckDue(last, now = Date.now()) {
5531
- if (!last)
5160
+
5161
+ // src/hooks/audit.ts
5162
+ function wanted(entries, tracking) {
5163
+ return entries.filter((entry) => entry.kind === "update-check" || tracking);
5164
+ }
5165
+ async function hookGaps() {
5166
+ const config = await readConfig();
5167
+ const optedIn = trackingOn(config);
5168
+ const gaps = [];
5169
+ for (const adapter of HOOK_ADAPTERS) {
5170
+ const path = adapter.settingsPath();
5171
+ if (!path || !existsSync3(adapter.home()))
5172
+ continue;
5173
+ const expected = wanted(adapter.entries, optedIn);
5174
+ if (expected.length === 0)
5175
+ continue;
5176
+ const file = await readSettingsFile(path);
5177
+ if (!file.ok) {
5178
+ gaps.push({
5179
+ agent: adapter.agent,
5180
+ path,
5181
+ registered: [],
5182
+ missing: [],
5183
+ error: file.error
5184
+ });
5185
+ continue;
5186
+ }
5187
+ const registered = registeredEntries(file.settings, expected).map((entry) => entry.kind);
5188
+ gaps.push({
5189
+ agent: adapter.agent,
5190
+ path,
5191
+ registered,
5192
+ missing: expected.map((entry) => entry.kind).filter((kind) => !registered.includes(kind))
5193
+ });
5194
+ }
5195
+ return gaps;
5196
+ }
5197
+ async function registerMissing(gap) {
5198
+ const adapter = HOOK_ADAPTERS.find((a) => a.agent === gap.agent);
5199
+ const entries = adapter?.entries.filter((entry) => gap.missing.includes(entry.kind));
5200
+ if (!adapter || !entries || entries.length === 0)
5201
+ throw new Error(`nothing to register for ${gap.agent}`);
5202
+ const applied = await applyTo(gap.path, (settings) => addHooks(settings, entries));
5203
+ if (applied.error)
5204
+ throw new Error(applied.error);
5205
+ return `registered ${gap.missing.join(" + ")} in ${gap.path}`;
5206
+ }
5207
+ // ../../packages/bundles/src/drift.ts
5208
+ var SHORT_SHA = 7;
5209
+ function namesSameCommit(prefix, full) {
5210
+ const short = prefix.trim().toLowerCase();
5211
+ const long = full.trim().toLowerCase();
5212
+ if (short === long)
5532
5213
  return true;
5533
- const at = Date.parse(last);
5534
- return Number.isNaN(at) || now - at >= DAY_MS;
5214
+ return short.length >= SHORT_SHA && long.startsWith(short);
5535
5215
  }
5536
- function withBudget(work, ms) {
5537
- return Promise.race([
5538
- work,
5539
- new Promise((resolve3) => {
5540
- const timer = setTimeout(() => resolve3(undefined), ms);
5541
- timer.unref?.();
5542
- })
5543
- ]);
5216
+
5217
+ // src/installed/installed.ts
5218
+ var shortRef = (ref) => ref.slice(0, SHORT_SHA);
5219
+ function refColumn(row) {
5220
+ return row.unpinned ? "unpinned" : shortRef(row.ref);
5544
5221
  }
5545
- async function check(ports, client, nearest) {
5546
- try {
5547
- const report2 = await ports.driftNow(client, nearest);
5548
- return { ok: true, moved: report2.moved };
5549
- } catch (error) {
5550
- return { ok: false, signedOut: isUnauthorized(error) };
5551
- }
5222
+ function installedRows(nearest) {
5223
+ return Object.values(nearest.manifest.skills).map((entry) => ({
5224
+ name: entry.name,
5225
+ ref: entry.ref,
5226
+ bundle: entry.bundle ?? null,
5227
+ scope: nearest.manifest.scope,
5228
+ unpinned: entry.unpinned === true
5229
+ })).sort((a, b) => a.name.localeCompare(b.name));
5552
5230
  }
5553
- async function applyUpdates(ports, client, nearest, moved, deadline) {
5554
- const where2 = {
5555
- ...nearest,
5556
- agents: await ports.resolveAgents(),
5557
- landed: ports.landed
5558
- };
5559
- const wanted2 = new Map(moved.map((row) => [row.name, row]));
5560
- const applied = [];
5561
- const failed = [];
5562
- const names = [
5563
- ...new Set(moved.map((row) => row.bundle).filter((name) => !!name))
5564
- ].sort().map((bundle) => `bundle:${bundle}`).concat(moved.filter((row) => !row.bundle).map((row) => row.name).sort());
5565
- try {
5566
- for await (const event of ports.add(names, where2, async () => client).stream()) {
5567
- if (event.kind === "installed") {
5568
- const row = wanted2.get(event.location.skillName);
5569
- if (row) {
5570
- applied.push({
5571
- name: row.name,
5572
- from: shortRef(row.installed),
5573
- to: shortRef(row.available)
5574
- });
5575
- }
5576
- } else if (event.kind === "failed" && wanted2.has(event.name)) {
5577
- failed.push({ name: event.name, message: event.message });
5231
+ function columns(rows) {
5232
+ const names = Math.max(...rows.map((r) => r.name.length));
5233
+ const bundles = Math.max(...rows.map((r) => (r.bundle ?? "-").length));
5234
+ return rows.map((row) => ({
5235
+ mark: "none",
5236
+ text: `${row.name.padEnd(names)} ${refColumn(row)}`,
5237
+ note: `${(row.bundle ?? "-").padEnd(bundles)} ${row.scope}`
5238
+ }));
5239
+ }
5240
+ function installedReport(nearest) {
5241
+ const rows = installedRows(nearest);
5242
+ const lines = rows.length === 0 ? [
5243
+ {
5244
+ mark: "none",
5245
+ text: "Nothing installed. Run `yourskills add <name>`.",
5246
+ note: nearest.path
5247
+ }
5248
+ ] : columns(rows);
5249
+ return {
5250
+ title: "installed",
5251
+ lines,
5252
+ json: {
5253
+ manifest: nearest.path,
5254
+ scope: nearest.manifest.scope,
5255
+ skills: rows
5256
+ },
5257
+ ...rows.length === 0 ? {
5258
+ next: {
5259
+ command: "yourskills browse",
5260
+ why: "pick something to install"
5578
5261
  }
5579
- if (ports.now() > deadline)
5580
- break;
5262
+ } : {}
5263
+ };
5264
+ }
5265
+ // src/installed/outdated.ts
5266
+ function driftTag(row) {
5267
+ return lifecycleTag(row.deprecated ? "deprecated" : undefined);
5268
+ }
5269
+ function hasMoved(installed, available) {
5270
+ return !(namesSameCommit(available, installed) || namesSameCommit(installed, available));
5271
+ }
5272
+ function drift(manifest, pins, catalog) {
5273
+ const byExternalId = new Map(catalog.map((row) => [row.externalId, row]));
5274
+ const byName = new Map(catalog.map((row) => [row.name, row]));
5275
+ const moved = [];
5276
+ const unknown = [];
5277
+ const unpinned = [];
5278
+ for (const entry of Object.values(manifest.skills)) {
5279
+ if (entry.unpinned) {
5280
+ unpinned.push(entry.name);
5281
+ continue;
5581
5282
  }
5582
- } catch (error) {
5583
- failed.push({
5584
- name: names.join(", "),
5585
- message: error instanceof Error ? error.message : String(error)
5283
+ const row = byExternalId.get(entry.externalId) ?? byName.get(entry.name);
5284
+ if (!row) {
5285
+ unknown.push(entry.name);
5286
+ continue;
5287
+ }
5288
+ const pin = entry.bundle ? pins.find((p) => p.bundle === entry.bundle && p.skillId === row.id) : undefined;
5289
+ const available = pin?.pinnedSha ?? row.currentSha;
5290
+ if (!hasMoved(entry.ref, available))
5291
+ continue;
5292
+ moved.push({
5293
+ name: entry.name,
5294
+ bundle: entry.bundle ?? null,
5295
+ installed: entry.ref,
5296
+ available,
5297
+ deprecated: pin?.deprecated ?? row.status === "deprecated"
5586
5298
  });
5587
5299
  }
5588
- if (applied.length === 0 && failed.length === 0)
5589
- return { kind: "quiet" };
5590
- return { kind: "updated", applied, failed, manifest: nearest.path };
5300
+ moved.sort((a, b) => a.name.localeCompare(b.name));
5301
+ return { moved, unknown, unpinned };
5591
5302
  }
5592
- async function sessionStart(ports = {}) {
5593
- const use = { ...REAL, ...ports };
5594
- const config = await use.readConfig();
5595
- if (!updateCheckDue(config.lastUpdateCheck, use.now())) {
5596
- return { kind: "quiet" };
5303
+ async function driftNow(client, nearest) {
5304
+ const [pins, catalog] = await Promise.all([
5305
+ client.bundles.pins.query(),
5306
+ client.skills.list.query({})
5307
+ ]);
5308
+ return drift(nearest.manifest, pins, catalog);
5309
+ }
5310
+ async function outdatedReport(client, nearest, quiet = false) {
5311
+ let report2;
5312
+ try {
5313
+ report2 = await driftNow(client, nearest);
5314
+ } catch (error) {
5315
+ const message = error instanceof Error ? error.message : String(error);
5316
+ return {
5317
+ title: "outdated",
5318
+ lines: [
5319
+ { mark: "fail", text: "Could not reach the registry.", note: message }
5320
+ ],
5321
+ json: { manifest: nearest.path, error: message },
5322
+ silent: quiet
5323
+ };
5597
5324
  }
5598
- const written = await use.updateConfig((current) => ({
5599
- ...current,
5600
- lastUpdateCheck: new Date(use.now()).toISOString()
5325
+ const lines = report2.moved.map((row) => ({
5326
+ mark: "pending",
5327
+ text: `${row.name} ${shortRef(row.installed)} -> ${shortRef(row.available)}`,
5328
+ note: [row.bundle ?? "-", driftTag(row)].filter(Boolean).join(" ")
5601
5329
  }));
5602
- if (written === null)
5603
- return { kind: "quiet" };
5604
- if (!config.token)
5605
- return { kind: "quiet" };
5606
- let client;
5607
- let nearest;
5608
- try {
5609
- client = await use.connect();
5610
- nearest = await use.nearestManifest();
5611
- } catch {
5612
- return { kind: "quiet" };
5330
+ for (const name of report2.unknown) {
5331
+ lines.push({
5332
+ mark: "fail",
5333
+ text: name,
5334
+ note: "no longer in your registry"
5335
+ });
5613
5336
  }
5614
- if (Object.keys(nearest.manifest.skills).length === 0)
5615
- return { kind: "quiet" };
5616
- const found = await withBudget(check(use, client, nearest), use.checkBudgetMs);
5617
- if (!found)
5618
- return { kind: "quiet" };
5619
- if (!found.ok)
5620
- return found.signedOut ? { kind: "signed-out" } : { kind: "quiet" };
5621
- if (found.moved.length === 0)
5622
- return { kind: "quiet" };
5623
- if (config.autoUpdate === false) {
5624
- return { kind: "behind", moved: found.moved, manifest: nearest.path };
5337
+ for (const name of report2.unpinned) {
5338
+ lines.push({
5339
+ mark: "pending",
5340
+ text: name,
5341
+ note: "not in your registry; imported from disk, nothing to compare"
5342
+ });
5625
5343
  }
5626
- return applyUpdates(use, client, nearest, found.moved, use.now() + UPDATE_BUDGET_MS);
5344
+ const current = report2.moved.length === 0 && report2.unknown.length === 0;
5345
+ if (current && !quiet) {
5346
+ lines.push({
5347
+ mark: "ok",
5348
+ text: "Everything is at the ref your registry pins."
5349
+ });
5350
+ }
5351
+ return {
5352
+ title: "outdated",
5353
+ lines,
5354
+ json: {
5355
+ manifest: nearest.path,
5356
+ outdated: report2.moved,
5357
+ unknown: report2.unknown,
5358
+ unpinned: report2.unpinned
5359
+ },
5360
+ silent: quiet && current,
5361
+ ...report2.moved.length > 0 ? {
5362
+ next: {
5363
+ command: "yourskills update --all",
5364
+ why: "install what moved"
5365
+ }
5366
+ } : {}
5367
+ };
5627
5368
  }
5628
-
5629
- // src/hooks/run.ts
5630
- function isHookEventName(value) {
5631
- return value === "session-start" || value === "post-tool-use";
5369
+ // src/hooks/emit.ts
5370
+ function describe(outcome) {
5371
+ switch (outcome.kind) {
5372
+ case "quiet":
5373
+ return [];
5374
+ case "signed-out":
5375
+ return [
5376
+ "yourskills: your sign-in has expired, so your skills are no longer kept up to date.",
5377
+ " run: yourskills login"
5378
+ ];
5379
+ case "behind": {
5380
+ const lines = [
5381
+ `yourskills: ${count(outcome.moved.length, "skill")} ${outcome.moved.length === 1 ? "has" : "have"} a newer version in your registry (autoUpdate is off)`
5382
+ ];
5383
+ for (const row of outcome.moved) {
5384
+ lines.push(` ${row.name} ${shortRef(row.installed)} -> ${shortRef(row.available)}${column(driftTag(row))}`);
5385
+ }
5386
+ lines.push(" run: yourskills update --all");
5387
+ return lines;
5388
+ }
5389
+ case "updated": {
5390
+ const { applied, failed } = outcome;
5391
+ const lines = [
5392
+ applied.length > 0 ? `yourskills: updated ${count(applied.length, "skill")} to what your registry pins; ${applied.length === 1 ? "it is" : "they are"} live in this session.` : `yourskills: could not update ${count(failed.length, "skill")}`
5393
+ ];
5394
+ for (const row of applied) {
5395
+ lines.push(` ${row.name} ${row.from} -> ${row.to}`);
5396
+ }
5397
+ for (const row of failed) {
5398
+ lines.push(` ${row.name} failed: ${row.message}`);
5399
+ }
5400
+ if (applied.length > 0)
5401
+ lines.push(` recorded in ${outcome.manifest}`);
5402
+ if (failed.length > 0)
5403
+ lines.push(" run: yourskills doctor");
5404
+ return lines;
5405
+ }
5406
+ }
5632
5407
  }
5633
- var STDIN_BUDGET_MS = 2000;
5634
- async function readStdin() {
5635
- if (process.stdin.isTTY)
5636
- return {};
5637
- const text = await Promise.race([
5638
- (async () => {
5639
- let raw = "";
5640
- process.stdin.setEncoding("utf8");
5641
- for await (const chunk of process.stdin)
5642
- raw += chunk;
5643
- return raw;
5644
- })(),
5645
- new Promise((resolve3) => {
5646
- const timer = setTimeout(() => resolve3(""), STDIN_BUDGET_MS);
5647
- timer.unref?.();
5648
- })
5649
- ]);
5650
- if (text.trim().length === 0)
5651
- return {};
5652
- try {
5653
- return JSON.parse(text);
5654
- } catch {
5655
- return {};
5408
+ function describeForModel(outcome) {
5409
+ switch (outcome.kind) {
5410
+ case "quiet":
5411
+ return [];
5412
+ case "signed-out":
5413
+ return [
5414
+ "yourskills: the user's session with the skills registry has expired, so installed skills are no longer being updated. The user has been told to run `yourskills login`; it is their call."
5415
+ ];
5416
+ case "behind": {
5417
+ const lines = [
5418
+ `yourskills: ${count(outcome.moved.length, "installed skill")} ${outcome.moved.length === 1 ? "is" : "are"} behind the registry. Automatic updates are off; the user has been told and it is their call whether to run \`yourskills update --all\`.`
5419
+ ];
5420
+ for (const row of outcome.moved) {
5421
+ lines.push(` ${row.name} installed ${shortRef(row.installed)}, registry pins ${shortRef(row.available)}${column(driftTag(row))}`);
5422
+ }
5423
+ return lines;
5424
+ }
5425
+ case "updated": {
5426
+ const { applied, failed } = outcome;
5427
+ const lines = [];
5428
+ if (applied.length > 0) {
5429
+ lines.push(`yourskills updated ${count(applied.length, "installed skill")} on disk at the start of this session. The contents changed: re-read a skill before relying on what you remember of it.`);
5430
+ for (const row of applied) {
5431
+ lines.push(` ${row.name} ${row.from} -> ${row.to}`);
5432
+ }
5433
+ lines.push(` recorded in ${outcome.manifest}`);
5434
+ }
5435
+ if (failed.length > 0) {
5436
+ lines.push(`yourskills could not update ${count(failed.length, "installed skill")}; the version on disk is unchanged. The user has been told.`);
5437
+ for (const row of failed) {
5438
+ lines.push(` ${row.name} ${row.message}`);
5439
+ }
5440
+ }
5441
+ return lines;
5442
+ }
5656
5443
  }
5657
5444
  }
5658
- async function lookup(name, cwd) {
5659
- const nearest = await nearestManifest(undefined, cwd);
5660
- const found = nearest.manifest.skills[name];
5661
- if (found)
5662
- return found;
5663
- if (nearest.scope === "global")
5445
+ function describeStale(stale) {
5446
+ if (!stale)
5447
+ return [];
5448
+ return [
5449
+ `yourskills: you are running ${stale.current}; ${stale.latest} is out.`,
5450
+ " run: yourskills upgrade"
5451
+ ];
5452
+ }
5453
+ function describeStaleForModel(stale) {
5454
+ if (!stale)
5455
+ return [];
5456
+ return [
5457
+ `yourskills: the installed CLI is ${stale.current} and ${stale.latest} is published. If the user hits a bug in the \`yourskills\` command itself, check whether it is fixed on the newer version before investigating further. Upgrading is their call; they have been told.`
5458
+ ];
5459
+ }
5460
+ function count(n, noun) {
5461
+ return `${n} ${noun}${n === 1 ? "" : "s"}`;
5462
+ }
5463
+ function column(text) {
5464
+ return text ? ` ${text}` : "";
5465
+ }
5466
+ function emit(outcome, stale = null) {
5467
+ const lines = [...describe(outcome), ...describeStale(stale)];
5468
+ if (lines.length === 0)
5664
5469
  return;
5665
- const global = readManifest(manifestPathFor("global"));
5666
- return global.skills[name];
5470
+ const installed = outcome.kind === "updated" && outcome.applied.length > 0;
5471
+ console.log(JSON.stringify({
5472
+ systemMessage: lines.join(`
5473
+ `),
5474
+ hookSpecificOutput: {
5475
+ hookEventName: "SessionStart",
5476
+ additionalContext: [
5477
+ ...describeForModel(outcome),
5478
+ ...describeStaleForModel(stale)
5479
+ ].join(`
5480
+ `),
5481
+ ...installed ? { reloadSkills: true } : {}
5482
+ }
5483
+ }));
5667
5484
  }
5668
- async function queueInvocation(payload, now = new Date) {
5669
- if (!await trackingEnabled())
5670
- return null;
5671
- const name = payload.tool_input?.skill;
5672
- if (typeof name !== "string" || name.length === 0)
5673
- return null;
5674
- const entry = await lookup(name, payload.cwd);
5675
- if (!entry)
5485
+ // src/hooks/ensure.ts
5486
+ import { existsSync as existsSync4 } from "node:fs";
5487
+ async function registerInto(adapter, tracking) {
5488
+ const wanted2 = adapter.entries.filter((entry) => entry.kind === "update-check" || tracking);
5489
+ if (wanted2.length === 0)
5676
5490
  return null;
5677
- const event = {
5678
- skillExternalId: entry.externalId,
5679
- ref: entry.ref,
5680
- agent: agentForEvent("usage", "PostToolUse"),
5681
- occurredAt: now.toISOString()
5682
- };
5683
- await appendEvent(event);
5684
- return event;
5491
+ const path = adapter.settingsPath();
5492
+ if (!path)
5493
+ return null;
5494
+ if (!existsSync4(adapter.home()))
5495
+ return null;
5496
+ const applied = await applyTo(path, (settings) => addHooks(settings, wanted2));
5497
+ if (!applied.changed)
5498
+ return null;
5499
+ return { agent: adapter.agent, path, entries: wanted2 };
5685
5500
  }
5686
- async function runHook(event) {
5687
- try {
5688
- const payload = await readStdin();
5689
- if (event === "post-tool-use")
5690
- await queueInvocation(payload);
5691
- else
5692
- await runSessionStart();
5693
- } catch {}
5694
- process.exitCode = 0;
5501
+ function linesFor2(registered, tracking) {
5502
+ const agents = registered.map((r) => r.agent).join(", ");
5503
+ const usage = registered.some((r) => r.entries.some((entry) => entry.kind === "usage"));
5504
+ const lines = [
5505
+ `+ yourskills will check your skills once a day, when ${agents} starts, and update what moved`
5506
+ ];
5507
+ if (usage) {
5508
+ lines.push(`+ yourskills will count which of your organization's skills you invoke in ${agents}, so your organization can see what is used`);
5509
+ }
5510
+ for (const { agent, path } of registered) {
5511
+ lines.push(` ${agent} ${path}`);
5512
+ }
5513
+ lines.push(" undo with: yourskills hooks uninstall");
5514
+ if (usage && tracking && !trackingDisabledByEnv()) {
5515
+ lines.push(" turn usage counting off with: yourskills config set track false");
5516
+ }
5517
+ return lines;
5695
5518
  }
5696
- async function runSessionStart() {
5697
- await flushQuietly();
5698
- emit(await sessionStart());
5519
+ async function ensureHooksRegistered() {
5520
+ try {
5521
+ const config = await readConfig();
5522
+ if (config.hooksOffered)
5523
+ return [];
5524
+ if (!config.token)
5525
+ return [];
5526
+ const tracking = trackingOn(config);
5527
+ const registered = [];
5528
+ for (const adapter of HOOK_ADAPTERS) {
5529
+ try {
5530
+ const result = await registerInto(adapter, tracking);
5531
+ if (result)
5532
+ registered.push(result);
5533
+ } catch {}
5534
+ }
5535
+ await updateConfig((stored) => ({
5536
+ ...stored,
5537
+ hooksOffered: new Date().toISOString()
5538
+ }));
5539
+ return registered.length > 0 ? linesFor2(registered, tracking) : [];
5540
+ } catch {
5541
+ return [];
5542
+ }
5699
5543
  }
5700
5544
  // src/upgrade/upgrade.ts
5701
5545
  import { spawn as spawn3 } from "node:child_process";
@@ -5703,7 +5547,7 @@ import { realpathSync } from "node:fs";
5703
5547
  // package.json
5704
5548
  var package_default = {
5705
5549
  name: "yourskills",
5706
- version: "0.1.4",
5550
+ version: "0.1.5",
5707
5551
  description: "Browse, install and keep up to date the agent skills and bundles your team publishes on yourskills.",
5708
5552
  license: "UNLICENSED",
5709
5553
  homepage: "https://yourskills.store",
@@ -5762,7 +5606,7 @@ var VERSION = package_default.version ?? "0.0.0-dev";
5762
5606
  // src/upgrade/upgrade.ts
5763
5607
  var PACKAGE = "yourskills";
5764
5608
  var REGISTRY = `https://registry.npmjs.org/${PACKAGE}/latest`;
5765
- var DAY_MS2 = 24 * 60 * 60 * 1000;
5609
+ var DAY_MS = 24 * 60 * 60 * 1000;
5766
5610
  function installMethod(entry = process.argv[1], env = process.env) {
5767
5611
  let path = entry ?? "";
5768
5612
  try {
@@ -5921,33 +5765,232 @@ async function remember(latest) {
5921
5765
  }));
5922
5766
  } catch {}
5923
5767
  }
5924
- async function upgradeNotice(now = new Date) {
5768
+ var ASK_TIMEOUT_MS = 1500;
5769
+ async function upgradeCheck({
5770
+ now = new Date,
5771
+ current = VERSION,
5772
+ ask = latestVersion
5773
+ } = {}) {
5925
5774
  if (process.env.YOURSKILLS_NO_UPGRADE_CHECK === "1")
5926
5775
  return null;
5927
5776
  try {
5928
5777
  const config = await readConfig();
5929
5778
  const last = config.lastUpgradeCheck ? Date.parse(config.lastUpgradeCheck) : 0;
5930
5779
  let latest = config.latestVersion ?? null;
5931
- if (Number.isNaN(last) || now.getTime() - last >= DAY_MS2) {
5780
+ if (Number.isNaN(last) || now.getTime() - last >= DAY_MS) {
5932
5781
  await updateConfig((stored) => ({
5933
5782
  ...stored,
5934
5783
  lastUpgradeCheck: now.toISOString()
5935
5784
  }));
5936
- latest = await latestVersion(1500);
5937
- const answered = latest;
5938
- if (answered)
5785
+ const answered = await ask(ASK_TIMEOUT_MS);
5786
+ if (answered) {
5787
+ latest = answered;
5939
5788
  await updateConfig((stored) => ({
5940
5789
  ...stored,
5941
5790
  latestVersion: answered
5942
5791
  }));
5792
+ }
5943
5793
  }
5944
- if (!latest || !isNewer(latest, VERSION))
5794
+ if (!latest || !isNewer(latest, current))
5945
5795
  return null;
5946
- return `+ yourskills ${latest} is out; you have ${VERSION}. run: yourskills upgrade`;
5796
+ return { current, latest };
5947
5797
  } catch {
5948
5798
  return null;
5949
5799
  }
5950
5800
  }
5801
+ async function upgradeNotice(now = new Date) {
5802
+ const stale = await upgradeCheck({ now });
5803
+ if (!stale)
5804
+ return null;
5805
+ return `+ yourskills ${stale.latest} is out; you have ${stale.current}. run: yourskills upgrade`;
5806
+ }
5807
+ // src/hooks/session.ts
5808
+ var CHECK_BUDGET_MS = 5000;
5809
+ var UPDATE_BUDGET_MS = 90000;
5810
+ var DAY_MS2 = 86400000;
5811
+ var REAL = {
5812
+ now: () => Date.now(),
5813
+ readConfig,
5814
+ updateConfig,
5815
+ connect: () => createClient(),
5816
+ nearestManifest: () => nearestManifest(),
5817
+ driftNow,
5818
+ add,
5819
+ checkBudgetMs: CHECK_BUDGET_MS,
5820
+ resolveAgents: () => resolveAgents(),
5821
+ landed: listInstalled
5822
+ };
5823
+ function updateCheckDue(last, now = Date.now()) {
5824
+ if (!last)
5825
+ return true;
5826
+ const at = Date.parse(last);
5827
+ return Number.isNaN(at) || now - at >= DAY_MS2;
5828
+ }
5829
+ function withBudget(work, ms) {
5830
+ return Promise.race([
5831
+ work,
5832
+ new Promise((resolve3) => {
5833
+ const timer = setTimeout(() => resolve3(undefined), ms);
5834
+ timer.unref?.();
5835
+ })
5836
+ ]);
5837
+ }
5838
+ async function check(ports, client, nearest) {
5839
+ try {
5840
+ const report2 = await ports.driftNow(client, nearest);
5841
+ return { ok: true, moved: report2.moved };
5842
+ } catch (error) {
5843
+ return { ok: false, signedOut: isUnauthorized(error) };
5844
+ }
5845
+ }
5846
+ async function applyUpdates(ports, client, nearest, moved, deadline) {
5847
+ const where2 = {
5848
+ ...nearest,
5849
+ agents: await ports.resolveAgents(),
5850
+ landed: ports.landed
5851
+ };
5852
+ const wanted2 = new Map(moved.map((row) => [row.name, row]));
5853
+ const applied = [];
5854
+ const failed = [];
5855
+ const names = [
5856
+ ...new Set(moved.map((row) => row.bundle).filter((name) => !!name))
5857
+ ].sort().map((bundle) => `bundle:${bundle}`).concat(moved.filter((row) => !row.bundle).map((row) => row.name).sort());
5858
+ try {
5859
+ for await (const event of ports.add(names, where2, async () => client).stream()) {
5860
+ if (event.kind === "installed") {
5861
+ const row = wanted2.get(event.location.skillName);
5862
+ if (row) {
5863
+ applied.push({
5864
+ name: row.name,
5865
+ from: shortRef(row.installed),
5866
+ to: shortRef(row.available)
5867
+ });
5868
+ }
5869
+ } else if (event.kind === "failed" && wanted2.has(event.name)) {
5870
+ failed.push({ name: event.name, message: event.message });
5871
+ }
5872
+ if (ports.now() > deadline)
5873
+ break;
5874
+ }
5875
+ } catch (error) {
5876
+ failed.push({
5877
+ name: names.join(", "),
5878
+ message: error instanceof Error ? error.message : String(error)
5879
+ });
5880
+ }
5881
+ if (applied.length === 0 && failed.length === 0)
5882
+ return { kind: "quiet" };
5883
+ return { kind: "updated", applied, failed, manifest: nearest.path };
5884
+ }
5885
+ async function sessionStart(ports = {}) {
5886
+ const use = { ...REAL, ...ports };
5887
+ const config = await use.readConfig();
5888
+ if (!updateCheckDue(config.lastUpdateCheck, use.now())) {
5889
+ return { kind: "quiet" };
5890
+ }
5891
+ const written = await use.updateConfig((current) => ({
5892
+ ...current,
5893
+ lastUpdateCheck: new Date(use.now()).toISOString()
5894
+ }));
5895
+ if (written === null)
5896
+ return { kind: "quiet" };
5897
+ if (!config.token)
5898
+ return { kind: "quiet" };
5899
+ let client;
5900
+ let nearest;
5901
+ try {
5902
+ client = await use.connect();
5903
+ nearest = await use.nearestManifest();
5904
+ } catch {
5905
+ return { kind: "quiet" };
5906
+ }
5907
+ if (Object.keys(nearest.manifest.skills).length === 0)
5908
+ return { kind: "quiet" };
5909
+ const found = await withBudget(check(use, client, nearest), use.checkBudgetMs);
5910
+ if (!found)
5911
+ return { kind: "quiet" };
5912
+ if (!found.ok)
5913
+ return found.signedOut ? { kind: "signed-out" } : { kind: "quiet" };
5914
+ if (found.moved.length === 0)
5915
+ return { kind: "quiet" };
5916
+ if (config.autoUpdate === false) {
5917
+ return { kind: "behind", moved: found.moved, manifest: nearest.path };
5918
+ }
5919
+ return applyUpdates(use, client, nearest, found.moved, use.now() + UPDATE_BUDGET_MS);
5920
+ }
5921
+
5922
+ // src/hooks/run.ts
5923
+ function isHookEventName(value) {
5924
+ return value === "session-start" || value === "post-tool-use";
5925
+ }
5926
+ var STDIN_BUDGET_MS = 2000;
5927
+ async function readStdin() {
5928
+ if (process.stdin.isTTY)
5929
+ return {};
5930
+ const text = await Promise.race([
5931
+ (async () => {
5932
+ let raw = "";
5933
+ process.stdin.setEncoding("utf8");
5934
+ for await (const chunk of process.stdin)
5935
+ raw += chunk;
5936
+ return raw;
5937
+ })(),
5938
+ new Promise((resolve3) => {
5939
+ const timer = setTimeout(() => resolve3(""), STDIN_BUDGET_MS);
5940
+ timer.unref?.();
5941
+ })
5942
+ ]);
5943
+ if (text.trim().length === 0)
5944
+ return {};
5945
+ try {
5946
+ return JSON.parse(text);
5947
+ } catch {
5948
+ return {};
5949
+ }
5950
+ }
5951
+ async function lookup(name, cwd) {
5952
+ const nearest = await nearestManifest(undefined, cwd);
5953
+ const found = nearest.manifest.skills[name];
5954
+ if (found)
5955
+ return found;
5956
+ if (nearest.scope === "global")
5957
+ return;
5958
+ const global = readManifest(manifestPathFor("global"));
5959
+ return global.skills[name];
5960
+ }
5961
+ async function queueInvocation(payload, now = new Date) {
5962
+ if (!await trackingEnabled())
5963
+ return null;
5964
+ const name = payload.tool_input?.skill;
5965
+ if (typeof name !== "string" || name.length === 0)
5966
+ return null;
5967
+ const entry = await lookup(name, payload.cwd);
5968
+ if (!entry)
5969
+ return null;
5970
+ const event = {
5971
+ skillExternalId: entry.externalId,
5972
+ ref: entry.ref,
5973
+ agent: agentForEvent("usage", "PostToolUse"),
5974
+ occurredAt: now.toISOString()
5975
+ };
5976
+ await appendEvent(event);
5977
+ return event;
5978
+ }
5979
+ async function runHook(event) {
5980
+ try {
5981
+ const payload = await readStdin();
5982
+ if (event === "post-tool-use")
5983
+ await queueInvocation(payload);
5984
+ else
5985
+ await runSessionStart();
5986
+ } catch {}
5987
+ process.exitCode = 0;
5988
+ }
5989
+ async function runSessionStart() {
5990
+ await flushQuietly();
5991
+ const outcome = await sessionStart();
5992
+ emit(outcome, await upgradeCheck());
5993
+ }
5951
5994
  // src/doctor/doctor.ts
5952
5995
  var REACH_TIMEOUT_MS = 5000;
5953
5996
  function withLogin(message) {
@@ -6281,7 +6324,7 @@ function trackFrom(config) {
6281
6324
  return ["off", "YOURSKILLS_NO_TRACK in the environment"];
6282
6325
  if (config.track !== undefined)
6283
6326
  return [config.track ? "on" : "off", `track in ${configPath()}`];
6284
- return ["off", "built-in default: opt in with `hooks install --track`"];
6327
+ return ["on", "built-in default: turn off with `config set track false`"];
6285
6328
  }
6286
6329
  var LABEL = 12;
6287
6330
  function row(label, value, from) {
@@ -7101,7 +7144,7 @@ var TABLE = {
7101
7144
  },
7102
7145
  {
7103
7146
  usage: "hooks install",
7104
- blurb: "Register them: the session update, and usage if opted in"
7147
+ blurb: "Register them: the session update, and usage unless turned off"
7105
7148
  },
7106
7149
  {
7107
7150
  usage: "hooks uninstall",
@@ -8031,7 +8074,18 @@ function AccountLine({ facts, width }) {
8031
8074
  ]
8032
8075
  });
8033
8076
  }
8077
+ function StaleLine({ stale, width }) {
8078
+ const text = `${mark.ok} ${stale.latest} is out; you have ${stale.current}. type upgrade`;
8079
+ return /* @__PURE__ */ jsx18(Box14, {
8080
+ width: width - FRAME_CHROME,
8081
+ children: /* @__PURE__ */ jsx18(Text16, {
8082
+ color: tone.notice,
8083
+ children: clip(text, width - FRAME_CHROME)
8084
+ })
8085
+ });
8086
+ }
8034
8087
  var RUN = "run";
8088
+ var ASK_NPM = () => upgradeCheck();
8035
8089
  var NO_EXIT_CODE = () => {
8036
8090
  return;
8037
8091
  };
@@ -8046,7 +8100,8 @@ function HomeScreen({
8046
8100
  onSummary,
8047
8101
  onWelcomed = async () => {
8048
8102
  return;
8049
- }
8103
+ },
8104
+ checkUpgrade = ASK_NPM
8050
8105
  }) {
8051
8106
  const { exit } = useApp7();
8052
8107
  const width = useFrameWidth();
@@ -8058,6 +8113,19 @@ function HomeScreen({
8058
8113
  const [route, setRoute] = useState11(null);
8059
8114
  const [welcome, setWelcome] = useState11(initial.firstRun);
8060
8115
  const [wizard, setWizard] = useState11(false);
8116
+ const [stale, setStale] = useState11(null);
8117
+ useEffect9(() => {
8118
+ let live = true;
8119
+ checkUpgrade().then((found) => {
8120
+ if (live)
8121
+ setStale(found);
8122
+ }).catch(() => {
8123
+ return;
8124
+ });
8125
+ return () => {
8126
+ live = false;
8127
+ };
8128
+ }, [checkUpgrade]);
8061
8129
  const typed = useMemo4(() => typedCommand(query), [query]);
8062
8130
  const visible = useMemo4(() => {
8063
8131
  const filtered = filterCommands(groups, query);
@@ -8406,6 +8474,10 @@ function HomeScreen({
8406
8474
  facts,
8407
8475
  width
8408
8476
  }),
8477
+ stale ? /* @__PURE__ */ jsx18(StaleLine, {
8478
+ stale,
8479
+ width
8480
+ }) : null,
8409
8481
  /* @__PURE__ */ jsx18(Divider, {
8410
8482
  width
8411
8483
  }),