@jacebenson/jsn 1.0.7 → 2.0.2

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 (45) hide show
  1. package/README.md +196 -243
  2. package/bin/jsn.js +2 -57
  3. package/package.json +35 -32
  4. package/skills/servicenow/SKILL.md +137 -0
  5. package/src/app.js +194 -0
  6. package/src/auth.js +517 -0
  7. package/src/cli.js +174 -0
  8. package/src/commands/_ticket.js +260 -0
  9. package/src/commands/auth.js +227 -0
  10. package/src/commands/catalog.js +325 -0
  11. package/src/commands/changes.js +7 -0
  12. package/src/commands/dev/_generic.js +342 -0
  13. package/src/commands/dev/_simple.js +89 -0
  14. package/src/commands/dev/eval.js +38 -0
  15. package/src/commands/dev/flows.js +573 -0
  16. package/src/commands/dev/forms.js +313 -0
  17. package/src/commands/dev/lists.js +233 -0
  18. package/src/commands/dev/logs.js +64 -0
  19. package/src/commands/dev/rest.js +64 -0
  20. package/src/commands/dev/scopes.js +156 -0
  21. package/src/commands/dev/updatesets.js +184 -0
  22. package/src/commands/dev.js +53 -0
  23. package/src/commands/groupmembers.js +152 -0
  24. package/src/commands/grouproles.js +152 -0
  25. package/src/commands/groups.js +141 -0
  26. package/src/commands/incidents.js +7 -0
  27. package/src/commands/profiles.js +91 -0
  28. package/src/commands/records.js +171 -0
  29. package/src/commands/requests.js +59 -0
  30. package/src/commands/setup.js +39 -0
  31. package/src/commands/skill.js +144 -0
  32. package/src/commands/tasks.js +7 -0
  33. package/src/commands/tickets.js +121 -0
  34. package/src/commands/users.js +144 -0
  35. package/src/commands/version.js +81 -0
  36. package/src/config.js +154 -0
  37. package/src/context.js +62 -0
  38. package/src/errors.js +101 -0
  39. package/src/help.js +85 -0
  40. package/src/helpers.js +182 -0
  41. package/src/output.js +440 -0
  42. package/src/sdk.js +615 -0
  43. package/scripts/install.sh +0 -227
  44. package/scripts/npm-install.js +0 -235
  45. package/scripts/pre-commit-check.sh +0 -61
package/package.json CHANGED
@@ -1,43 +1,46 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "1.0.7",
3
+ "version": "2.0.2",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
- "homepage": "https://github.com/jacebenson/jsn#readme",
6
- "bugs": {
7
- "url": "https://github.com/jacebenson/jsn/issues"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/jacebenson/jsn.git"
12
- },
13
- "license": "MIT",
14
- "author": "Jace Benson",
15
- "type": "commonjs",
16
- "files": [
17
- "bin",
18
- "scripts",
19
- "README.md",
20
- "LICENSE"
21
- ],
5
+ "type": "module",
22
6
  "bin": {
23
7
  "jsn": "bin/jsn.js"
24
8
  },
9
+ "main": "src/cli.js",
25
10
  "scripts": {
26
- "postinstall": "node scripts/npm-install.js"
11
+ "test": "node --test $(find test -name '*.test.js')",
12
+ "lint": "npx eslint src/ bin/ test/",
13
+ "start": "node bin/jsn.js",
14
+ "tag-reminder": "node scripts/tag-reminder.js",
15
+ "release": "node scripts/release.js"
27
16
  },
17
+ "keywords": [
18
+ "servicenow",
19
+ "cli",
20
+ "rest-api",
21
+ "automation"
22
+ ],
23
+ "author": "Jace Benson",
24
+ "license": "MIT",
28
25
  "engines": {
29
- "node": ">=14"
26
+ "node": ">=18.0.0"
27
+ },
28
+ "dependencies": {
29
+ "@inquirer/prompts": "^7.5.1",
30
+ "chalk": "^5.4.1",
31
+ "cli-table3": "^0.6.5",
32
+ "yargs": "^17.7.2"
30
33
  },
31
- "os": [
32
- "darwin",
33
- "linux",
34
- "win32"
35
- ],
36
- "cpu": [
37
- "x64",
38
- "arm64"
39
- ],
40
34
  "devDependencies": {
41
- "@servicenow/sdk": "^4.7.0"
42
- }
43
- }
35
+ "@eslint/js": "^10.0.1",
36
+ "@inquirer/search": "^4.2.0",
37
+ "globals": "^17.6.0"
38
+ },
39
+ "files": [
40
+ "bin/jsn.js",
41
+ "src/",
42
+ "skills/",
43
+ "README.md",
44
+ "LICENSE"
45
+ ]
46
+ }
@@ -0,0 +1,137 @@
1
+ ---
2
+ name: servicenow
3
+ description: |
4
+ Interact with ServiceNow instances via the jsn CLI. Use when working with ServiceNow
5
+ development, administration, or data exploration. Handles tables, records, business rules,
6
+ flows, script includes, ACLs, update sets, and more. Triggered by ServiceNow URLs
7
+ (service-now.com, servicenow.com) or when the user mentions ServiceNow, jsn, servicenow,
8
+ or related terms like tables, records, business rules, flows, script includes, ACLs,
9
+ update sets, or encoded queries.
10
+ license: MIT
11
+ compatibility: |
12
+ Requires jsn CLI (github.com/jacebenson/jsn). Install via:
13
+ curl -fsSL https://jsn.jace.pro/install | bash
14
+ Works with Claude Code, OpenCode, Cursor, and agentskills-compatible tools.
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`).
19
+ metadata:
20
+ author: jacebenson
21
+ version: "1.0.0"
22
+ repository: https://github.com/jacebenson/jsn
23
+ ---
24
+
25
+ # Jace's ServiceNow CLI
26
+
27
+ Explore and manage ServiceNow instances. Works standalone or with AI agents.
28
+
29
+ ## Discovery
30
+
31
+ Commands are self-documenting. Use these to learn what's available:
32
+
33
+ ```bash
34
+ jsn --help # Root help with all top-level commands
35
+ jsn <command> --help # Detailed usage, flags, and examples for any command
36
+ jsn dev # Shows categorized dev subcommands
37
+ ```
38
+
39
+ ## Agent Rules
40
+
41
+ 1. **Output modes** — `--json` when parsing data; `--md` when presenting to humans; `--agent` for automation
42
+ 2. **Use sys_id for updates** — All update/delete operations require sys_id
43
+ 3. **Check auth first** — Run `jsn auth status` before operations
44
+ 4. **NEVER logout** — Only run `jsn auth logout` if the user explicitly asks
45
+ 5. **Use `--profile <name>`** to target a specific instance, or `jsn profiles use <name>` to change default
46
+ 6. **Before using `eval` or `rest`** — Ask yourself: *"Have I checked if there's a more specific jsn command?"* Verify `jsn records --table <name> create` won't work first. Prefer specific over generic over escape hatch over eval.
47
+ 7. **CONFIRM before destructive operations** — Always show the user exactly what will be created, updated, or deleted and ask for explicit confirmation before executing. Never run `create`, `update`, `delete`, `set`, or `remove` commands without user approval.
48
+
49
+ ## ⚠️ Destructive Operations Require Confirmation
50
+
51
+ **jsn does not have a `--confirm` or `--dry-run` flag.** It assumes you mean what you type. This means **YOU** (the agent) are the safety layer.
52
+
53
+ **Before any write operation, you MUST:**
54
+
55
+ 1. **Show the user what you found** — Run a `list` or `show` command first
56
+ 2. **Present the exact command you plan to run** — Include the sys_id, table name, and data
57
+ 3. **Wait for explicit approval** — "Proceed? [y/N]" or similar
58
+
59
+ **Examples of destructive commands that require confirmation:**
60
+ - `jsn records --table <name> create`
61
+ - `jsn records --table <name> update <sys_id>`
62
+ - `jsn records --table <name> delete <sys_id>`
63
+ - `jsn dev updatesets set <name>`
64
+ - `jsn profiles remove <name>`
65
+ - Any `delete`, `update`, `create`, `set`, or `remove` subcommand
66
+
67
+ **Good pattern:**
68
+ ```
69
+ User: "Delete that incident"
70
+ Agent: "Found: INC0010001 'Server down' (sys_id: abc123...)
71
+ Command: jsn records --table incident delete abc123...
72
+ Proceed? [y/N]"
73
+ User: "y"
74
+ Agent: [runs command]
75
+ ```
76
+
77
+ **Bad pattern:**
78
+ ```
79
+ User: "Delete that incident"
80
+ Agent: [immediately runs jsn records --table incident delete ...]
81
+ ```
82
+
83
+ ## ⚠️ AVOID `jsn eval` for Record Creation
84
+
85
+ **Why:** `jsn eval` runs server-side JavaScript in the global scope. It returns HTTP 200 even when:
86
+ - The insert fails due to ACL violations
87
+ - Mandatory fields are missing
88
+ - The record lands in the wrong scope
89
+ - Logic errors prevent the insert entirely
90
+
91
+ **The AI trap:** Agents see "success" and assume the record was created. It wasn't.
92
+
93
+ **Always use instead:** `jsn records --table <name> create` — it returns the created record with sys_id or an explicit validation error.
94
+
95
+ **Only use `eval` when:** No specific command exists, `records` lacks required fields, and `rest` doesn't work. This is rare.
96
+
97
+ ## Command Hierarchy
98
+
99
+ Pick the most specific tool for the job. **Never default to eval** — it's the last resort:
100
+
101
+ 1. **Specific commands** — `jsn flows`, `jsn rules`, `jsn jobs`, etc. — curated views with domain-aware formatting and validation
102
+ 2. **`jsn records --table <name>`** — generic CRUD on any table (the workhorse, preferred for record creation)
103
+ 3. **`jsn rest`** — raw Table API escape hatch when records command lacks required fields
104
+ 4. **`jsn eval`** — ⚠️ **LAST RESORT ONLY** — server-side script execution when no other option exists
105
+ 5. **Ask the human** — if none of the above work. Never generate standalone GlideRecord scripts as a fallback.
106
+
107
+ ## JSON Envelope
108
+
109
+ All commands support `--json`. The envelope structure:
110
+
111
+ ```json
112
+ {
113
+ "ok": true,
114
+ "data": [ ... ],
115
+ "summary": "5 records",
116
+ "breadcrumbs": [
117
+ {"action": "show", "cmd": "jsn records --table incident <sys_id>", "description": "View details"}
118
+ ]
119
+ }
120
+ ```
121
+
122
+ Breadcrumbs suggest next commands — follow them for navigation.
123
+
124
+ ## Configuration
125
+
126
+ ```
127
+ ~/.config/servicenow/
128
+ ├── config.json # Profiles and settings
129
+ └── credentials.json # Auth tokens (fallback)
130
+
131
+ .servicenow/ # Per-repo config (optional)
132
+ └── config.json # Project-specific settings
133
+ ```
134
+
135
+ **Environment variables:**
136
+ - `SERVICENOW_TOKEN` — Override stored token
137
+ - `SERVICENOW_INSTANCE` — Override instance URL
package/src/app.js ADDED
@@ -0,0 +1,194 @@
1
+ // App context: bundles config, auth, SDK, output, and runtime context
2
+
3
+ import { AuthManager } from './auth.js';
4
+ import { SDKClient } from './sdk.js';
5
+ import { OutputWriter, FormatAuto, FormatJSON, FormatMarkdown, FormatQuiet, FormatStyled } from './output.js';
6
+ import { getEffectiveInstance } from './config.js';
7
+ import { extractProfileName } from './helpers.js';
8
+ import { getCurrentUser, getCurrentApplication, getCurrentUpdateSet } from './context.js';
9
+ import { errUsage, errAuth } from './errors.js';
10
+ import process from 'node:process';
11
+
12
+
13
+ export class App {
14
+ constructor(cfg) {
15
+ this.config = cfg;
16
+ this.auth = new AuthManager(this);
17
+ this.output = new OutputWriter({ format: resolveFormat(cfg.format) });
18
+ this.sdk = null;
19
+
20
+ const instance = getEffectiveInstance(cfg);
21
+ if (instance) {
22
+ this.sdk = new SDKClient(instance, this.auth);
23
+ }
24
+
25
+ this.context = {
26
+ profileName: '',
27
+ username: '',
28
+ scope: '',
29
+ updateSet: '',
30
+ };
31
+
32
+ this.loadContext();
33
+ }
34
+
35
+ loadContext() {
36
+ const instance = getEffectiveInstance(this.config);
37
+ if (!instance) return;
38
+ this.context.profileName = extractProfileName(instance);
39
+ for (const [name, profile] of Object.entries(this.config.profiles || {})) {
40
+ if (profile.instance_url === instance) {
41
+ this.context.profileName = name;
42
+ this.context.username = profile.username || '';
43
+ break;
44
+ }
45
+ }
46
+ }
47
+
48
+ getEffectiveInstance() {
49
+ return getEffectiveInstance(this.config);
50
+ }
51
+
52
+ async printContextHeader() {
53
+ if (!this.getEffectiveInstance() || !this.sdk) return;
54
+ if (process.env.JSN_NO_HEADER) return;
55
+ if (this.output.getFormat() === FormatJSON || this.output.getFormat() === FormatQuiet) return;
56
+
57
+ let userDisplayName = 'Unknown';
58
+ let userSysID = '';
59
+
60
+ try {
61
+ const user = await getCurrentUser(this.sdk);
62
+ if (user) {
63
+ userDisplayName = user.name || user.user_name;
64
+ userSysID = user.sys_id;
65
+ this.context.username = userDisplayName;
66
+ }
67
+ } catch {
68
+ // ignore
69
+ }
70
+
71
+ let displayUserName = userDisplayName;
72
+ if (displayUserName.length > 10) {
73
+ displayUserName = displayUserName.slice(0, 6) + '...';
74
+ }
75
+
76
+ let scope = 'global';
77
+ if (userSysID) {
78
+ try {
79
+ const app = await getCurrentApplication(this.sdk, userSysID);
80
+ if (app && app.scope) scope = app.scope;
81
+ } catch {
82
+ // ignore
83
+ }
84
+ }
85
+ this.context.scope = scope;
86
+
87
+ let updateSet = 'Default';
88
+ let updateSetSysID = '';
89
+ if (userSysID) {
90
+ try {
91
+ const us = await getCurrentUpdateSet(this.sdk, userSysID);
92
+ if (us && us.name && us.name !== '-') {
93
+ updateSet = us.name;
94
+ updateSetSysID = us.sys_id;
95
+ }
96
+ } catch {
97
+ // ignore
98
+ }
99
+ }
100
+ this.context.updateSet = updateSet;
101
+
102
+ const instance = this.getEffectiveInstance();
103
+ const instanceLink = instance;
104
+ const userLink = `${instance}/sys_user_list.do?sysparm_query=sys_id=${userSysID}`;
105
+ const scopeLink = `${instance}/sys_scope.do?sysparm_query=scope=${scope}`;
106
+ const updateSetLink = updateSetSysID
107
+ ? `${instance}/sys_update_set.do?sys_id=${updateSetSysID}`
108
+ : `${instance}/sys_update_set_list.do`;
109
+
110
+ const scopeFormatted = `[${scope}]`;
111
+
112
+ process.stderr.write('# Use `jsn dev scopes` to change scope, `jsn dev updatesets set` to change updateset\n');
113
+ process.stderr.write('PROFILE USER [SCOPE] UPDATE SET\n');
114
+
115
+ const profileStr = `]8;;${instanceLink}\x07${String(this.context.profileName).padEnd(9)}]8;;\x07`;
116
+ const userStr = `]8;;${userLink}\x07${String(displayUserName).padEnd(9)}]8;;\x07`;
117
+ const scopeStr = `]8;;${scopeLink}\x07${String(scopeFormatted).padEnd(17)}]8;;\x07`;
118
+ const updateSetStr = `]8;;${updateSetLink}\x07${updateSet}]8;;\x07`;
119
+
120
+ process.stderr.write(`${profileStr} ${userStr} ${scopeStr} ${updateSetStr}\n\n`);
121
+
122
+ // ⚠️ Warning if in the Default update set
123
+ if (updateSet && updateSet.toLowerCase().includes('default')) {
124
+ const isGlobal = scope === 'global';
125
+ if (isGlobal) {
126
+ // 🔴 HARD WARNING — Global + Default
127
+ process.stderr.write(
128
+ '\x1b[31m' + // red
129
+ '┏━ GLOBAL + DEFAULT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n' +
130
+ '┃ You are in Global scope with the Default update set. ┃\n' +
131
+ '┃ Changes ARE captured — in the Default update set. ┃\n' +
132
+ '┃ Moving them to a named update set later requires ┃\n' +
133
+ '┃ manual surgery, one change at a time. Avoid the mess. ┃\n' +
134
+ '┃ ┃\n' +
135
+ '┃ Create a named update set now: ┃\n' +
136
+ '┃ jsn dev updatesets create --name "My Feature" ┃\n' +
137
+ '┃ ┃\n' +
138
+ '┃ Or switch to a scoped scope first: ┃\n' +
139
+ '┃ jsn dev scopes list ┃\n' +
140
+ '┃ ┃\n' +
141
+ '┃ (Run jsn updatesets yolo to silence this check) ┃\n' +
142
+ '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\x1b[0m\n'
143
+ );
144
+ } else {
145
+ // 🟡 SOFT WARNING — Scope + Default
146
+ process.stderr.write(
147
+ '\x1b[33m' + // yellow
148
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
149
+ ' ⚠ Default update set in scope [' + scope + ']\n' +
150
+ ' Changes are contained to this scope, but a named\n' +
151
+ ' update set is still recommended for tracking.\n' +
152
+ ' (Run \x1b[1mjsn updatesets yolo\x1b[22m to silence this warning)\n' +
153
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
154
+ '\x1b[0m' // reset
155
+ );
156
+ }
157
+ }
158
+ }
159
+
160
+ ok(data, opts = {}) {
161
+ this.output.ok(data, opts);
162
+ }
163
+
164
+ err(error) {
165
+ this.output.err(error);
166
+ }
167
+
168
+ isInteractive() {
169
+ return process.stdout.isTTY === true;
170
+ }
171
+
172
+ requireInstance() {
173
+ if (!this.getEffectiveInstance()) {
174
+ throw errUsage('Instance URL required. Set via --instance flag, SERVICENOW_INSTANCE_URL env, or config file.');
175
+ }
176
+ }
177
+
178
+ requireAuth() {
179
+ if (!this.auth.isAuthenticated()) {
180
+ throw errAuth('Not authenticated');
181
+ }
182
+ }
183
+ }
184
+
185
+ function resolveFormat(fmt) {
186
+ switch (fmt) {
187
+ case 'json': return FormatJSON;
188
+ case 'markdown':
189
+ case 'md': return FormatMarkdown;
190
+ case 'quiet': return FormatQuiet;
191
+ case 'styled': return FormatStyled;
192
+ default: return FormatAuto;
193
+ }
194
+ }