nowaikit-utils 1.1.0

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.
@@ -0,0 +1,398 @@
1
+ /**
2
+ * NowAIKit Utils — Background Service Worker
3
+ *
4
+ * Handles context menus, badge updates, cookie proxy, and AI streaming.
5
+ */
6
+
7
+ // ─── Helpers ───────────────────────────────────────────────────────────────
8
+
9
+ /** Validate a URL belongs to a ServiceNow domain */
10
+ function isServiceNowUrl(url) {
11
+ try {
12
+ const parsed = new URL(url);
13
+ return parsed.hostname.endsWith('.service-now.com') || parsed.hostname.endsWith('.servicenow.com');
14
+ } catch (e) {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ // ─── Context Menus ──────────────────────────────────────────────────────────
20
+
21
+ chrome.runtime.onInstalled.addListener(() => {
22
+ chrome.contextMenus.create({
23
+ id: 'nowaikit-copy-sysid',
24
+ title: 'Copy sys_id',
25
+ contexts: ['page'],
26
+ documentUrlPatterns: ['https://*.service-now.com/*', 'https://*.servicenow.com/*'],
27
+ });
28
+
29
+ chrome.contextMenus.create({
30
+ id: 'nowaikit-copy-record-url',
31
+ title: 'Copy Record URL',
32
+ contexts: ['page'],
33
+ documentUrlPatterns: ['https://*.service-now.com/*', 'https://*.servicenow.com/*'],
34
+ });
35
+
36
+ chrome.contextMenus.create({
37
+ id: 'nowaikit-copy-table-name',
38
+ title: 'Copy Table Name',
39
+ contexts: ['page'],
40
+ documentUrlPatterns: ['https://*.service-now.com/*', 'https://*.servicenow.com/*'],
41
+ });
42
+
43
+ chrome.contextMenus.create({
44
+ id: 'nowaikit-open-list',
45
+ title: 'Open Table List View',
46
+ contexts: ['page'],
47
+ documentUrlPatterns: ['https://*.service-now.com/*', 'https://*.servicenow.com/*'],
48
+ });
49
+
50
+ chrome.contextMenus.create({
51
+ id: 'nowaikit-open-schema',
52
+ title: 'View Table Schema',
53
+ contexts: ['page'],
54
+ documentUrlPatterns: ['https://*.service-now.com/*', 'https://*.servicenow.com/*'],
55
+ });
56
+
57
+ chrome.contextMenus.create({
58
+ id: 'nowaikit-separator',
59
+ type: 'separator',
60
+ contexts: ['page'],
61
+ documentUrlPatterns: ['https://*.service-now.com/*', 'https://*.servicenow.com/*'],
62
+ });
63
+
64
+ chrome.contextMenus.create({
65
+ id: 'nowaikit-xml-view',
66
+ title: 'View as XML',
67
+ contexts: ['page'],
68
+ documentUrlPatterns: ['https://*.service-now.com/*', 'https://*.servicenow.com/*'],
69
+ });
70
+
71
+ chrome.contextMenus.create({
72
+ id: 'nowaikit-json-view',
73
+ title: 'View as JSON (REST)',
74
+ contexts: ['page'],
75
+ documentUrlPatterns: ['https://*.service-now.com/*', 'https://*.servicenow.com/*'],
76
+ });
77
+ });
78
+
79
+ chrome.contextMenus.onClicked.addListener((info, tab) => {
80
+ if (!tab?.id) return;
81
+
82
+ chrome.tabs.sendMessage(tab.id, {
83
+ action: info.menuItemId,
84
+ });
85
+ });
86
+
87
+ // ─── Badge Updates ──────────────────────────────────────────────────────────
88
+
89
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
90
+ if (changeInfo.status !== 'complete') return;
91
+ if (!tab.url) return;
92
+
93
+ if (isServiceNowUrl(tab.url)) {
94
+ chrome.action.setBadgeText({ text: 'ON', tabId });
95
+ chrome.action.setBadgeBackgroundColor({ color: '#00D4AA', tabId });
96
+ } else {
97
+ chrome.action.setBadgeText({ text: '', tabId });
98
+ }
99
+ });
100
+
101
+ // ─── Message Handler ────────────────────────────────────────────────────────
102
+
103
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
104
+ // Security: only accept messages from our own extension
105
+ if (sender.id !== chrome.runtime.id) return;
106
+
107
+ if (message.action === 'getSettings') {
108
+ chrome.storage.sync.get({
109
+ showTechnicalNames: false,
110
+ showUpdateSetBanner: false,
111
+ showFieldTypes: false,
112
+ enableNodeSwitcher: false,
113
+ enableQuickNav: false,
114
+ enableScriptHighlight: false,
115
+ enableFieldCopy: false,
116
+ enableAISidebar: false,
117
+ darkOverlay: false,
118
+ }, (settings) => {
119
+ sendResponse(settings);
120
+ });
121
+ return true;
122
+ }
123
+
124
+ if (message.action === 'openUrl') {
125
+ // Validate URL is a ServiceNow domain
126
+ if (isServiceNowUrl(message.url)) {
127
+ chrome.tabs.create({ url: message.url });
128
+ }
129
+ }
130
+
131
+ // ─── Cookie Handlers (for Node Switcher) ───────────────────────────────
132
+ // Content scripts can't set httpOnly cookies — must go through background
133
+
134
+ if (message.action === 'setCookie') {
135
+ // Security: only allow cookie ops on ServiceNow domains
136
+ if (!isServiceNowUrl(message.url)) {
137
+ sendResponse({ success: false, error: 'Invalid domain' });
138
+ return true;
139
+ }
140
+ const opts = {
141
+ url: message.url,
142
+ name: message.name,
143
+ value: message.value,
144
+ path: message.path || '/',
145
+ };
146
+ if (message.domain) opts.domain = message.domain;
147
+ if (message.httpOnly !== undefined) opts.httpOnly = message.httpOnly;
148
+ if (message.secure !== undefined) opts.secure = message.secure;
149
+ if (message.sameSite) opts.sameSite = message.sameSite;
150
+ if (message.expirationDate) {
151
+ opts.expirationDate = message.expirationDate;
152
+ } else {
153
+ opts.expirationDate = Math.floor(Date.now() / 1000) + 86400 * 365;
154
+ }
155
+ chrome.cookies.set(opts, (cookie) => {
156
+ sendResponse({ success: !!cookie, cookie: cookie });
157
+ });
158
+ return true;
159
+ }
160
+
161
+ if (message.action === 'getCookie') {
162
+ if (!isServiceNowUrl(message.url)) {
163
+ sendResponse({ cookie: null });
164
+ return true;
165
+ }
166
+ chrome.cookies.get({ url: message.url, name: message.name }, (cookie) => {
167
+ sendResponse({ cookie: cookie });
168
+ });
169
+ return true;
170
+ }
171
+
172
+ if (message.action === 'getAllCookies') {
173
+ if (!isServiceNowUrl(message.url)) {
174
+ sendResponse({ cookies: [] });
175
+ return true;
176
+ }
177
+ chrome.cookies.getAll({ url: message.url }, (cookies) => {
178
+ sendResponse({ cookies: cookies || [] });
179
+ });
180
+ return true;
181
+ }
182
+
183
+ if (message.action === 'removeCookie') {
184
+ if (!isServiceNowUrl(message.url)) {
185
+ sendResponse({ success: false });
186
+ return true;
187
+ }
188
+ chrome.cookies.remove({ url: message.url, name: message.name }, (details) => {
189
+ sendResponse({ success: !!details });
190
+ });
191
+ return true;
192
+ }
193
+ });
194
+
195
+ // ─── AI Chat: LLM API Proxy with Streaming ─────────────────────────────────
196
+
197
+ chrome.runtime.onConnect.addListener((port) => {
198
+ if (port.name !== 'nowaikit-ai-stream') return;
199
+
200
+ let aborted = false;
201
+ let activeReader = null;
202
+
203
+ // Handle port disconnect — abort streaming
204
+ port.onDisconnect.addListener(() => {
205
+ aborted = true;
206
+ if (activeReader) {
207
+ try { activeReader.cancel(); } catch (e) { /* ignore */ }
208
+ }
209
+ });
210
+
211
+ port.onMessage.addListener(async (msg) => {
212
+ if (msg.action !== 'nowaikit-ai-chat') return;
213
+
214
+ const { provider, apiKey, model, messages, ollamaUrl } = msg;
215
+
216
+ try {
217
+ let url, headers, body;
218
+
219
+ switch (provider) {
220
+ case 'anthropic':
221
+ url = 'https://api.anthropic.com/v1/messages';
222
+ headers = {
223
+ 'Content-Type': 'application/json',
224
+ 'x-api-key': apiKey,
225
+ 'anthropic-version': '2023-06-01',
226
+ 'anthropic-dangerous-direct-browser-access': 'true',
227
+ };
228
+ const systemMsg = messages.find(m => m.role === 'system');
229
+ const nonSystemMsgs = messages.filter(m => m.role !== 'system');
230
+ body = JSON.stringify({
231
+ model: model,
232
+ max_tokens: 4096,
233
+ system: systemMsg ? systemMsg.content : '',
234
+ messages: nonSystemMsgs,
235
+ stream: true,
236
+ });
237
+ break;
238
+
239
+ case 'google':
240
+ url = 'https://generativelanguage.googleapis.com/v1beta/models/' + encodeURIComponent(model) + ':streamGenerateContent?alt=sse';
241
+ headers = {
242
+ 'Content-Type': 'application/json',
243
+ 'x-goog-api-key': apiKey,
244
+ };
245
+ const googleContents = messages
246
+ .filter(m => m.role !== 'system')
247
+ .map(m => ({
248
+ role: m.role === 'assistant' ? 'model' : 'user',
249
+ parts: [{ text: m.content }],
250
+ }));
251
+ const googleSystem = messages.find(m => m.role === 'system');
252
+ body = JSON.stringify({
253
+ contents: googleContents,
254
+ systemInstruction: googleSystem ? { parts: [{ text: googleSystem.content }] } : undefined,
255
+ });
256
+ break;
257
+
258
+ case 'ollama':
259
+ url = (ollamaUrl || 'http://localhost:11434') + '/api/chat';
260
+ headers = { 'Content-Type': 'application/json' };
261
+ body = JSON.stringify({
262
+ model: model,
263
+ messages: messages,
264
+ stream: true,
265
+ });
266
+ break;
267
+
268
+ case 'openrouter':
269
+ url = 'https://openrouter.ai/api/v1/chat/completions';
270
+ headers = {
271
+ 'Content-Type': 'application/json',
272
+ 'Authorization': 'Bearer ' + apiKey,
273
+ 'HTTP-Referer': 'https://nowaikit.com',
274
+ 'X-Title': 'NowAIKit Utils',
275
+ };
276
+ body = JSON.stringify({
277
+ model: model,
278
+ messages: messages,
279
+ stream: true,
280
+ });
281
+ break;
282
+
283
+ case 'openai':
284
+ default:
285
+ url = 'https://api.openai.com/v1/chat/completions';
286
+ headers = {
287
+ 'Content-Type': 'application/json',
288
+ 'Authorization': 'Bearer ' + apiKey,
289
+ };
290
+ body = JSON.stringify({
291
+ model: model,
292
+ messages: messages,
293
+ stream: true,
294
+ });
295
+ break;
296
+ }
297
+
298
+ const response = await fetch(url, {
299
+ method: 'POST',
300
+ headers: headers,
301
+ body: body,
302
+ });
303
+
304
+ if (!response.ok) {
305
+ const errText = await response.text();
306
+ let errMsg = 'API error (' + response.status + ')';
307
+ try {
308
+ const errJson = JSON.parse(errText);
309
+ errMsg = errJson.error?.message || errJson.message || errMsg;
310
+ } catch (e) {
311
+ errMsg = errText.substring(0, 200) || errMsg;
312
+ }
313
+ if (!aborted) {
314
+ try { port.postMessage({ type: 'error', content: errMsg }); } catch (e) { /* port closed */ }
315
+ }
316
+ return;
317
+ }
318
+
319
+ // Stream the response
320
+ const reader = response.body.getReader();
321
+ activeReader = reader;
322
+ const decoder = new TextDecoder();
323
+ let buffer = '';
324
+
325
+ while (!aborted) {
326
+ const { done, value } = await reader.read();
327
+ if (done) break;
328
+
329
+ buffer += decoder.decode(value, { stream: true });
330
+ const lines = buffer.split('\n');
331
+ buffer = lines.pop() || '';
332
+
333
+ for (const line of lines) {
334
+ if (aborted) break;
335
+ const trimmed = line.trim();
336
+ if (!trimmed || trimmed === 'data: [DONE]') continue;
337
+
338
+ try {
339
+ let json;
340
+ let token = '';
341
+
342
+ if (provider === 'ollama') {
343
+ json = JSON.parse(trimmed);
344
+ if (json.message?.content) {
345
+ token = json.message.content;
346
+ }
347
+ } else {
348
+ if (!trimmed.startsWith('data: ')) continue;
349
+ json = JSON.parse(trimmed.slice(6));
350
+
351
+ if (provider === 'anthropic') {
352
+ if (json.type === 'content_block_delta' && json.delta?.text) {
353
+ token = json.delta.text;
354
+ }
355
+ } else if (provider === 'google') {
356
+ if (json.candidates?.[0]?.content?.parts?.[0]?.text) {
357
+ token = json.candidates[0].content.parts[0].text;
358
+ }
359
+ } else {
360
+ if (json.choices?.[0]?.delta?.content) {
361
+ token = json.choices[0].delta.content;
362
+ }
363
+ }
364
+ }
365
+
366
+ if (token && !aborted) {
367
+ try { port.postMessage({ type: 'token', content: token }); } catch (e) { aborted = true; break; }
368
+ }
369
+ } catch (e) {
370
+ // Skip malformed JSON lines
371
+ }
372
+ }
373
+ }
374
+
375
+ // Handle remaining buffer for Ollama
376
+ if (!aborted && provider === 'ollama' && buffer.trim()) {
377
+ try {
378
+ const json = JSON.parse(buffer.trim());
379
+ if (json.message?.content) {
380
+ try { port.postMessage({ type: 'token', content: json.message.content }); } catch (e) { /* port closed */ }
381
+ }
382
+ } catch (e) {
383
+ // Ignore
384
+ }
385
+ }
386
+
387
+ activeReader = null;
388
+ if (!aborted) {
389
+ try { port.postMessage({ type: 'done' }); } catch (e) { /* port closed */ }
390
+ }
391
+
392
+ } catch (err) {
393
+ if (!aborted) {
394
+ try { port.postMessage({ type: 'error', content: err.message || 'Failed to connect to AI provider' }); } catch (e) { /* port closed */ }
395
+ }
396
+ }
397
+ });
398
+ });
package/cli.mjs ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, resolve } from 'path';
5
+ import { readFileSync } from 'fs';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf8'));
9
+
10
+ const TEAL = '\x1b[36m';
11
+ const BOLD = '\x1b[1m';
12
+ const DIM = '\x1b[2m';
13
+ const RESET = '\x1b[0m';
14
+ const GREEN = '\x1b[32m';
15
+ const YELLOW = '\x1b[33m';
16
+
17
+ console.log(`
18
+ ${TEAL}${BOLD} _ _ _ ___ _ ___ _
19
+ | \\ | | _____ __/ \\ |_ _| |/ (_) |_
20
+ | \\| |/ _ \\ \\ /\\ / / _ \\ | || ' /| | __|
21
+ | |\\ | (_) \\ V V / ___ \\ | || . \\| | |_
22
+ |_| \\_|\\___/ \\_/\\_/_/ \\_\\___|_|\\_\\_|\\__|
23
+ ${DIM}Utils${RESET}
24
+ ${TEAL}${BOLD} The AI-Powered ServiceNow Browser Extension${RESET}
25
+ ${DIM} v${pkg.version}${RESET}
26
+ `);
27
+
28
+ console.log(`${BOLD}Features:${RESET}
29
+ ${GREEN}+${RESET} AI Assistant with 5 providers (OpenAI, Anthropic, Google, OpenRouter, Ollama)
30
+ ${GREEN}+${RESET} 22 code templates (GlideRecord, GlideQuery, Business Rules, REST, etc.)
31
+ ${GREEN}+${RESET} 12 slash commands for instant navigation
32
+ ${GREEN}+${RESET} 10 keyboard shortcuts for zero-mouse workflow
33
+ ${GREEN}+${RESET} Node switcher with cluster-aware discovery
34
+ ${GREEN}+${RESET} Two-way script sync with VS Code (NowAIKit Builder)
35
+ ${GREEN}+${RESET} Multi-instance management with color-coded environments
36
+ ${GREEN}+${RESET} Technical name resolver, field copy, syntax highlighting
37
+ ${GREEN}+${RESET} AES-256-GCM encrypted API key storage
38
+ ${GREEN}+${RESET} Zero telemetry, domain-scoped, CSP enforced
39
+ `);
40
+
41
+ console.log(`${BOLD}Installation:${RESET}
42
+
43
+ ${YELLOW}Option 1: Chrome Web Store${RESET}
44
+ Visit: https://chrome.google.com/webstore/detail/nowaikit-utils
45
+
46
+ ${YELLOW}Option 2: Load Unpacked (Developer Mode)${RESET}
47
+ 1. Extension files are installed at:
48
+ ${TEAL}${resolve(__dirname)}${RESET}
49
+ 2. Open Chrome and navigate to: ${TEAL}chrome://extensions${RESET}
50
+ 3. Enable ${BOLD}Developer mode${RESET} (toggle in top-right)
51
+ 4. Click ${BOLD}Load unpacked${RESET}
52
+ 5. Select the directory above
53
+ `);
54
+
55
+ console.log(`${BOLD}Part of the NowAIKit Ecosystem:${RESET}
56
+ ${DIM}NowAIKit MCP${RESET} — 400+ tools, connect any AI to ServiceNow
57
+ ${DIM}NowAIKit Builder${RESET} — VS Code extension with 10 Copilot agents
58
+ ${TEAL}NowAIKit Utils${RESET} — Chrome extension (you are here)
59
+ ${DIM}NowAIKit Apex${RESET} — CLI with 26 scan/review/build capabilities
60
+ `);
61
+
62
+ console.log(`${DIM}GitHub: https://github.com/aartiq/nowaikit-utils-browser${RESET}`);
63
+ console.log(`${DIM}Website: https://nowaikit.com/products/utils${RESET}`);
64
+ console.log(`${DIM}npm: https://www.npmjs.com/package/nowaikit-utils${RESET}`);
65
+ console.log();