@jacebenson/jsn 4.3.13-dev.96.1 → 4.3.13-dev.98.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.
- package/package.json +1 -1
- package/src/commands/skill.js +65 -69
package/package.json
CHANGED
package/src/commands/skill.js
CHANGED
|
@@ -18,8 +18,6 @@ const SKILL_REPO_PATH = path.join(
|
|
|
18
18
|
'..', '..', 'skills', SKILL_NAME, 'SKILL.md'
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
-
const SKILL_RAW_URL = 'https://raw.githubusercontent.com/jacebenson/jsn/main/skills/servicenow/SKILL.md';
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* Resolve the user's real home directory, even when Hermes overrides $HOME.
|
|
25
23
|
*/
|
|
@@ -119,61 +117,56 @@ function saveSkillConfig(updates) {
|
|
|
119
117
|
fs.writeFileSync(globalConfigPath(), JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
function
|
|
120
|
+
function replaceSkill(targetPath, content) {
|
|
121
|
+
const temporary = `${targetPath}.${process.pid}.tmp`;
|
|
122
|
+
try {
|
|
123
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf-8', mode: 0o600 });
|
|
124
|
+
fs.renameSync(temporary, targetPath);
|
|
125
|
+
} finally {
|
|
126
|
+
fs.rmSync(temporary, { force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function recordSkillInstall(targetPaths, content) {
|
|
123
131
|
const version = extractVersion(content) || 'unknown';
|
|
124
132
|
saveSkillConfig({
|
|
125
|
-
skillLocation:
|
|
133
|
+
skillLocation: targetPaths[0],
|
|
134
|
+
skillLocations: targetPaths,
|
|
126
135
|
skillVersion: version,
|
|
127
136
|
skillLastChecked: new Date().toISOString(),
|
|
128
137
|
});
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
export function checkSkill() {
|
|
140
|
+
// Compare bytes, including edits that retain the same version number.
|
|
141
|
+
export function checkSkill({ warn = true } = {}) {
|
|
134
142
|
const cfg = loadSkillConfig();
|
|
135
|
-
const
|
|
136
|
-
if (!location) return { current: true, note: 'Skill not installed — run "jsn skill install"' };
|
|
137
|
-
|
|
138
|
-
// Throttle: skip if checked within last 24 hours
|
|
139
|
-
if (cfg.skillLastChecked) {
|
|
140
|
-
const elapsed = Date.now() - new Date(cfg.skillLastChecked).getTime();
|
|
141
|
-
if (elapsed < 86400000) return { current: true, note: 'Checked within 24h — skipping' };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
let installed;
|
|
145
|
-
try {
|
|
146
|
-
installed = fs.readFileSync(location, 'utf-8');
|
|
147
|
-
} catch {
|
|
148
|
-
return { current: true, note: `Skill file not found at ${location} — run "jsn skill install"` };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const installedVersion = extractVersion(installed);
|
|
143
|
+
const locations = cfg.skillLocations || (cfg.skillLocation ? [cfg.skillLocation] : []);
|
|
152
144
|
const bundled = readBundledSkill();
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
145
|
+
if (bundled === null) return { current: false, error: 'Bundled skill missing; reinstall jsn.' };
|
|
146
|
+
if (!locations.length) return { current: false, note: 'Skill not installed — run "jsn skill install"' };
|
|
147
|
+
const copies = locations.map(location => {
|
|
148
|
+
let installed;
|
|
149
|
+
try { installed = fs.readFileSync(location, 'utf-8'); } catch {
|
|
150
|
+
return { location, current: false, status: 'missing' };
|
|
151
|
+
}
|
|
152
|
+
const current = installed === bundled;
|
|
153
|
+
const actual = installed.split('\n');
|
|
154
|
+
const expected = bundled.split('\n');
|
|
155
|
+
const line = current ? undefined : Array.from({ length: Math.max(actual.length, expected.length) }, (_, i) => i)
|
|
156
|
+
.find(i => actual[i] !== expected[i]) + 1;
|
|
157
|
+
return { location, current, status: current ? 'matching' : 'different',
|
|
158
|
+
installed_version: extractVersion(installed), first_different_line: line };
|
|
159
|
+
});
|
|
160
|
+
const current = copies.every(copy => copy.current);
|
|
161
|
+
if (!current && warn) process.stderr.write('ServiceNow skill differs from the packaged copy. Run "jsn skill check" for locations or "jsn skill install" to replace it.\n');
|
|
169
162
|
saveSkillConfig({ skillLastChecked: new Date().toISOString() });
|
|
170
|
-
|
|
171
|
-
|
|
163
|
+
return { current, installed_version: copies[0].installed_version,
|
|
164
|
+
bundled_version: extractVersion(bundled), copies };
|
|
172
165
|
}
|
|
173
166
|
|
|
174
167
|
function extractVersion(content) {
|
|
175
168
|
// Matches version in YAML frontmatter (top-level or nested under metadata:)
|
|
176
|
-
const m = content.match(/^\s*version:\s*["']?(.+?)["']?\s*$/m);
|
|
169
|
+
const m = (content || '').match(/^\s*version:\s*["']?(.+?)["']?\s*$/m);
|
|
177
170
|
return m ? m[1] : null;
|
|
178
171
|
}
|
|
179
172
|
|
|
@@ -204,9 +197,9 @@ export function skillCmd(wrap) {
|
|
|
204
197
|
})
|
|
205
198
|
.command({
|
|
206
199
|
command: 'check',
|
|
207
|
-
describe: '
|
|
200
|
+
describe: 'Compare installed skill contents with the packaged skill',
|
|
208
201
|
handler: wrap(async (_argv, app) => {
|
|
209
|
-
const result = checkSkill();
|
|
202
|
+
const result = checkSkill({ warn: false });
|
|
210
203
|
|
|
211
204
|
let summary;
|
|
212
205
|
if (result.error) {
|
|
@@ -214,21 +207,21 @@ export function skillCmd(wrap) {
|
|
|
214
207
|
} else if (result.note) {
|
|
215
208
|
summary = result.note;
|
|
216
209
|
} else if (result.current) {
|
|
217
|
-
summary = `✓ Skill
|
|
210
|
+
summary = `✓ Skill contents match the packaged copy (v${result.bundled_version})`;
|
|
218
211
|
} else {
|
|
219
|
-
summary = `⚠ Skill
|
|
212
|
+
summary = `⚠ Skill contents differ — run "jsn skill install" to replace installed copies`;
|
|
220
213
|
}
|
|
221
214
|
|
|
222
|
-
|
|
215
|
+
const details = result.copies?.map(copy => `${copy.status}: ${copy.location}${copy.first_different_line ? ` (first difference: line ${copy.first_different_line})` : ''}`).join('\n');
|
|
216
|
+
app.ok({ ...result, _formatted: [summary, details].filter(Boolean).join('\n') }, { summary });
|
|
223
217
|
}),
|
|
224
218
|
})
|
|
225
219
|
.command({
|
|
226
220
|
command: 'fetch',
|
|
227
|
-
describe: '
|
|
221
|
+
describe: 'Print the packaged skill to stdout (offline)',
|
|
228
222
|
handler: wrap(async (_argv, _app) => {
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
231
|
-
const content = await res.text();
|
|
223
|
+
const content = readBundledSkill();
|
|
224
|
+
if (content === null) throw new Error('Bundled skill missing; reinstall jsn.');
|
|
232
225
|
process.stdout.write(content);
|
|
233
226
|
}),
|
|
234
227
|
})
|
|
@@ -245,7 +238,6 @@ export function skillCmd(wrap) {
|
|
|
245
238
|
app.ok({
|
|
246
239
|
bundled: SKILL_REPO_PATH,
|
|
247
240
|
targets: allPaths,
|
|
248
|
-
raw_url: SKILL_RAW_URL,
|
|
249
241
|
}, {
|
|
250
242
|
summary: 'Skill file locations and install targets',
|
|
251
243
|
breadcrumbs: [
|
|
@@ -267,7 +259,7 @@ export function skillCmd(wrap) {
|
|
|
267
259
|
})
|
|
268
260
|
.option('target', {
|
|
269
261
|
type: 'string',
|
|
270
|
-
describe: 'Target agent(s): hermes, copilot, vscode, claude, cursor, agents, opencode, openclaw, codex, or "all" (comma-separated). Omit
|
|
262
|
+
describe: 'Target agent(s): hermes, copilot, vscode, claude, cursor, agents, opencode, openclaw, codex, or "all" (comma-separated). Omit to replace recorded copies, or choose targets on first install.',
|
|
271
263
|
}),
|
|
272
264
|
handler: wrap(async (argv, app) => {
|
|
273
265
|
const content = readBundledSkill();
|
|
@@ -283,7 +275,7 @@ export function skillCmd(wrap) {
|
|
|
283
275
|
const p = path.resolve(argv.dir);
|
|
284
276
|
fs.mkdirSync(p, { recursive: true });
|
|
285
277
|
const targetPath = path.join(p, 'SKILL.md');
|
|
286
|
-
|
|
278
|
+
replaceSkill(targetPath, content);
|
|
287
279
|
targets.push({ name: path.basename(argv.dir), path: targetPath });
|
|
288
280
|
} else if (argv.target) {
|
|
289
281
|
// Explicit --target: install to specified agents
|
|
@@ -296,38 +288,41 @@ export function skillCmd(wrap) {
|
|
|
296
288
|
if (key === 'cursor') {
|
|
297
289
|
fs.mkdirSync(dir, { recursive: true });
|
|
298
290
|
const targetPath = path.join(dir, `${SKILL_NAME}.mdc`);
|
|
299
|
-
|
|
291
|
+
replaceSkill(targetPath, content);
|
|
300
292
|
targets.push({ name: TARGET_NAMES[key], path: targetPath });
|
|
301
293
|
} else {
|
|
302
294
|
fs.mkdirSync(dir, { recursive: true });
|
|
303
295
|
const targetPath = path.join(dir, 'SKILL.md');
|
|
304
|
-
|
|
296
|
+
replaceSkill(targetPath, content);
|
|
305
297
|
targets.push({ name: TARGET_NAMES[key], path: targetPath });
|
|
306
298
|
}
|
|
307
299
|
}
|
|
308
300
|
}
|
|
301
|
+
} else if (loadSkillConfig().skillLocation) {
|
|
302
|
+
const cfg = loadSkillConfig();
|
|
303
|
+
for (const targetPath of cfg.skillLocations || [cfg.skillLocation]) {
|
|
304
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
305
|
+
replaceSkill(targetPath, content);
|
|
306
|
+
targets.push({ name: targetPath, path: targetPath });
|
|
307
|
+
}
|
|
309
308
|
} else if (canUsePicker(app, argv)) {
|
|
310
309
|
// Interactive: show multi-select picker. canUsePicker() (not a
|
|
311
310
|
// bare stdin TTY check) keeps a PTY allocated for an agent,
|
|
312
311
|
// CI, JSN_NO_PROMPTS, --json/--quiet or --get out of a prompt.
|
|
313
|
-
const fetchedVersion = extractVersion(content);
|
|
314
312
|
// Check if any existing installed copies are outdated
|
|
315
313
|
const outdated = [];
|
|
316
|
-
|
|
317
|
-
for (const [key, dir] of Object.entries(AGENT_SKILL_DIRS)) {
|
|
314
|
+
for (const [key, dir] of Object.entries(AGENT_SKILL_DIRS)) {
|
|
318
315
|
const skillPath = key === 'cursor'
|
|
319
316
|
? path.join(dir, `${SKILL_NAME}.mdc`)
|
|
320
317
|
: path.join(dir, 'SKILL.md');
|
|
321
318
|
try {
|
|
322
319
|
const existing = fs.readFileSync(skillPath, 'utf-8');
|
|
323
|
-
|
|
324
|
-
if (existingVersion && existingVersion !== fetchedVersion) {
|
|
320
|
+
if (existing !== content) {
|
|
325
321
|
outdated.push(TARGET_NAMES[key]);
|
|
326
322
|
}
|
|
327
323
|
} catch {
|
|
328
324
|
// Not installed — skip
|
|
329
325
|
}
|
|
330
|
-
}
|
|
331
326
|
}
|
|
332
327
|
const promptMsg = outdated.length > 0
|
|
333
328
|
? `Where should the jsn skill be installed? (${outdated.length} outdated)`
|
|
@@ -366,12 +361,12 @@ export function skillCmd(wrap) {
|
|
|
366
361
|
if (key === 'cursor') {
|
|
367
362
|
fs.mkdirSync(dir, { recursive: true });
|
|
368
363
|
const targetPath = path.join(dir, `${SKILL_NAME}.mdc`);
|
|
369
|
-
|
|
364
|
+
replaceSkill(targetPath, content);
|
|
370
365
|
targets.push({ name: TARGET_NAMES[key], path: targetPath });
|
|
371
366
|
} else {
|
|
372
367
|
fs.mkdirSync(dir, { recursive: true });
|
|
373
368
|
const targetPath = path.join(dir, 'SKILL.md');
|
|
374
|
-
|
|
369
|
+
replaceSkill(targetPath, content);
|
|
375
370
|
targets.push({ name: TARGET_NAMES[key], path: targetPath });
|
|
376
371
|
}
|
|
377
372
|
}
|
|
@@ -380,7 +375,7 @@ export function skillCmd(wrap) {
|
|
|
380
375
|
const dir = AGENT_SKILL_DIRS.hermes;
|
|
381
376
|
fs.mkdirSync(dir, { recursive: true });
|
|
382
377
|
const targetPath = path.join(dir, 'SKILL.md');
|
|
383
|
-
|
|
378
|
+
replaceSkill(targetPath, content);
|
|
384
379
|
targets.push({ name: TARGET_NAMES.hermes, path: targetPath });
|
|
385
380
|
}
|
|
386
381
|
|
|
@@ -389,7 +384,7 @@ export function skillCmd(wrap) {
|
|
|
389
384
|
}
|
|
390
385
|
|
|
391
386
|
// Record install location so checkSkill knows where to look
|
|
392
|
-
recordSkillInstall(targets
|
|
387
|
+
recordSkillInstall(targets.map(target => target.path), content);
|
|
393
388
|
|
|
394
389
|
const installed = targets.reduce((acc, t) => { acc[t.name] = t.path; return acc; }, {});
|
|
395
390
|
const summary = targets.length === 1
|
|
@@ -399,7 +394,7 @@ export function skillCmd(wrap) {
|
|
|
399
394
|
const okOpts = {
|
|
400
395
|
summary,
|
|
401
396
|
breadcrumbs: [
|
|
402
|
-
{ action: 'reinstall', cmd: 'jsn skill install', description: '
|
|
397
|
+
{ action: 'reinstall', cmd: 'jsn skill install', description: 'Replace recorded copies with the packaged skill' },
|
|
403
398
|
],
|
|
404
399
|
};
|
|
405
400
|
|
|
@@ -416,7 +411,8 @@ export function skillCmd(wrap) {
|
|
|
416
411
|
console.log('');
|
|
417
412
|
console.log('Available subcommands:');
|
|
418
413
|
console.log(' show Show the bundled skill file');
|
|
419
|
-
console.log(' fetch
|
|
414
|
+
console.log(' fetch Print the packaged skill (offline)');
|
|
415
|
+
console.log(' check Show whether installed contents differ');
|
|
420
416
|
console.log(' path Show skill file locations');
|
|
421
417
|
console.log(' install Install skill file to agents directory');
|
|
422
418
|
console.log('');
|