@jacebenson/jsn 3.2.0 → 3.3.1

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.
@@ -6,6 +6,8 @@ import { fileURLToPath } from 'node:url';
6
6
 
7
7
  import { execSync } from 'node:child_process';
8
8
 
9
+ import { globalConfigPath } from '../config.js';
10
+
9
11
  const SKILL_NAME = 'servicenow';
10
12
  const SKILL_REPO_PATH = path.join(
11
13
  path.dirname(fileURLToPath(import.meta.url)),
@@ -94,73 +96,75 @@ function readBundledSkill() {
94
96
  }
95
97
  }
96
98
 
97
- export async function checkSkill() {
98
- // Compare all installed skill copies against GitHub.
99
- // Uses a single HTTP fetch and reads all harness paths that exist.
100
- // Returns the first installed version found for comparison, plus a list
101
- // of harnesses where the skill is current vs outdated (or missing).
102
- const SKILL_NAME = 'servicenow';
103
-
104
- // Collect all installed copies that actually exist on disk
105
- const installedCopies = [];
106
- for (const [key, dir] of Object.entries(AGENT_SKILL_DIRS)) {
107
- const skillPath = key === 'cursor'
108
- ? path.join(dir, `${SKILL_NAME}.mdc`)
109
- : path.join(dir, 'SKILL.md');
110
- try {
111
- const content = fs.readFileSync(skillPath, 'utf-8');
112
- installedCopies.push({ harness: key, label: TARGET_NAMES[key], content, path: skillPath });
113
- } catch {
114
- // Not installed to this harness — skip
115
- }
99
+ // ── Skill config helpers — read/write skillLocation / skillVersion / skillLastChecked ──
100
+
101
+ function loadSkillConfig() {
102
+ try {
103
+ const raw = fs.readFileSync(globalConfigPath(), 'utf-8');
104
+ return JSON.parse(raw);
105
+ } catch {
106
+ return {};
116
107
  }
108
+ }
117
109
 
118
- // Fall back to bundled skill if nothing installed yet
119
- const primary = installedCopies[0] || { content: readBundledSkill() };
120
- if (!primary.content) return { current: false, error: 'Skill file not found' };
110
+ function saveSkillConfig(updates) {
111
+ const cfg = loadSkillConfig();
112
+ Object.assign(cfg, updates);
113
+ fs.mkdirSync(path.dirname(globalConfigPath()), { recursive: true });
114
+ fs.writeFileSync(globalConfigPath(), JSON.stringify(cfg, null, 2), { mode: 0o600 });
115
+ }
121
116
 
122
- const installedVersion = extractVersion(primary.content);
123
- if (!installedVersion) return { current: false, error: 'No version field in installed skill' };
117
+ function recordSkillInstall(targetPath) {
118
+ const bundled = readBundledSkill();
119
+ const version = extractVersion(bundled) || 'unknown';
120
+ saveSkillConfig({
121
+ skillLocation: targetPath,
122
+ skillVersion: version,
123
+ skillLastChecked: new Date().toISOString(),
124
+ });
125
+ }
124
126
 
125
- try {
126
- const controller = new AbortController();
127
- const timer = setTimeout(() => controller.abort(), 10000);
128
- const res = await fetch(SKILL_RAW_URL, { signal: controller.signal });
129
- clearTimeout(timer);
130
- if (!res.ok) return { current: false, error: `GitHub returned ${res.status}` };
131
- const upstream = await res.text();
132
- const upstreamVersion = extractVersion(upstream);
133
- if (!upstreamVersion) return { current: false, error: 'No version field in upstream skill' };
134
-
135
- const current = installedVersion === upstreamVersion;
136
-
137
- // Check each installed copy for version match
138
- const outdated = [];
139
- for (const copy of installedCopies) {
140
- const v = extractVersion(copy.content);
141
- if (v && v !== upstreamVersion) {
142
- outdated.push(copy.label);
143
- }
144
- }
127
+ // ── Skill version check (once per day, uses recorded location) ──
128
+
129
+ export function checkSkill() {
130
+ const cfg = loadSkillConfig();
131
+ const location = cfg.skillLocation;
132
+ if (!location) return { current: true, note: 'Skill not installed — run "jsn skill install"' };
145
133
 
146
- const outdatedMsg = outdated.length > 0
147
- ? ` Skill outdated for ${outdated.join(', ')} (v${installedVersion} vs GitHub v${upstreamVersion}) — run "jsn skill install" to update`
148
- : null;
149
-
150
- return {
151
- current,
152
- installed_version: installedVersion,
153
- upstream_version: upstreamVersion,
154
- checked: installedCopies.length,
155
- harnesses: {
156
- installed: installedCopies.map(c => c.label),
157
- outdated,
158
- },
159
- error: current ? null : outdatedMsg,
160
- };
134
+ // Throttle: skip if checked within last 24 hours
135
+ if (cfg.skillLastChecked) {
136
+ const elapsed = Date.now() - new Date(cfg.skillLastChecked).getTime();
137
+ if (elapsed < 86400000) return { current: true, note: 'Checked within 24h — skipping' };
138
+ }
139
+
140
+ let installed;
141
+ try {
142
+ installed = fs.readFileSync(location, 'utf-8');
161
143
  } catch {
162
- return { current: false, error: 'Could not check — GitHub unreachable' };
144
+ return { current: true, note: `Skill file not found at ${location} — run "jsn skill install"` };
163
145
  }
146
+
147
+ const installedVersion = extractVersion(installed);
148
+ const bundled = readBundledSkill();
149
+ const bundledVersion = extractVersion(bundled);
150
+
151
+ if (!installedVersion || !bundledVersion) {
152
+ return { current: true, error: 'Could not determine skill version' };
153
+ }
154
+
155
+ const current = installedVersion === bundledVersion;
156
+
157
+ if (!current) {
158
+ process.stderr.write(
159
+ `\n⚠ jsn: ServiceNow skill is outdated (installed v${installedVersion}, bundled v${bundledVersion}).\n` +
160
+ ` Run "jsn skill install" to update.\n\n`
161
+ );
162
+ }
163
+
164
+ // Update last-checked timestamp
165
+ saveSkillConfig({ skillLastChecked: new Date().toISOString() });
166
+
167
+ return { current, installed_version: installedVersion, bundled_version: bundledVersion };
164
168
  }
165
169
 
166
170
  function extractVersion(content) {
@@ -196,28 +200,19 @@ export function skillCmd(wrap) {
196
200
  })
197
201
  .command({
198
202
  command: 'check',
199
- describe: 'Check if the bundled skill file is up to date with GitHub',
203
+ describe: 'Check if the installed skill matches the bundled version',
200
204
  handler: wrap(async (_argv, app) => {
201
- const result = await checkSkill();
202
- const h = result.harnesses || {};
205
+ const result = checkSkill();
203
206
 
204
- // Build a summary that shows which harnesses are installed and current
205
207
  let summary;
206
- if (result.error && !result.current) {
208
+ if (result.error) {
207
209
  summary = result.error;
210
+ } else if (result.note) {
211
+ summary = result.note;
208
212
  } else if (result.current) {
209
- const parts = [`✓ Skill is current (v${result.installed_version})`];
210
- if (h.installed && h.installed.length > 0) {
211
- parts.push(` — installed for ${h.installed.join(', ')}`);
212
- }
213
- summary = parts.join('');
213
+ summary = `✓ Skill is current (v${result.installed_version} matches bundled v${result.bundled_version})`;
214
214
  } else {
215
- const parts = [`⚠ Skill outdated: installed v${result.installed_version} vs GitHub v${result.upstream_version}`];
216
- if (h.outdated && h.outdated.length > 0) {
217
- parts.push(` — outdated for ${h.outdated.join(', ')}`);
218
- }
219
- parts.push(` — run "jsn skill install" to update`);
220
- summary = parts.join('');
215
+ summary = `⚠ Skill outdated: installed v${result.installed_version} vs bundled v${result.bundled_version} — run "jsn skill install" to update`;
221
216
  }
222
217
 
223
218
  app.ok(result, { summary });
@@ -386,6 +381,9 @@ export function skillCmd(wrap) {
386
381
  throw new Error(`No targets matched. Valid targets: ${Object.keys(AGENT_SKILL_DIRS).join(', ')}, or "all"`);
387
382
  }
388
383
 
384
+ // Record install location so checkSkill knows where to look
385
+ recordSkillInstall(targets[0].path);
386
+
389
387
  const installed = targets.reduce((acc, t) => { acc[t.name] = t.path; return acc; }, {});
390
388
  const summary = targets.length === 1
391
389
  ? `Skill installed to ${targets[0].path}`
@@ -0,0 +1,13 @@
1
+ // Suppresses the node:sqlite ExperimentalWarning on Node 22.x.
2
+ // Loaded via NODE_OPTIONS=--require before the main ESM entry point.
3
+
4
+ const original = process.emitWarning;
5
+ process.emitWarning = function emitWarning(warning, ...args) {
6
+ const message = typeof warning === 'string'
7
+ ? warning
8
+ : (warning?.message || '');
9
+ if (message.includes('SQLite')) {
10
+ return;
11
+ }
12
+ return original.apply(this, [warning, ...args]);
13
+ };