@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "3.0.3",
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
- curl -fsSL https://jsn.jace.pro/install | bash
13
+ npm install -g @jacebenson/jsn
14
14
  Works with Claude Code, OpenCode, Cursor, and agentskills-compatible tools.
15
15
 
16
- Windows users: Download from GitHub releases. If jsn is not in your PATH, the setup
17
- command will automatically detect its location and show the correct command path
18
- (e.g., `.\jsn.exe tables list` instead of `jsn tables list`).
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"
@@ -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
- const output = await app.sdk.executeScript(script);
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 \'...\'',
@@ -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
- const bundled = readBundledSkill();
23
- if (!bundled) return { current: false, error: 'Skill file not found in package' };
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 current = bundled === upstream;
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
- bundled_lines: bundled.split('\n').length,
35
- upstream_lines: upstream.split('\n').length,
36
- error: current ? null : 'Bundled skill is outdated — run "jsn skill install" to update',
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.bundled_lines} lines)` });
98
+ app.ok(result, { summary: `✓ Skill is current (v${result.installed_version})` });
74
99
  } else {
75
- app.ok(result, { summary: `⚠ Skill is outdated (bundled ${result.bundled_lines} lines vs upstream ${result.upstream_lines} lines) — run "jsn skill install" to update` });
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