@jacebenson/jsn 3.0.3 → 3.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/servicenow/SKILL.md +4 -4
- package/src/commands/dev/eval.js +19 -3
- package/src/commands/skill.js +33 -8
- package/src/sdk.js +21 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacebenson/jsn",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
"README.md",
|
|
44
44
|
"LICENSE"
|
|
45
45
|
]
|
|
46
|
-
}
|
|
46
|
+
}
|
|
@@ -10,12 +10,12 @@ description: |
|
|
|
10
10
|
license: MIT
|
|
11
11
|
compatibility: |
|
|
12
12
|
Requires jsn CLI (github.com/jacebenson/jsn). Install via:
|
|
13
|
-
|
|
13
|
+
npm install -g @jacebenson/jsn
|
|
14
14
|
Works with Claude Code, OpenCode, Cursor, and agentskills-compatible tools.
|
|
15
15
|
|
|
16
|
-
Windows users:
|
|
17
|
-
command will automatically detect its location and show
|
|
18
|
-
|
|
16
|
+
Windows users: Install Node.js first, then the npm command above. If jsn is not
|
|
17
|
+
in your PATH, the setup command will automatically detect its location and show
|
|
18
|
+
the correct command path.
|
|
19
19
|
metadata:
|
|
20
20
|
author: jacebenson
|
|
21
21
|
version: "1.0.0"
|
package/src/commands/dev/eval.js
CHANGED
|
@@ -7,10 +7,19 @@ export function evalCmd(wrap) {
|
|
|
7
7
|
builder: (yargs) => {
|
|
8
8
|
return yargs
|
|
9
9
|
.option('script', { alias: 's', type: 'string', describe: "JavaScript code to execute (single-quote the script, double quotes inside: --script 'gs.log(\"Hello\")')" })
|
|
10
|
-
.option('file', { alias: 'f', type: 'string', describe: 'Read script from file (avoids shell quoting issues)' })
|
|
10
|
+
.option('file', { alias: 'f', type: 'string', describe: 'Read script from file (avoids shell quoting issues)' })
|
|
11
|
+
.option('scope', { alias: null, type: 'string', describe: 'Scope name or sys_id to run the script under (default: global)' });
|
|
11
12
|
},
|
|
12
13
|
handler: wrap(async (argv, app) => {
|
|
13
14
|
let script;
|
|
15
|
+
let scopeSysId = null;
|
|
16
|
+
|
|
17
|
+
if (argv.scope) {
|
|
18
|
+
scopeSysId = await app.sdk.resolveScope(argv.scope);
|
|
19
|
+
if (!scopeSysId) {
|
|
20
|
+
throw new Error(`Scope not found: ${argv.scope}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
|
|
15
24
|
if (argv.file) {
|
|
16
25
|
script = fs.readFileSync(argv.file, 'utf-8');
|
|
@@ -20,13 +29,20 @@ export function evalCmd(wrap) {
|
|
|
20
29
|
throw new Error('--script or --file is required');
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
if (scopeSysId) {
|
|
33
|
+
script = `var $scopeSysId = '${scopeSysId}';\n${script}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const warning = scopeSysId
|
|
37
|
+
? ` ⚠️ Records without explicit sys_scope will land in global. Use $scopeSysId variable.`
|
|
38
|
+
: '';
|
|
39
|
+
const output = await app.sdk.executeScript(script, argv.scope);
|
|
24
40
|
app.ok({
|
|
25
41
|
script,
|
|
26
42
|
output,
|
|
27
43
|
instance: app.getEffectiveInstance(),
|
|
28
44
|
}, {
|
|
29
|
-
summary: 'Script executed',
|
|
45
|
+
summary: 'Script executed' + warning,
|
|
30
46
|
breadcrumbs: [{
|
|
31
47
|
action: 'eval',
|
|
32
48
|
cmd: 'jsn dev eval --script \'...\'',
|
package/src/commands/skill.js
CHANGED
|
@@ -8,6 +8,8 @@ const SKILL_REPO_PATH = path.join(
|
|
|
8
8
|
'..', '..', 'skills', 'servicenow', 'SKILL.md'
|
|
9
9
|
);
|
|
10
10
|
|
|
11
|
+
const HERMES_SKILL_PATH = path.join(os.homedir(), '.hermes', 'skills', 'servicenow', 'SKILL.md');
|
|
12
|
+
|
|
11
13
|
const SKILL_RAW_URL = 'https://raw.githubusercontent.com/jacebenson/jsn/nodejs/skills/servicenow/SKILL.md';
|
|
12
14
|
|
|
13
15
|
function readBundledSkill() {
|
|
@@ -18,9 +20,23 @@ function readBundledSkill() {
|
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
function readInstalledSkill() {
|
|
24
|
+
try {
|
|
25
|
+
return fs.readFileSync(HERMES_SKILL_PATH, 'utf-8');
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
export async function checkSkill() {
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
// Compare the Hermes-installed copy's version against GitHub (not the npm-bundled copy).
|
|
33
|
+
// Uses the YAML frontmatter "version" field so publishing jsn doesn't flag it.
|
|
34
|
+
const installed = readInstalledSkill() || readBundledSkill();
|
|
35
|
+
if (!installed) return { current: false, error: 'Skill file not found' };
|
|
36
|
+
|
|
37
|
+
const installedVersion = extractVersion(installed);
|
|
38
|
+
if (!installedVersion) return { current: false, error: 'No version field in installed skill' };
|
|
39
|
+
|
|
24
40
|
try {
|
|
25
41
|
const controller = new AbortController();
|
|
26
42
|
const timer = setTimeout(() => controller.abort(), 10000);
|
|
@@ -28,18 +44,27 @@ export async function checkSkill() {
|
|
|
28
44
|
clearTimeout(timer);
|
|
29
45
|
if (!res.ok) return { current: false, error: `GitHub returned ${res.status}` };
|
|
30
46
|
const upstream = await res.text();
|
|
31
|
-
const
|
|
47
|
+
const upstreamVersion = extractVersion(upstream);
|
|
48
|
+
if (!upstreamVersion) return { current: false, error: 'No version field in upstream skill' };
|
|
49
|
+
|
|
50
|
+
const current = installedVersion === upstreamVersion;
|
|
32
51
|
return {
|
|
33
52
|
current,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
error: current ? null :
|
|
53
|
+
installed_version: installedVersion,
|
|
54
|
+
upstream_version: upstreamVersion,
|
|
55
|
+
error: current ? null : `Skill version ${installedVersion} vs GitHub ${upstreamVersion} — run "jsn skill install" to update`,
|
|
37
56
|
};
|
|
38
57
|
} catch {
|
|
39
58
|
return { current: false, error: 'Could not check — GitHub unreachable' };
|
|
40
59
|
}
|
|
41
60
|
}
|
|
42
61
|
|
|
62
|
+
function extractVersion(content) {
|
|
63
|
+
// Matches version in YAML frontmatter (top-level or nested under metadata:)
|
|
64
|
+
const m = content.match(/^\s*version:\s*["']?(.+?)["']?\s*$/m);
|
|
65
|
+
return m ? m[1] : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
43
68
|
export function skillCmd(wrap) {
|
|
44
69
|
return {
|
|
45
70
|
command: 'skill',
|
|
@@ -70,9 +95,9 @@ export function skillCmd(wrap) {
|
|
|
70
95
|
if (result.error && !result.current) {
|
|
71
96
|
app.ok(result, { summary: result.error });
|
|
72
97
|
} else if (result.current) {
|
|
73
|
-
app.ok(result, { summary: `✓ Skill is current (${result.
|
|
98
|
+
app.ok(result, { summary: `✓ Skill is current (v${result.installed_version})` });
|
|
74
99
|
} else {
|
|
75
|
-
app.ok(result, { summary: `⚠ Skill
|
|
100
|
+
app.ok(result, { summary: `⚠ Skill outdated: installed v${result.installed_version} vs GitHub v${result.upstream_version} — run "jsn skill install" to update` });
|
|
76
101
|
}
|
|
77
102
|
}),
|
|
78
103
|
})
|
package/src/sdk.js
CHANGED
|
@@ -188,6 +188,25 @@ export class SDKClient {
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Resolve a scope name or sys_id to a sys_id.
|
|
193
|
+
* @param {string} scope - Scope name (e.g. "sn_notif_dashboard") or sys_id
|
|
194
|
+
* @returns {Promise<string|null>} The scope sys_id, or null if not found
|
|
195
|
+
*/
|
|
196
|
+
async resolveScope(scope) {
|
|
197
|
+
if (!scope) return null;
|
|
198
|
+
// If it looks like a sys_id (32 hex chars), try it directly
|
|
199
|
+
if (/^[0-9a-fA-F]{32}$/.test(scope)) return scope;
|
|
200
|
+
|
|
201
|
+
const params = new URLSearchParams();
|
|
202
|
+
params.set('sysparm_query', `scope=${scope}`);
|
|
203
|
+
params.set('sysparm_limit', '1');
|
|
204
|
+
params.set('sysparm_fields', 'sys_id,name');
|
|
205
|
+
const records = await this.list('sys_scope', params);
|
|
206
|
+
if (records.length === 0) return null;
|
|
207
|
+
return records[0].sys_id?.value || records[0].sys_id;
|
|
208
|
+
}
|
|
209
|
+
|
|
191
210
|
async inspectFlow(identifier) {
|
|
192
211
|
const isSysID = identifier.length === 32 && /^[0-9a-fA-F]+$/.test(identifier);
|
|
193
212
|
|
|
@@ -372,7 +391,7 @@ export class SDKClient {
|
|
|
372
391
|
* @param {string} script - JavaScript code to execute
|
|
373
392
|
* @returns {Promise<string>} The script's output text
|
|
374
393
|
*/
|
|
375
|
-
async executeScript(script) {
|
|
394
|
+
async executeScript(script, scope) {
|
|
376
395
|
// Step 1: Warm up the session by hitting any REST API — this makes
|
|
377
396
|
// ServiceNow issue session cookies for subsequent UI page requests.
|
|
378
397
|
// We capture cookies to forward them (Node.js fetch() has no built-in cookie jar).
|
|
@@ -387,7 +406,7 @@ export class SDKClient {
|
|
|
387
406
|
formBody.set('script', script);
|
|
388
407
|
formBody.set('sysparm_ck', csrfToken);
|
|
389
408
|
formBody.set('runscript', 'Run script');
|
|
390
|
-
formBody.set('sys_scope', 'global');
|
|
409
|
+
formBody.set('sys_scope', scope || 'global');
|
|
391
410
|
formBody.set('record_for_rollback', 'on');
|
|
392
411
|
formBody.set('quota_managed_transaction', 'on');
|
|
393
412
|
|