@xano/xanoscript-language-server 11.0.5 → 11.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.
package/CLAUDE.md CHANGED
@@ -30,12 +30,14 @@ This is a Language Server Protocol (LSP) implementation for XanoScript, built on
30
30
  ### Core Components
31
31
 
32
32
  **Lexer (`lexer/`)**: Token definitions and lexical analysis
33
+
33
34
  - `tokens.js` - Main token registry
34
35
  - `lexer.js` - Chevrotain lexer implementation
35
36
  - Domain tokens: `api.js`, `db.js`, `cloud.js`, `function.js`, etc.
36
37
  - `utils.js` - Token creation utilities (`createToken`, `createTokenByName`)
37
38
 
38
39
  **Parser (`parser/`)**: Grammar rules and parsing logic
40
+
39
41
  - `base_parser.js` - Core XanoBaseParser extending Chevrotain
40
42
  - Main parsers: `query_parser.js`, `function_parser.js`, `task_parser.js`, `api_group_parser.js`, `table_parser.js`, `workflow_test_parser.js`, `table_trigger_parser.js`
41
43
  - `attributes/` - Field attributes (description, disabled, sensitive)
@@ -45,6 +47,7 @@ This is a Language Server Protocol (LSP) implementation for XanoScript, built on
45
47
  - `generic/` - Reusable parsing components
46
48
 
47
49
  **Language Server (`server.js` + feature directories)**:
50
+
48
51
  - `onCompletion/` - Auto-completion logic
49
52
  - `onDidChangeContent/` - Live diagnostics and error reporting
50
53
  - `onHover/` - Documentation and hover information
@@ -53,6 +56,7 @@ This is a Language Server Protocol (LSP) implementation for XanoScript, built on
53
56
  ### XanoScript Object Types
54
57
 
55
58
  Primary constructs parsed by dedicated parsers:
59
+
56
60
  - **query** - API endpoints with HTTP verbs, input validation, processing logic, responses
57
61
  - **function** - Reusable logic blocks with testing capabilities
58
62
  - **task** - Scheduled operations with cron-like triggers
@@ -63,18 +67,147 @@ Primary constructs parsed by dedicated parsers:
63
67
 
64
68
  ## Adding New Features
65
69
 
66
- ### New Object Type
67
- 1. Create a new parser in `parser/` (e.g., `my_object_parser.js`)
68
- 2. Follow existing parser patterns (see `query_parser.js`, `function_parser.js`)
69
- 3. Register it in `server.js`
70
+ ### New Top-Level Parser (e.g., `run.job`, `run.service`)
71
+
72
+ Follow this step-by-step process:
73
+
74
+ **1. Create lexer tokens (`lexer/my_feature.js`)**
75
+
76
+ ```javascript
77
+ import { Identifier } from "./identifier.js";
78
+ import { createTokenByName } from "./utils.js";
79
+
80
+ export const MyToken = createTokenByName("my_keyword", {
81
+ longer_alt: Identifier,
82
+ categories: [Identifier],
83
+ });
84
+
85
+ export const MyFeatureTokens = [MyToken];
86
+
87
+ export function mapTokenToType(token) {
88
+ switch (token) {
89
+ case MyToken.name:
90
+ return "keyword";
91
+ default:
92
+ return null;
93
+ }
94
+ }
95
+ ```
96
+
97
+ **2. Register tokens in `lexer/tokens.js`**
98
+
99
+ - Import tokens and mapper at the top
100
+ - Add `...MyFeatureTokens` to `allTokens` array
101
+ - Add `mapMyFeatureTokenToType` to `tokenMappers` array
102
+
103
+ **3. Create parser (`parser/my_feature_parser.js`)**
104
+
105
+ ```javascript
106
+ import { StringLiteral } from "../lexer/literal.js";
107
+ import { MyToken } from "../lexer/my_feature.js";
108
+ import { Identifier, NewlineToken } from "../lexer/tokens.js";
109
+
110
+ export function myFeatureDeclaration($) {
111
+ return () => {
112
+ $.sectionStack.push("myFeatureDeclaration");
113
+ $.SUBRULE($.optionalCommentBlockFn);
114
+
115
+ const parent = $.CONSUME(MyToken);
116
+ $.OR([
117
+ { ALT: () => $.CONSUME(StringLiteral) },
118
+ { ALT: () => $.CONSUME(Identifier) },
119
+ ]);
120
+
121
+ // Use schemaParseAttributeFn for body with declarative schema
122
+ $.SUBRULE($.schemaParseAttributeFn, {
123
+ ARGS: [
124
+ parent,
125
+ {
126
+ required_attr: "[string]",
127
+ "optional_attr?": "[boolean]",
128
+ "nested?": {
129
+ name: "[string]",
130
+ "value?": { "[string]": "[constant]" },
131
+ },
132
+ "array_of_strings?": ["[string]"],
133
+ },
134
+ ],
135
+ });
136
+
137
+ $.MANY2(() => $.CONSUME2(NewlineToken));
138
+ $.sectionStack.pop();
139
+ };
140
+ }
141
+ ```
142
+
143
+ **4. Register parser in `parser/register.js`**
144
+
145
+ ```javascript
146
+ import { myFeatureDeclaration } from "./my_feature_parser.js";
147
+ // In register function:
148
+ $.myFeatureDeclaration = $.RULE(
149
+ "myFeatureDeclaration",
150
+ myFeatureDeclaration($)
151
+ );
152
+ ```
153
+
154
+ **5. Add scheme detection in `utils.js`**
155
+
156
+ ```javascript
157
+ const schemeByFirstWord = {
158
+ // ... existing entries
159
+ my_feature: "my_feature",
160
+ };
161
+ ```
162
+
163
+ **6. Route scheme in `parser/parser.js`**
164
+
165
+ ```javascript
166
+ case "my_feature":
167
+ parser.myFeatureDeclaration();
168
+ return parser;
169
+ ```
170
+
171
+ **7. Write tests (`parser/my_feature_parser.spec.js`)**
172
+
173
+ ```javascript
174
+ import { expect } from "chai";
175
+ import { describe, it } from "mocha";
176
+ import { xanoscriptParser } from "./parser.js";
177
+
178
+ describe("my_feature", () => {
179
+ it("should parse a basic my_feature", () => {
180
+ const parser = xanoscriptParser(`my_feature "name" {
181
+ required_attr = "value"
182
+ }`);
183
+ expect(parser.errors).to.be.empty;
184
+ });
185
+ });
186
+ ```
187
+
188
+ ### Schema Definition Types (for `schemaParseAttributeFn`)
189
+
190
+ | Schema | Description | Example |
191
+ | ------------------------------ | ---------------------------------- | ------------------------- |
192
+ | `"[string]"` | String literal | `"hello"` |
193
+ | `"[number]"` | Number literal | `123` |
194
+ | `"[boolean]"` | Boolean value | `true` / `false` |
195
+ | `"[constant]"` | Value expression (no variables) | `"text"`, `123`, `{a: 1}` |
196
+ | `"[expression]"` | Any expression including variables | `$var`, `$input.name` |
197
+ | `["[string]"]` | Array of strings | `["a", "b"]` |
198
+ | `{ "[string]": "[constant]" }` | Object with string keys | `{key: "value"}` |
199
+ | `"attr?"` | Optional attribute | May be omitted |
200
+ | `"!attr"` | Can be disabled | `!attr = value` |
70
201
 
71
202
  ### New Keyword/Function
203
+
72
204
  1. Add token definition in appropriate `lexer/` file
73
205
  2. Create function implementation in `parser/functions/[domain]/`
74
206
  3. Register in parent clause or parser
75
207
  4. Add comprehensive tests in corresponding `.spec.js` file
76
208
 
77
209
  ### Token Creation Pattern
210
+
78
211
  ```javascript
79
212
  export const MyToken = createTokenByName("keyword", {
80
213
  longer_alt: Identifier,
@@ -83,6 +216,7 @@ export const MyToken = createTokenByName("keyword", {
83
216
  ```
84
217
 
85
218
  ### Parser Rule Pattern
219
+
86
220
  ```javascript
87
221
  myRule = this.RULE("myRule", () => {
88
222
  this.CONSUME(MyToken);
@@ -100,6 +234,7 @@ myRule = this.RULE("myRule", () => {
100
234
  - CI requires all tests to pass
101
235
 
102
236
  ### Test Pattern
237
+
103
238
  ```javascript
104
239
  function parse(inputText) {
105
240
  const lexResult = lexDocument(inputText);
@@ -118,4 +253,4 @@ function parse(inputText) {
118
253
  - Follow existing code conventions when adding features
119
254
  - Check neighboring files for patterns and conventions
120
255
  - Never assume a library is available - check `package.json` first
121
- - 3 low-severity npm audit warnings are known and acceptable
256
+ - 3 low-severity npm audit warnings are known and acceptable
@@ -0,0 +1,91 @@
1
+ import { lexDocument } from "../lexer/lexer.js";
2
+ import { xanoscriptParser } from "../parser/parser.js";
3
+ import { getSchemeFromContent } from "../utils.js";
4
+
5
+ /**
6
+ * Cache for parsed documents to avoid redundant parsing.
7
+ * Stores parse results keyed by document URI + version.
8
+ * Note: We cache a snapshot of parser state, not the parser instance itself,
9
+ * since the parser is a singleton that gets mutated on each parse.
10
+ */
11
+ class DocumentCache {
12
+ constructor() {
13
+ // Map<uri, { version: number, lexResult: Object, parserState: Object, scheme: string }>
14
+ this.cache = new Map();
15
+ }
16
+
17
+ /**
18
+ * Get cached parse result or parse and cache if not available.
19
+ * @param {string} uri - Document URI
20
+ * @param {number} version - Document version
21
+ * @param {string} text - Document text
22
+ * @returns {{ lexResult: Object, parser: Object, scheme: string }}
23
+ */
24
+ getOrParse(uri, version, text) {
25
+ const cached = this.cache.get(uri);
26
+ const textLength = text.length;
27
+
28
+ // Check version and text length to detect same URI with different content (e.g., in tests)
29
+ if (
30
+ cached &&
31
+ cached.version === version &&
32
+ cached.textLength === textLength
33
+ ) {
34
+ // Return cached result with a proxy parser object containing cached state
35
+ return {
36
+ lexResult: cached.lexResult,
37
+ parser: cached.parserState,
38
+ scheme: cached.scheme,
39
+ };
40
+ }
41
+
42
+ // Parse and cache a snapshot of the parser state
43
+ const scheme = getSchemeFromContent(text);
44
+ const lexResult = lexDocument(text);
45
+ const parser = xanoscriptParser(text, scheme, lexResult);
46
+
47
+ // Create a snapshot of the parser's state including symbol table
48
+ const parserState = {
49
+ errors: [...parser.errors],
50
+ warnings: [...parser.warnings],
51
+ informations: [...parser.informations],
52
+ hints: [...parser.hints],
53
+ __symbolTable: parser.__symbolTable
54
+ ? JSON.parse(JSON.stringify(parser.__symbolTable))
55
+ : null,
56
+ };
57
+
58
+ const cacheEntry = {
59
+ version,
60
+ textLength,
61
+ lexResult,
62
+ parserState,
63
+ scheme,
64
+ };
65
+
66
+ this.cache.set(uri, cacheEntry);
67
+
68
+ return {
69
+ lexResult,
70
+ parser: parserState,
71
+ scheme,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Invalidate cache for a document.
77
+ * @param {string} uri - Document URI
78
+ */
79
+ invalidate(uri) {
80
+ this.cache.delete(uri);
81
+ }
82
+
83
+ /**
84
+ * Clear all cached documents.
85
+ */
86
+ clear() {
87
+ this.cache.clear();
88
+ }
89
+ }
90
+
91
+ export const documentCache = new DocumentCache();
package/debug.js ADDED
@@ -0,0 +1,18 @@
1
+ /* global process */
2
+ /**
3
+ * Debug logging utility.
4
+ * Set XS_DEBUG=1 environment variable to enable debug logging.
5
+ */
6
+ const isDebug = process.env.XS_DEBUG === "1";
7
+
8
+ export function debugLog(...args) {
9
+ if (isDebug) {
10
+ console.log(...args);
11
+ }
12
+ }
13
+
14
+ export function debugError(...args) {
15
+ if (isDebug) {
16
+ console.error(...args);
17
+ }
18
+ }
package/lexer/run.js ADDED
@@ -0,0 +1,37 @@
1
+ import { Identifier } from "./identifier.js";
2
+ import { createTokenByName } from "./utils.js";
3
+
4
+ // "run"
5
+ export const RunToken = createTokenByName("run", {
6
+ longer_alt: Identifier,
7
+ categories: [Identifier],
8
+ });
9
+
10
+ // "job"
11
+ export const JobToken = createTokenByName("job", {
12
+ longer_alt: Identifier,
13
+ categories: [Identifier],
14
+ });
15
+
16
+ // "service"
17
+ export const ServiceToken = createTokenByName("service", {
18
+ longer_alt: Identifier,
19
+ categories: [Identifier],
20
+ });
21
+
22
+ export const RunTokens = [RunToken, JobToken, ServiceToken];
23
+
24
+ /**
25
+ * Maps a token name to a type
26
+ * @param {string} token the token name
27
+ */
28
+ export function mapTokenToType(token) {
29
+ switch (token) {
30
+ case RunToken.name:
31
+ case JobToken.name:
32
+ case ServiceToken.name:
33
+ return "keyword";
34
+ default:
35
+ return null;
36
+ }
37
+ }
package/lexer/tokens.js CHANGED
@@ -93,6 +93,7 @@ import {
93
93
  RealtimeTriggerTokens,
94
94
  } from "./realtime_trigger.js";
95
95
  import { mapTokenToType as mapRedisTokenToType, RedisTokens } from "./redis.js";
96
+ import { mapTokenToType as mapRunTokenToType, RunTokens } from "./run.js";
96
97
  import {
97
98
  mapTokenToType as mapSecurityTokenToType,
98
99
  SecurityTokens,
@@ -382,6 +383,7 @@ export const allTokens = uniq([
382
383
  ...DbTokens,
383
384
  ...RedisTokens,
384
385
  ...TextTokens,
386
+ ...RunTokens,
385
387
  ...ObjectTokens,
386
388
  ...StreamTokens,
387
389
  ...DebugTokens,
@@ -430,6 +432,7 @@ const tokenMappers = [
430
432
  mapQueryTokenToType,
431
433
  mapRealtimeTriggerTokenToType,
432
434
  mapRedisTokenToType,
435
+ mapRunTokenToType,
433
436
  mapSecurityTokenToType,
434
437
  mapStorageTokenToType,
435
438
  mapStreamTokenToType,
@@ -447,80 +450,91 @@ const tokenMappers = [
447
450
  mapZipTokenToType,
448
451
  ];
449
452
 
450
- /**
451
- * Map a token to a type (e.g., keyword, variable, etc.)
452
- * @param {import('chevrotain').TokenType} token
453
- * @returns string | null | undefined The type of the token
454
- */
455
- export function mapTokenToType(token) {
456
- // Check if the token is a keyword
457
- for (const mapper of tokenMappers) {
458
- const type = mapper(token);
459
- if (type) {
460
- return type;
461
- }
462
- }
463
-
464
- switch (token) {
465
- // Structural and control keywords (e.g., query blocks, conditionals)
466
- case Cachetoken.name:
467
- case HistoryToken.name:
468
- case IndexToken.name:
469
- case InputToken.name:
470
- case MiddlewareToken.name:
471
- case MockToken.name:
472
- case ResponseToken.name:
473
- case ViewToken.name:
474
- case SchemaToken.name:
475
- case SecurityToken.name:
476
- case StackToken.name:
477
- case TestToken.name:
478
- case FiltersToken.name:
479
- return "keyword";
480
-
481
- case DbLinkToken.name:
482
- return "function";
453
+ // Pre-built token type map for O(1) lookups
454
+ const tokenTypeMap = new Map();
455
+
456
+ // Build the map at module initialization time
457
+ function buildTokenTypeMap() {
458
+ // Add local token mappings first
459
+ const localMappings = {
460
+ // Structural and control keywords
461
+ [Cachetoken.name]: "keyword",
462
+ [HistoryToken.name]: "keyword",
463
+ [IndexToken.name]: "keyword",
464
+ [InputToken.name]: "keyword",
465
+ [MiddlewareToken.name]: "keyword",
466
+ [MockToken.name]: "keyword",
467
+ [ResponseToken.name]: "keyword",
468
+ [ViewToken.name]: "keyword",
469
+ [SchemaToken.name]: "keyword",
470
+ [SecurityToken.name]: "keyword",
471
+ [StackToken.name]: "keyword",
472
+ [TestToken.name]: "keyword",
473
+ [FiltersToken.name]: "keyword",
474
+
475
+ [DbLinkToken.name]: "function",
483
476
 
484
477
  // Variable-related tokens
485
- case AuthToken.name:
486
- case DbIdentifier.name:
487
- case DbReturnAggregateToken.name:
488
- case DescriptionToken.name:
489
- case DisabledToken.name:
490
- case DocsToken.name:
491
- case FieldToken.name: // field is also used as variable name in index definitio.namen
492
- case GuidToken.name:
493
- case SensitiveToken.name:
494
- case TagsToken.name:
495
- case TypeToken.name: // type is used as a variable name in index definitio.namen
496
- case ValueToken.name:
497
- case ValuesToken.name:
498
- return "variable";
499
-
500
- case Identifier.name:
501
- return "property";
502
-
503
- case FalseToken.name:
504
- case NowToken.name:
505
- case NullToken.name:
506
- case TrueToken.name:
507
- return "enumMember";
508
-
509
- case DotToken.name:
510
- return "punctuation";
511
-
512
- case RegExpToken.name:
513
- return "regexp";
514
-
515
- case JsonInToken.name:
516
- return "operator";
478
+ [AuthToken.name]: "variable",
479
+ [DbIdentifier.name]: "variable",
480
+ [DbReturnAggregateToken.name]: "variable",
481
+ [DescriptionToken.name]: "variable",
482
+ [DisabledToken.name]: "variable",
483
+ [DocsToken.name]: "variable",
484
+ [FieldToken.name]: "variable",
485
+ [GuidToken.name]: "variable",
486
+ [SensitiveToken.name]: "variable",
487
+ [TagsToken.name]: "variable",
488
+ [TypeToken.name]: "variable",
489
+ [ValueToken.name]: "variable",
490
+ [ValuesToken.name]: "variable",
491
+
492
+ [Identifier.name]: "property",
493
+
494
+ [FalseToken.name]: "enumMember",
495
+ [NowToken.name]: "enumMember",
496
+ [NullToken.name]: "enumMember",
497
+ [TrueToken.name]: "enumMember",
498
+
499
+ [DotToken.name]: "punctuation",
500
+ [RegExpToken.name]: "regexp",
501
+ [JsonInToken.name]: "operator",
517
502
 
518
503
  // Skip whitespace and newlines
519
- case NewlineToken.name:
520
- case WhiteSpace.name:
521
- return null;
504
+ [NewlineToken.name]: null,
505
+ [WhiteSpace.name]: null,
506
+ };
507
+
508
+ for (const [tokenName, type] of Object.entries(localMappings)) {
509
+ tokenTypeMap.set(tokenName, type);
510
+ }
511
+
512
+ // Iterate through all tokens and populate the map using the mappers
513
+ for (const token of allTokens) {
514
+ if (tokenTypeMap.has(token.name)) continue;
522
515
 
523
- default:
524
- return undefined; // Skip unmapped or unknown tokens
516
+ for (const mapper of tokenMappers) {
517
+ const type = mapper(token.name);
518
+ if (type) {
519
+ tokenTypeMap.set(token.name, type);
520
+ break;
521
+ }
522
+ }
523
+ }
524
+ }
525
+
526
+ // Initialize the map at module load
527
+ buildTokenTypeMap();
528
+
529
+ /**
530
+ * Map a token to a type (e.g., keyword, variable, etc.)
531
+ * Uses pre-built map for O(1) lookup instead of O(n) iteration.
532
+ * @param {string} token - Token name
533
+ * @returns {string | null | undefined} The type of the token
534
+ */
535
+ export function mapTokenToType(token) {
536
+ if (tokenTypeMap.has(token)) {
537
+ return tokenTypeMap.get(token);
525
538
  }
539
+ return undefined; // Skip unmapped or unknown tokens
526
540
  }
@@ -109,22 +109,23 @@ function isAfterPipeToken(tokens) {
109
109
  return lastToken.tokenType === PipeToken;
110
110
  }
111
111
 
112
- function createFilterSuggestions() {
113
- return filterNames.map((filterName) => {
112
+ // Pre-computed filter suggestions - frozen to prevent accidental mutations
113
+ const filterSuggestions = Object.freeze(
114
+ filterNames.map((filterName) => {
114
115
  const documentation = filterMessageProvider.__filterDoc[filterName];
115
116
 
116
- return {
117
+ return Object.freeze({
117
118
  label: filterName,
118
119
  kind: encodeTokenType("function"), // Filters are function-like
119
120
  documentation: documentation
120
- ? {
121
+ ? Object.freeze({
121
122
  kind: "markdown",
122
123
  value: documentation,
123
- }
124
+ })
124
125
  : undefined,
125
- };
126
- });
127
- }
126
+ });
127
+ })
128
+ );
128
129
 
129
130
  export function getContentAssistSuggestions(text, scheme) {
130
131
  try {
@@ -134,7 +135,7 @@ export function getContentAssistSuggestions(text, scheme) {
134
135
 
135
136
  // Check if we're after a pipe token - if so, suggest filters
136
137
  if (isAfterPipeToken(partialTokenVector)) {
137
- return createFilterSuggestions();
138
+ return filterSuggestions;
138
139
  }
139
140
 
140
141
  let syntacticSuggestions;
@@ -1,5 +1,55 @@
1
- import { xanoscriptParser } from "../parser/parser";
2
- import { getSchemeFromContent } from "../utils";
1
+ import { documentCache } from "../cache/documentCache.js";
2
+ import { debugError, debugLog } from "../debug.js";
3
+
4
+ // Diagnostic severity constants
5
+ const SEVERITY = {
6
+ ERROR: 1,
7
+ WARNING: 2,
8
+ INFORMATION: 3,
9
+ HINT: 4,
10
+ };
11
+
12
+ /**
13
+ * Creates diagnostics from parser results in a single pass.
14
+ * @param {Object} parser - The parser with errors, warnings, informations, hints
15
+ * @param {Object} document - The text document for position conversion
16
+ * @returns {Array} Array of diagnostic objects
17
+ */
18
+ function createDiagnostics(parser, document) {
19
+ const diagnostics = [];
20
+ const defaultRange = {
21
+ start: { line: 0, character: 0 },
22
+ end: { line: 0, character: 1 },
23
+ };
24
+
25
+ const addDiagnostic = (item, severity) => {
26
+ diagnostics.push({
27
+ severity,
28
+ range: item.token
29
+ ? {
30
+ start: document.positionAt(item.token.startOffset),
31
+ end: document.positionAt(item.token.endOffset + 1),
32
+ }
33
+ : defaultRange,
34
+ message: item.message,
35
+ });
36
+ };
37
+
38
+ for (const error of parser.errors) {
39
+ addDiagnostic(error, SEVERITY.ERROR);
40
+ }
41
+ for (const warning of parser.warnings) {
42
+ addDiagnostic(warning, SEVERITY.WARNING);
43
+ }
44
+ for (const info of parser.informations) {
45
+ addDiagnostic(info, SEVERITY.INFORMATION);
46
+ }
47
+ for (const hint of parser.hints) {
48
+ addDiagnostic(hint, SEVERITY.HINT);
49
+ }
50
+
51
+ return diagnostics;
52
+ }
3
53
 
4
54
  /**
5
55
  *
@@ -11,7 +61,7 @@ export function onDidChangeContent(params, connection) {
11
61
  const document = params.document;
12
62
 
13
63
  if (!document) {
14
- console.error(
64
+ debugError(
15
65
  "onDidChangeContent(): Document not found for URI:",
16
66
  params.textDocument.uri
17
67
  );
@@ -20,96 +70,36 @@ export function onDidChangeContent(params, connection) {
20
70
 
21
71
  const text = document.getText();
22
72
 
23
- const scheme = getSchemeFromContent(text);
24
-
25
73
  try {
26
- // Parse the XanoScript file
27
- const parser = xanoscriptParser(text, scheme);
74
+ // Parse the XanoScript file using cache
75
+ const { parser, scheme } = documentCache.getOrParse(
76
+ document.uri,
77
+ document.version,
78
+ text
79
+ );
28
80
 
29
- if (parser.errors.length > 0) {
30
- // If parsing succeeds, send an empty diagnostics array (no errors)
81
+ if (parser.errors.length === 0) {
82
+ // If parsing succeeds with no errors, send an empty diagnostics array
31
83
  connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
32
84
  }
33
85
 
34
86
  for (const error of parser.errors) {
35
- console.error(
87
+ debugError(
36
88
  `onDidChangeContent(): Error parsing document: ${error.name}`
37
89
  );
38
90
  }
39
91
 
40
- // If parsing fails, create a diagnostic (error message) to display in VS Code
41
- const errors = parser.errors.map((error) => {
42
- return {
43
- severity: 1,
44
- range: error.token
45
- ? {
46
- start: document.positionAt(error.token.startOffset),
47
- end: document.positionAt(error.token.endOffset + 1),
48
- }
49
- : {
50
- start: { line: 0, character: 0 },
51
- end: { line: 0, character: 1 },
52
- },
53
- message: error.message,
54
- };
55
- });
56
-
57
- const warnings = parser.warnings.map((warning) => {
58
- return {
59
- severity: 2,
60
- range: warning.token
61
- ? {
62
- start: document.positionAt(warning.token.startOffset),
63
- end: document.positionAt(warning.token.endOffset + 1),
64
- }
65
- : {
66
- start: { line: 0, character: 0 },
67
- end: { line: 0, character: 1 },
68
- },
69
- message: warning.message,
70
- };
71
- });
72
-
73
- const informations = parser.informations.map((info) => {
74
- return {
75
- severity: 3,
76
- range: info.token
77
- ? {
78
- start: document.positionAt(info.token.startOffset),
79
- end: document.positionAt(info.token.endOffset + 1),
80
- }
81
- : {
82
- start: { line: 0, character: 0 },
83
- end: { line: 0, character: 1 },
84
- },
85
- message: info.message,
86
- };
87
- });
88
-
89
- const hints = parser.hints.map((hint) => {
90
- return {
91
- severity: 4,
92
- range: hint.token
93
- ? {
94
- start: document.positionAt(hint.token.startOffset),
95
- end: document.positionAt(hint.token.endOffset + 1),
96
- }
97
- : {
98
- start: { line: 0, character: 0 },
99
- end: { line: 0, character: 1 },
100
- },
101
- message: hint.message,
102
- };
103
- });
92
+ // Create diagnostics in a single pass
93
+ const diagnostics = createDiagnostics(parser, document);
104
94
 
105
- console.log(
95
+ debugLog(
106
96
  `onDidChangeContent(): sending diagnostic (${parser.errors.length} errors) for scheme:`,
107
97
  scheme
108
98
  );
109
99
 
110
100
  connection.sendDiagnostics({
111
101
  uri: document.uri,
112
- diagnostics: [...errors, ...warnings, ...informations, ...hints],
102
+ diagnostics,
113
103
  });
114
104
  } catch (error) {
115
105
  // If parsing fails, create a diagnostic (error message) to display in VS Code
@@ -287,6 +287,62 @@ A `task` file defines a scheduled job that runs automatically at specified times
287
287
 
288
288
  Tasks are ideal for automating recurring operations like generating reports or syncing data.
289
289
 
290
+ # run.job
291
+
292
+ ```xs
293
+ run.job "Gemini -> Image Understanding" {
294
+ main = {
295
+ name: "Gemini -> Image Understanding"
296
+ input: {
297
+ model: "gemini-1.5-flash"
298
+ prompt: "Describe what is happening in this image."
299
+ image: "(attach image file)"
300
+ }
301
+ }
302
+ env = ["gemini_api_key"]
303
+ }
304
+ ```
305
+
306
+ A `run.job` file defines a job configuration for execution in the Xano Job Runner. It includes:
307
+
308
+ - A name (e.g., `"Gemini -> Image Understanding"`) to identify the job,
309
+ - A required `main` attribute specifying the function to execute:
310
+ - `name`: The name of the function to call,
311
+ - `input`: Optional input parameters to pass to the function,
312
+ - An optional `env` array listing environment variable names required by the job.
313
+
314
+ Jobs are used to run functions as standalone processes, typically for long-running or resource-intensive operations.
315
+
316
+ # run.service
317
+
318
+ ```xs
319
+ run.service "email proxy" {
320
+ pre = {
321
+ name: "email_proxy_init"
322
+ input: {
323
+ config: "default"
324
+ }
325
+ }
326
+ env = ["email_proxy_api_key"]
327
+ }
328
+ ```
329
+
330
+ A `run.service` file defines a service configuration for the Xano Job Runner. It includes:
331
+
332
+ - A name (e.g., `"email proxy"`) to identify the service,
333
+ - An optional `pre` attribute specifying an initialization function to run before the service starts:
334
+ - `name`: The name of the initialization function,
335
+ - `input`: Optional input parameters for the initialization,
336
+ - An optional `env` array listing environment variable names required by the service.
337
+
338
+ Services can also be defined in minimal form without a body:
339
+
340
+ ```xs
341
+ run.service "email proxy"
342
+ ```
343
+
344
+ Services are ideal for long-running background processes like proxies, webhooks listeners, or daemon-style operations.
345
+
290
346
  # action.call
291
347
 
292
348
  ```xs
@@ -1,6 +1,32 @@
1
- import { lexDocument } from "../lexer/lexer.js";
2
- import { xanoscriptParser } from "../parser/parser.js";
3
- import { getSchemeFromContent } from "../utils.js";
1
+ import { documentCache } from "../cache/documentCache.js";
2
+ import { debugError } from "../debug.js";
3
+
4
+ /**
5
+ * Binary search to find token index at the given offset.
6
+ * Reduces complexity from O(n) to O(log n).
7
+ * @param {Array} tokens - Array of tokens sorted by startOffset
8
+ * @param {number} offset - Cursor offset position
9
+ * @returns {number} Token index or -1 if not found
10
+ */
11
+ function findTokenAtOffset(tokens, offset) {
12
+ let left = 0;
13
+ let right = tokens.length - 1;
14
+
15
+ while (left <= right) {
16
+ const mid = Math.floor((left + right) / 2);
17
+ const token = tokens[mid];
18
+
19
+ if (token.startOffset <= offset && token.endOffset >= offset) {
20
+ return mid;
21
+ } else if (token.endOffset < offset) {
22
+ left = mid + 1;
23
+ } else {
24
+ right = mid - 1;
25
+ }
26
+ }
27
+
28
+ return -1;
29
+ }
4
30
 
5
31
  /**
6
32
  *
@@ -12,7 +38,7 @@ export function onHoverDocument(params, documents, hoverProviders = []) {
12
38
  const document = documents.get(params.textDocument.uri);
13
39
 
14
40
  if (!document) {
15
- console.error(
41
+ debugError(
16
42
  "onHover(): Document not found for URI:",
17
43
  params.textDocument.uri
18
44
  );
@@ -22,21 +48,19 @@ export function onHoverDocument(params, documents, hoverProviders = []) {
22
48
  const text = document.getText();
23
49
  const offset = document.offsetAt(params.position);
24
50
 
25
- // Tokenize the document
26
- const lexResult = lexDocument(text);
27
- if (lexResult.errors.length > 0) return null;
51
+ // Get cached parse result or parse and cache
52
+ const { lexResult, parser } = documentCache.getOrParse(
53
+ params.textDocument.uri,
54
+ document.version,
55
+ text
56
+ );
28
57
 
29
- // attempt to get the scheme from the document uri
30
- const scheme = getSchemeFromContent(text);
58
+ if (lexResult.errors.length > 0) return null;
31
59
 
32
- // Parse the XanoScript file
33
- const parser = xanoscriptParser(text, scheme);
34
60
  const tokens = lexResult.tokens;
35
61
 
36
- // Find the token under the cursor
37
- const tokenIdx = tokens.findIndex(
38
- (token) => token.startOffset <= offset && token.endOffset >= offset
39
- );
62
+ // Find the token under the cursor using binary search (O(log n))
63
+ const tokenIdx = findTokenAtOffset(tokens, offset);
40
64
 
41
65
  if (tokenIdx === -1) {
42
66
  return null;
@@ -1,3 +1,4 @@
1
+ import { debugLog } from "../debug.js";
1
2
  import { lexDocument } from "../lexer/lexer.js";
2
3
  import { mapTokenToType } from "../lexer/tokens.js";
3
4
  import { encodeTokenType } from "./tokens.js";
@@ -33,7 +34,7 @@ function higlightDefault(text, SemanticTokensBuilder) {
33
34
  0 // No modifiers for now
34
35
  );
35
36
  } else if (tokenType === undefined) {
36
- console.log(
37
+ debugLog(
37
38
  `token type not mapped to a type: ${JSON.stringify(
38
39
  token.tokenType.name
39
40
  )}`
@@ -1,3 +1,4 @@
1
+ import { debugError } from "../debug.js";
1
2
  import { getSchemeFromContent } from "../utils";
2
3
  import { higlightText } from "./highlight";
3
4
 
@@ -11,7 +12,7 @@ export function onSemanticCheck(params, documents, SemanticTokensBuilder) {
11
12
  const document = documents.get(params.textDocument.uri);
12
13
 
13
14
  if (!document) {
14
- console.error(
15
+ debugError(
15
16
  "onSemanticCheck(): Document not found for URI:",
16
17
  params.textDocument.uri
17
18
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xano/xanoscript-language-server",
3
- "version": "11.0.5",
3
+ "version": "11.1.0",
4
4
  "description": "Language Server Protocol implementation for XanoScript",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -24,10 +24,8 @@
24
24
  "prepare": "husky"
25
25
  },
26
26
  "dependencies": {
27
- "chai": "^5.2.0",
28
27
  "chevrotain": "^11.0.3",
29
28
  "lodash-es": "^4.17.21",
30
- "mocha": "^11.1.0",
31
29
  "vscode-languageserver": "^9.0.1",
32
30
  "vscode-languageserver-textdocument": "^1.0.12"
33
31
  },
@@ -38,11 +36,13 @@
38
36
  },
39
37
  "devDependencies": {
40
38
  "@eslint/js": "^9.22.0",
39
+ "chai": "^5.2.0",
41
40
  "eslint": "^9.22.0",
42
41
  "eslint-plugin-simple-import-sort": "^12.1.1",
43
42
  "eslint-plugin-unused-imports": "^4.2.0",
44
43
  "globals": "^16.0.0",
45
44
  "husky": "^9.1.7",
46
- "lint-staged": "^16.2.4"
45
+ "lint-staged": "^16.2.4",
46
+ "mocha": "^11.1.0"
47
47
  }
48
48
  }
@@ -32,6 +32,11 @@ describe("docsFieldAttribute", () => {
32
32
  expect(parser.errors).to.be.empty;
33
33
  });
34
34
 
35
+ it("docsFieldAttribute can be multiline", () => {
36
+ const parser = parse('docs="""\nanother docs\n"""');
37
+ expect(parser.errors).to.be.empty;
38
+ });
39
+
35
40
  it("docsFieldAttribute does not require a new line", () => {
36
41
  const parser = parse('docs="some docs"');
37
42
  expect(parser.errors).to.be.empty;
@@ -47,12 +47,12 @@ describe("apiCallFn", () => {
47
47
  });
48
48
 
49
49
  it("apiCallFn requires a url and method field", () => {
50
- let parser = parse(`request {
50
+ let parser = parse(`call foo {
51
51
  url = "https://www.example.com"
52
52
  } as $user`);
53
53
  expect(parser.errors).to.not.be.empty;
54
54
 
55
- parser = parse(`request {
55
+ parser = parse(`call foo {
56
56
  method = "GET"
57
57
  } as $user`);
58
58
  expect(parser.errors).to.not.be.empty;
@@ -59,7 +59,6 @@ describe("tryCatchFn", () => {
59
59
  }
60
60
  }
61
61
  }`);
62
- console.log(parser.errors);
63
62
  expect(parser.errors).to.be.empty;
64
63
  });
65
64
 
@@ -5,6 +5,7 @@ import { Identifier, NewlineToken } from "../../lexer/tokens.js";
5
5
  import { getVarName } from "./utils.js";
6
6
 
7
7
  /**
8
+ * @deprecated use parser/functions/schema/schemaParseAttributeFn.js instead
8
9
  * @param {import('../base_parser.js').XanoBaseParser} $
9
10
  */
10
11
  export function objectWithAttributes($) {
package/parser/parser.js CHANGED
@@ -5,15 +5,16 @@ import { XanoBaseParser } from "./base_parser.js";
5
5
 
6
6
  /**
7
7
  * Will parse the content of the file based on its scheme (db:/, task:/, api:/...)
8
- * @param {string} scheme
9
8
  * @param {string} text
9
+ * @param {string} scheme
10
+ * @param {Object} [preTokenized] - Optional pre-tokenized result from lexDocument to avoid re-lexing
10
11
  * @returns
11
12
  */
12
- export function xanoscriptParser(text, scheme) {
13
+ export function xanoscriptParser(text, scheme, preTokenized = null) {
13
14
  if (!scheme) {
14
15
  scheme = getSchemeFromContent(text);
15
16
  }
16
- const lexResult = lexDocument(text);
17
+ const lexResult = preTokenized || lexDocument(text);
17
18
  parser.input = lexResult.tokens;
18
19
  switch (scheme.toLowerCase()) {
19
20
  case "addon":
@@ -52,6 +53,9 @@ export function xanoscriptParser(text, scheme) {
52
53
  case "realtime_channel":
53
54
  parser.realtimeChannelDeclaration();
54
55
  return parser;
56
+ case "run":
57
+ parser.runDeclaration();
58
+ return parser;
55
59
  case "table_trigger":
56
60
  parser.tableTriggerDeclaration();
57
61
  return parser;
@@ -16,6 +16,11 @@ import { middlewareDeclaration } from "./middleware_parser.js";
16
16
  import { queryDeclaration } from "./query_parser.js";
17
17
  import { realtimeChannelDeclaration } from "./realtime_channel_parser.js";
18
18
  import { realtimeTriggerDeclaration } from "./realtime_trigger_parser.js";
19
+ import {
20
+ runDeclaration,
21
+ runJobClause,
22
+ runServiceClause,
23
+ } from "./run_parser.js";
19
24
  import { tableDeclaration } from "./table_parser.js";
20
25
  import { tableTriggerDeclaration } from "./table_trigger_parser.js";
21
26
  import { taskDeclaration } from "./task_parser.js";
@@ -66,6 +71,9 @@ export const register = ($) => {
66
71
  "realtimeChannelDeclaration",
67
72
  realtimeChannelDeclaration($)
68
73
  );
74
+ $.runDeclaration = $.RULE("runDeclaration", runDeclaration($));
75
+ $.runJobClause = $.RULE("runJobClause", runJobClause($));
76
+ $.runServiceClause = $.RULE("runServiceClause", runServiceClause($));
69
77
  $.tableTriggerDeclaration = $.RULE(
70
78
  "tableTriggerDeclaration",
71
79
  tableTriggerDeclaration($)
@@ -0,0 +1,70 @@
1
+ import { StringLiteral } from "../lexer/literal.js";
2
+ import { JobToken, RunToken, ServiceToken } from "../lexer/run.js";
3
+ import { DotToken, Identifier, NewlineToken } from "../lexer/tokens.js";
4
+
5
+ export function runDeclaration($) {
6
+ return () => {
7
+ $.sectionStack.push("runDeclaration");
8
+ // Allow leading comments and newlines before the run declaration
9
+ $.SUBRULE($.optionalCommentBlockFn);
10
+
11
+ $.CONSUME(RunToken); // "run"
12
+ $.CONSUME(DotToken); // "."
13
+
14
+ $.OR([
15
+ { ALT: () => $.SUBRULE($.runJobClause) },
16
+ { ALT: () => $.SUBRULE($.runServiceClause) },
17
+ ]);
18
+
19
+ $.MANY2(() => $.CONSUME2(NewlineToken)); // optional trailing newlines
20
+ $.sectionStack.pop();
21
+ };
22
+ }
23
+
24
+ export function runJobClause($) {
25
+ return () => {
26
+ const parent = $.CONSUME(JobToken); // "job"
27
+
28
+ $.OR([
29
+ { ALT: () => $.CONSUME(StringLiteral) },
30
+ { ALT: () => $.CONSUME(Identifier) },
31
+ ]);
32
+
33
+ $.SUBRULE($.schemaParseAttributeFn, {
34
+ ARGS: [
35
+ parent,
36
+ {
37
+ main: {
38
+ name: "[string]",
39
+ input: { "[string]?": "[constant]" },
40
+ },
41
+ "env?": ["[string]"],
42
+ },
43
+ ],
44
+ });
45
+ };
46
+ }
47
+
48
+ export function runServiceClause($) {
49
+ return () => {
50
+ const parent = $.CONSUME(ServiceToken); // "service"
51
+
52
+ $.OR([
53
+ { ALT: () => $.CONSUME(StringLiteral) },
54
+ { ALT: () => $.CONSUME(Identifier) },
55
+ ]);
56
+
57
+ $.SUBRULE($.schemaParseAttributeFn, {
58
+ ARGS: [
59
+ parent,
60
+ {
61
+ "pre?": {
62
+ name: "[string]",
63
+ input: { "[string]?": "[constant]" },
64
+ },
65
+ "env?": ["[string]"],
66
+ },
67
+ ],
68
+ });
69
+ };
70
+ }
@@ -0,0 +1,100 @@
1
+ import { expect } from "chai";
2
+ import { describe, it } from "mocha";
3
+ import { xanoscriptParser } from "./parser.js";
4
+
5
+ describe("run", () => {
6
+ it("should parse a basic run.job", () => {
7
+ const parser = xanoscriptParser(`run.job "Average of values" {
8
+ main = {name: "avg_value", input: {}}
9
+ }`);
10
+ expect(parser.errors).to.be.empty;
11
+ });
12
+
13
+ it("should parse a basic run.job with inputs", () => {
14
+ const parser = xanoscriptParser(`run.job "Average of values" {
15
+ main = {name: "avg_value", input: {left: 1, right: 2}}
16
+ }`);
17
+ expect(parser.errors).to.be.empty;
18
+ });
19
+
20
+ it("run.job requires a main attribute", () => {
21
+ let parser = xanoscriptParser(`run.job "Average of values" {
22
+ pre = {name: "avg_value", input: {left: 1, right: 2}}
23
+ }`);
24
+ expect(parser.errors).to.not.be.empty;
25
+
26
+ parser = xanoscriptParser(`run.job "Average of values" {}`);
27
+ expect(parser.errors).to.not.be.empty;
28
+ });
29
+
30
+ it("should accept env variable", () => {
31
+ const parser = xanoscriptParser(`run.job "Gemini -> Image Understanding" {
32
+ main = {
33
+ name : "Gemini -> Image Understanding"
34
+ input: {
35
+ model : "gemini-1.5-flash"
36
+ prompt: "Describe what is happening in this image."
37
+ image : "(attach image file)"
38
+ }
39
+ }
40
+
41
+ env = ["gemini_api_key"]
42
+ }`);
43
+ expect(parser.errors).to.be.empty;
44
+ });
45
+
46
+ it("should parse a basic run.service", () => {
47
+ const parser = xanoscriptParser(`run.service "email proxy" {
48
+ pre = {name: "email_proxy_fn", input: {}}
49
+ }`);
50
+ expect(parser.errors).to.be.empty;
51
+ });
52
+
53
+ it("run.service has a minimal form", () => {
54
+ const parser = xanoscriptParser(`run.service "email proxy"`);
55
+ expect(parser.errors).to.be.empty;
56
+ });
57
+
58
+ it("environment variables can only be an array of strings", () => {
59
+ let parser = xanoscriptParser(`run.service "email proxy" {
60
+ env = ["email_proxy_api_key", 123, true]
61
+ }`);
62
+ expect(parser.errors).to.not.be.empty;
63
+
64
+ parser = xanoscriptParser(`run.job "Average of values" {
65
+ main = {name: "avg_value", input: {}}
66
+ env = ["email_proxy_api_key", 123, true]
67
+ }`);
68
+ expect(parser.errors).to.not.be.empty;
69
+
70
+ parser = xanoscriptParser(`run.service "email proxy" {
71
+ env = []
72
+ }`);
73
+ expect(parser.errors).to.be.empty;
74
+
75
+ parser = xanoscriptParser(`run.service "email proxy" {
76
+ env = ["a_value", "another value"]
77
+ }`);
78
+ expect(parser.errors).to.be.empty;
79
+ });
80
+
81
+ it("run.service does not require a pre", () => {
82
+ const parser = xanoscriptParser(`run.service "email proxy"`);
83
+ expect(parser.errors).to.be.empty;
84
+ });
85
+
86
+ it("run.service does not accept a main", () => {
87
+ const parser = xanoscriptParser(`run.service "email proxy" {
88
+ main = { name : "some_fn", input: {} }
89
+ }`);
90
+ expect(parser.errors).to.not.be.empty;
91
+ });
92
+
93
+ it("run.service accepts env variables", () => {
94
+ const parser = xanoscriptParser(`run.service "email proxy" {
95
+ pre = {name: "email_proxy_init", input: {}}
96
+ env = ["email_proxy_api_key"]
97
+ }`);
98
+ expect(parser.errors).to.be.empty;
99
+ });
100
+ });
package/server.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  TextDocuments,
6
6
  } from "vscode-languageserver/node.js";
7
7
  import { TextDocument } from "vscode-languageserver-textdocument";
8
+ import { debugLog } from "./debug.js";
8
9
  import { onCompletion } from "./onCompletion/onCompletion.js";
9
10
  import { onDidChangeContent } from "./onDidChangeContent/onDidChangeContent.js";
10
11
  import { onHover } from "./onHover/onHover.js";
@@ -50,7 +51,7 @@ documents.onDidChangeContent((params) =>
50
51
  onDidChangeContent(params, connection)
51
52
  );
52
53
  connection.onDidOpenTextDocument((params) => {
53
- console.log("Document opened:", params.textDocument.uri);
54
+ debugLog("Document opened:", params.textDocument.uri);
54
55
  // Existing handler logic
55
56
  });
56
57
 
package/utils.js CHANGED
@@ -30,6 +30,7 @@ const schemeByFirstWord = {
30
30
  query: "api",
31
31
  realtime_trigger: "realtime_trigger",
32
32
  realtime_channel: "realtime_channel",
33
+ run: "run",
33
34
  table: "db",
34
35
  table_trigger: "table_trigger",
35
36
  task: "task",
@@ -1,34 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm test:*)",
5
- "Bash(npm run lint)",
6
- "Bash(npm run test:*)",
7
- "Bash(npx eslint:*)",
8
- "Bash(node:*)",
9
- "Bash(npm install)",
10
- "Bash(grep:*)",
11
- "Bash(git -C /Users/justinalbrecht/git/xs-language-server log --all --pretty=format:\"%h %s\")",
12
- "Bash(git -C /Users/justinalbrecht/git/xs-language-server log --all -S \"password\\\\|secret\\\\|api_key\\\\|token\" --oneline)",
13
- "Bash(xargs file:*)",
14
- "Bash(find:*)",
15
- "Bash(git -C /Users/justinalbrecht/git/xs-language-server log -p --all -S \"webhook\\\\|Bearer\\\\|discord\\\\|slack\\\\|xano_insiders\" -- \"*.xs\" \"*.js\")",
16
- "Bash(git -C /Users/justinalbrecht/git/xs-language-server ls-files:*)",
17
- "Bash(git -C /Users/justinalbrecht/git/xs-language-server status)",
18
- "Bash(git -C /Users/justinalbrecht/git/xs-language-server diff --stat)",
19
- "Bash(git -C /Users/justinalbrecht/git/xs-language-server log --oneline -5)",
20
- "Bash(git -C /Users/justinalbrecht/git/xs-language-server add README.md LICENSE package.json lexer/tests/query/valid_sources/basic_query.xs parser/functions/stream/streamFromRequestFn.js parser/functions/stream/streamFromRequestFn.spec.js parser/tests/function/valid_sources/discord_poll_send_to_slack.xs parser/tests/query/valid_sources/all_basics.xs)",
21
- "Bash(git -C /Users/justinalbrecht/git/xs-language-server commit -m \"$\\(cat <<''EOF''\nPrepare package for public npm release as @xano/xanoscript-language-server\n\n- Rename package to @xano/xanoscript-language-server\n- Add MIT LICENSE file\n- Add npm metadata \\(description, repository, author, license, bugs, homepage\\)\n- Remove private flag to allow publishing\n- Sanitize test files: replace internal URLs, IDs, and env var names with placeholders\n- Simplify README maintainer section\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
22
- "Bash(git -C /Users/justinalbrecht/git/xs-language-server log -1 --oneline)",
23
- "Bash(git add:*)",
24
- "Bash(git commit:*)",
25
- "Bash(git push)",
26
- "Bash(npm whoami:*)",
27
- "Bash(npm view:*)",
28
- "Bash(npm version:*)",
29
- "Bash(npm publish:*)"
30
- ],
31
- "deny": [],
32
- "ask": []
33
- }
34
- }