@telorun/ide-support 0.2.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.
Files changed (47) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +238 -0
  3. package/dist/completions/build.d.ts +4 -0
  4. package/dist/completions/build.d.ts.map +1 -0
  5. package/dist/completions/build.js +30 -0
  6. package/dist/completions/detect-context.d.ts +34 -0
  7. package/dist/completions/detect-context.d.ts.map +1 -0
  8. package/dist/completions/detect-context.js +154 -0
  9. package/dist/completions/index.d.ts +3 -0
  10. package/dist/completions/index.d.ts.map +1 -0
  11. package/dist/completions/index.js +1 -0
  12. package/dist/completions/prop-keys.d.ts +4 -0
  13. package/dist/completions/prop-keys.d.ts.map +1 -0
  14. package/dist/completions/prop-keys.js +45 -0
  15. package/dist/completions/valid-capabilities.d.ts +2 -0
  16. package/dist/completions/valid-capabilities.d.ts.map +1 -0
  17. package/dist/completions/valid-capabilities.js +8 -0
  18. package/dist/diagnostics/index.d.ts +4 -0
  19. package/dist/diagnostics/index.d.ts.map +1 -0
  20. package/dist/diagnostics/index.js +3 -0
  21. package/dist/diagnostics/normalize.d.ts +11 -0
  22. package/dist/diagnostics/normalize.d.ts.map +1 -0
  23. package/dist/diagnostics/normalize.js +23 -0
  24. package/dist/diagnostics/range-resolver.d.ts +10 -0
  25. package/dist/diagnostics/range-resolver.d.ts.map +1 -0
  26. package/dist/diagnostics/range-resolver.js +27 -0
  27. package/dist/diagnostics/severity.d.ts +6 -0
  28. package/dist/diagnostics/severity.d.ts.map +1 -0
  29. package/dist/diagnostics/severity.js +6 -0
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +3 -0
  33. package/dist/types.d.ts +31 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/types.js +3 -0
  36. package/package.json +48 -0
  37. package/src/completions/build.ts +39 -0
  38. package/src/completions/detect-context.ts +185 -0
  39. package/src/completions/index.ts +2 -0
  40. package/src/completions/prop-keys.ts +57 -0
  41. package/src/completions/valid-capabilities.ts +8 -0
  42. package/src/diagnostics/index.ts +3 -0
  43. package/src/diagnostics/normalize.ts +30 -0
  44. package/src/diagnostics/range-resolver.ts +32 -0
  45. package/src/diagnostics/severity.ts +8 -0
  46. package/src/index.ts +3 -0
  47. package/src/types.ts +41 -0
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ # SUSTAINABLE USE LICENSE (Fair-code)
2
+
3
+ Copyright (c) 2026 DiglyAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute the Software for any purpose—including commercial purposes—subject to the following conditions:
6
+
7
+ 1. ANTI-COMPETITION RESTRICTION: The Software may not be provided to third parties as a managed service, commercial SaaS (Software-as-a-Service), PaaS (Platform-as-a-Service), BaaS (Backend-as-a-Service), or similar offering where the primary value provided to the user is the functionality of the Software itself, without a separate commercial license from the copyright holder.
8
+
9
+ 2. PERMITTED COMMERCIAL USE: You are free to use the Software to build, host, and monetize your own commercial applications, products, and services, provided such use does not violate Clause 1.
10
+
11
+ 3. ATTRIBUTION: This copyright notice and license must be included in all copies or substantial portions of the Software.
12
+
13
+ 4. CONTRIBUTIONS: Contributions to the Software are welcome and encouraged. By contributing, you agree that your contributions may be incorporated into the Software and distributed under this license.
14
+
15
+ 5. DISCLAIMER: The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
16
+
17
+ For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact DiglyAI.
package/README.md ADDED
@@ -0,0 +1,238 @@
1
+ ---
2
+ description: "Telo: YAML-driven execution engine for declarative backends with micro-kernel architecture and language-agnostic design"
3
+ ---
4
+
5
+ # ⚡ Telo
6
+
7
+ Runtime for declarative backends.
8
+
9
+ Telo is an execution engine (Micro-Kernel) that runs logic defined entirely in YAML manifests. Instead of writing imperative backend code, you define your routes, databases, schemas, and AI workflows as atomic, interconnected YAML documents. Telo takes those manifests and runs them.
10
+
11
+ Built to be language-agnostic and infinitely extensible.
12
+
13
+ ```bash
14
+ # Reconcile your manifest into a running backend
15
+ $ telo ./examples/hello-api.yaml
16
+
17
+ {"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8844"}
18
+ ```
19
+
20
+ ## Why use Telo?
21
+
22
+ - **Open Standards:** Built on YAML, JSON Schema, and CEL — no proprietary DSL.
23
+ - **Static Analysis:** CEL type checking, reference validation, and IDE diagnostics catch errors before runtime.
24
+ - **Micro-Kernel Architecture:** Telo itself knows nothing about HTTP or SQL. Everything is a module you import, scope, and compose with typed variable and secret contracts.
25
+ - **Language Agnostic:** Available as a Node.js runtime today, with a shared YAML runtime contract that allows for future Rust or Go implementations without changing your manifests.
26
+
27
+ ## What It Does
28
+
29
+ - **Loads** YAML resources and compiles CEL expressions (`${{ }}`) into an in-memory registry.
30
+ - **Resolves** resource dependencies via a multi-pass init loop, handling ordering automatically.
31
+ - **Indexes** resources by Kind and Name for constant-time lookup.
32
+ - **Dispatches** execution to the controller that owns each Kind.
33
+
34
+ Manifests also support directives for dynamic generation: `$let`, `$if`, `$for`, `$eval`, and `$include`. See [CEL-YAML Templating](./yaml-cel-templating/README.md) for documentation.
35
+
36
+ ## Example manifest
37
+
38
+ Here is an example Telo application that defines a simple HTTP API:
39
+
40
+ ```yaml
41
+ kind: Telo.Application
42
+ metadata:
43
+ name: feedback
44
+ version: 1.0.0
45
+ description: |
46
+ A complete feedback collection REST API — no code, pure YAML.
47
+ Persists entries to SQLite and serves them over HTTP.
48
+ targets:
49
+ - Migrations
50
+ - Server
51
+ ---
52
+ kind: Telo.Import
53
+ metadata:
54
+ name: Http
55
+ source: ../modules/http-server
56
+ ---
57
+ kind: Telo.Import
58
+ metadata:
59
+ name: Sql
60
+ source: ../modules/sql
61
+ ---
62
+ # SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
63
+ kind: Sql.Connection
64
+ metadata:
65
+ name: Db
66
+ driver: sqlite
67
+ file: ./tmp/feedback.db
68
+ ---
69
+ # Migrations: applied automatically before the server starts
70
+ kind: Sql.Migrations
71
+ metadata:
72
+ name: Migrations
73
+ connection:
74
+ kind: Sql.Connection
75
+ name: Db
76
+ ---
77
+ kind: Sql.Migration
78
+ metadata:
79
+ name: Migration_20260413_182154_CreateFeedback
80
+ version: 20260413_182154_CreateFeedback
81
+ sql: |
82
+ CREATE TABLE IF NOT EXISTS feedback (
83
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
84
+ text TEXT NOT NULL,
85
+ source TEXT,
86
+ score INTEGER NOT NULL DEFAULT 0,
87
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
88
+ )
89
+ ---
90
+ kind: Http.Server
91
+ metadata:
92
+ name: Server
93
+ baseUrl: http://localhost:8844
94
+ port: 8844
95
+ logger: true
96
+ openapi:
97
+ info:
98
+ title: Feedback API
99
+ version: 1.0.0
100
+ mounts:
101
+ - path: /v1
102
+ type: Http.Api.FeedbackRoutes
103
+ ---
104
+ kind: Http.Api
105
+ metadata:
106
+ name: FeedbackRoutes
107
+ routes:
108
+ # POST /v1/feedback — insert a new entry, score derived from body length heuristic
109
+ - request:
110
+ path: /feedback
111
+ method: POST
112
+ schema:
113
+ body:
114
+ type: object
115
+ properties:
116
+ text:
117
+ type: string
118
+ minLength: 1
119
+ source:
120
+ type: string
121
+ required: [text]
122
+ handler:
123
+ kind: Sql.Exec
124
+ connection:
125
+ kind: Sql.Connection
126
+ name: Db
127
+ inputs:
128
+ sql: "INSERT INTO feedback (text, source, score) VALUES (?, ?, ?)"
129
+ bindings:
130
+ - "${{ request.body.text }}"
131
+ - "${{ request.body.source }}"
132
+ - "${{ size(request.body.text) }}"
133
+ response:
134
+ - status: 201
135
+ headers:
136
+ Content-Type: application/json
137
+ body:
138
+ ok: true
139
+ message: Feedback received
140
+
141
+ # GET /v1/feedback — list all entries, newest first
142
+ - request:
143
+ path: /feedback
144
+ method: GET
145
+ handler:
146
+ kind: Sql.Select
147
+ connection:
148
+ kind: Sql.Connection
149
+ name: Db
150
+ from: feedback
151
+ columns: [id, text, source, score, created_at]
152
+ orderBy:
153
+ - { column: created_at, direction: desc }
154
+ response:
155
+ - status: 200
156
+ headers:
157
+ Content-Type: application/json
158
+ body: "${{ result.rows }}"
159
+
160
+ # GET /v1/feedback/{id} — fetch a single entry
161
+ - request:
162
+ path: /feedback/{id}
163
+ method: GET
164
+ schema:
165
+ params:
166
+ type: object
167
+ properties:
168
+ id:
169
+ type: integer
170
+ required: [id]
171
+ handler:
172
+ kind: Sql.Select
173
+ connection:
174
+ kind: Sql.Connection
175
+ name: Db
176
+ from: feedback
177
+ columns: [id, text, source, score, created_at]
178
+ where:
179
+ - { column: id, op: "=", value: "${{ request.params.id }}" }
180
+ response:
181
+ - status: 200
182
+ when: "size(result.rows) > 0"
183
+ headers:
184
+ Content-Type: application/json
185
+ body: "${{ result.rows[0] }}"
186
+ - status: 404
187
+ headers:
188
+ Content-Type: application/json
189
+ body:
190
+ ok: false
191
+ message: Not found
192
+ ```
193
+
194
+ ## Status
195
+
196
+ Telo is under **active development**. The core runtime, module system, and standard library are functional, but the API surface — including YAML shapes — may change without notice. Not yet recommended for production use.
197
+
198
+ ## The Meaning of Telo
199
+
200
+ The name Telo is derived from the Greek root Telos - meaning the "end goal", "purpose", or "final state". That is exactly the philosophy behind this runtime. In standard imperative programming, you have to write thousands of lines of code to tell a server exactly how to start. With Telo, you simply declare your desired final state.
201
+
202
+ You define the end state. Telo makes it real.
203
+
204
+ ## Philosophy
205
+
206
+ Modern platforms often spend disproportionate effort on technical mechanics-wiring frameworks, managing infrastructure, and negotiating toolchains-while the original business problem gets delayed or diluted. Telo pushes in the opposite direction: it treats kernel execution as a stable, predictable host so teams can concentrate on the **business logic and outcomes** instead of the plumbing.
207
+
208
+ By separating "what the system should do" from "how it is hosted", the runtime reduces friction for domain‑level changes. Teams can move faster on product requirements, experiment more safely, and keep conversations centered on value delivered rather than implementation trivia.
209
+
210
+ Telo also aims to **join forces across all programming language communities**, so the best ideas, patterns, and implementations can converge into a shared kernel truth without forcing everyone into a single stack.
211
+
212
+ YAML also makes the system more **AI‑friendly** than traditional programming languages: it is explicit, structured, and easier for tools to generate, review, and transform without losing intent.
213
+
214
+ ## Modularity
215
+
216
+ Telo is built around **modules** that own specific resource kinds. A module is loaded from a manifest, declares which kinds it implements, and then receives only the resources of those kinds. This keeps concerns isolated and lets teams compose systems from focused building blocks rather than monolithic services.
217
+
218
+ At kernel execution time, execution is always routed by **Kind.Name**. The kernel resolves the Kind to its owning module and hands off execution. Modules can call back into the kernel to execute other resources, enabling composition without tight coupling.
219
+
220
+ ## Architecture
221
+
222
+ The architecture is inspired by Kubernetes-style manifests: declarative resources, explicit kinds, and a control plane that routes work based on those definitions.
223
+ Those manifests were taken to the next level by allowing them to run inside a standalone runtime host.
224
+
225
+ ## See more at
226
+
227
+ - [Telo Kernel](./kernel/README.md)
228
+ - [CEL-YAML Templating](./yaml-cel-templating/README.md)
229
+ - [Telo SDK for module authors](sdk/README.md)
230
+ - [Modules](modules/README.md)
231
+
232
+ ## License
233
+
234
+ See [LICENSE](https://github.com/telorun/telo/blob/main/LICENSE).
235
+
236
+ ## Contribution Note
237
+
238
+ By contributing, you agree that code and examples in this repository may be translated or re‑implemented in other programming languages (including by AI systems) to support the project’s polyglot goals.
@@ -0,0 +1,4 @@
1
+ import type { AnalysisRegistry } from "@telorun/analyzer";
2
+ import type { CompletionResult } from "../types.js";
3
+ export declare function buildCompletions(text: string, line: number, character: number, registry: AnalysisRegistry | undefined): CompletionResult[];
4
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/completions/build.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA0BpD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,GACrC,gBAAgB,EAAE,CAMpB"}
@@ -0,0 +1,30 @@
1
+ import { detectContext } from "./detect-context.js";
2
+ import { propKeyCompletions } from "./prop-keys.js";
3
+ import { CAPABILITY_VALUES } from "./valid-capabilities.js";
4
+ function kindCompletions(registry) {
5
+ const kinds = new Set(registry
6
+ ? registry.validUserFacingKinds()
7
+ : ["Telo.Application", "Telo.Library", "Telo.Import", "Telo.Definition"]);
8
+ return Array.from(kinds).map((kind) => ({
9
+ label: kind,
10
+ kind: "class",
11
+ detail: "Telo resource kind",
12
+ }));
13
+ }
14
+ function capabilityCompletions() {
15
+ return CAPABILITY_VALUES.map((cap) => ({
16
+ label: cap,
17
+ kind: "enumMember",
18
+ detail: "Telo capability",
19
+ }));
20
+ }
21
+ export function buildCompletions(text, line, character, registry) {
22
+ const ctx = detectContext(text, line, character);
23
+ if (!ctx)
24
+ return [];
25
+ if (ctx.type === "kind")
26
+ return kindCompletions(registry);
27
+ if (ctx.type === "capability")
28
+ return capabilityCompletions();
29
+ return propKeyCompletions(ctx.docKind, ctx.yamlPath, ctx.existingKeys, registry);
30
+ }
@@ -0,0 +1,34 @@
1
+ export type CompletionCtx = {
2
+ type: "kind";
3
+ } | {
4
+ type: "capability";
5
+ } | {
6
+ type: "prop-key";
7
+ docKind: string;
8
+ yamlPath: string[];
9
+ existingKeys: Set<string>;
10
+ };
11
+ export declare function findDocBounds(lines: string[], cursorLine: number): {
12
+ start: number;
13
+ end: number;
14
+ };
15
+ export declare function extractKindFromDoc(lines: string[], start: number, end: number): string | undefined;
16
+ export declare function extractRootKeys(lines: string[], start: number, end: number): Set<string>;
17
+ /** Walk backward from cursorLine to build the chain of parent YAML keys. */
18
+ export declare function buildYamlPath(lines: string[], cursorLine: number, docStart: number, cursorIndent: number): string[];
19
+ /** Extract sibling keys already present at `indent` within the doc bounds. */
20
+ export declare function extractKeysAtIndent(lines: string[], start: number, end: number, indent: number): Set<string>;
21
+ /** Navigate a JSON Schema hierarchy following `path`, auto-descending into array items. */
22
+ export declare function navigateSchema(schema: Record<string, any>, path: string[]): Record<string, any> | undefined;
23
+ /**
24
+ * For blank lines (and lines with only whitespace), infer the intended indent
25
+ * from context rather than relying on the cursor column, which is often 0
26
+ * even when the cursor is semantically inside a nested block.
27
+ *
28
+ * Strategy: look at the previous non-empty line.
29
+ * - If it ends with `:` (bare object key, no value) → cursor is one level deeper.
30
+ * - Otherwise → cursor is a sibling of that key (same indent).
31
+ */
32
+ export declare function inferIndentForBlankLine(lines: string[], cursorLine: number, docStart: number): number;
33
+ export declare function detectContext(text: string, line: number, character: number): CompletionCtx | undefined;
34
+ //# sourceMappingURL=detect-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-context.d.ts","sourceRoot":"","sources":["../../src/completions/detect-context.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAEzF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAgBjG;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMlG;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAOxF;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,MAAM,EAAE,CA2BV;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EAAE,EACf,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GACb,GAAG,CAAC,MAAM,CAAC,CAab;AAED,2FAA2F;AAC3F,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,EAAE,GACb,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAgBjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYrG;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,aAAa,GAAG,SAAS,CAuC3B"}
@@ -0,0 +1,154 @@
1
+ export function findDocBounds(lines, cursorLine) {
2
+ let start = 0;
3
+ for (let i = cursorLine; i >= 0; i--) {
4
+ if (lines[i]?.trimEnd() === "---") {
5
+ start = i + 1;
6
+ break;
7
+ }
8
+ }
9
+ let end = lines.length;
10
+ for (let i = cursorLine + 1; i < lines.length; i++) {
11
+ if (lines[i]?.trimEnd() === "---") {
12
+ end = i;
13
+ break;
14
+ }
15
+ }
16
+ return { start, end };
17
+ }
18
+ export function extractKindFromDoc(lines, start, end) {
19
+ for (let i = start; i < end; i++) {
20
+ const m = lines[i]?.match(/^kind:\s*(\S+)/);
21
+ if (m)
22
+ return m[1];
23
+ }
24
+ return undefined;
25
+ }
26
+ export function extractRootKeys(lines, start, end) {
27
+ const keys = new Set();
28
+ for (let i = start; i < end; i++) {
29
+ const m = lines[i]?.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
30
+ if (m)
31
+ keys.add(m[1]);
32
+ }
33
+ return keys;
34
+ }
35
+ /** Walk backward from cursorLine to build the chain of parent YAML keys. */
36
+ export function buildYamlPath(lines, cursorLine, docStart, cursorIndent) {
37
+ if (cursorIndent === 0)
38
+ return [];
39
+ const path = [];
40
+ let targetIndent = cursorIndent;
41
+ for (let i = cursorLine - 1; i >= docStart; i--) {
42
+ const line = lines[i] ?? "";
43
+ const trimmed = line.trimStart();
44
+ if (trimmed === "" || trimmed.startsWith("#"))
45
+ continue;
46
+ const lineIndent = line.length - trimmed.length;
47
+ if (lineIndent < targetIndent) {
48
+ // Match a plain object key (not a list item marker)
49
+ const m = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
50
+ if (m) {
51
+ path.unshift(m[1]);
52
+ targetIndent = lineIndent;
53
+ if (lineIndent === 0)
54
+ break;
55
+ }
56
+ else {
57
+ // Hit something we can't parse (e.g. a list item `- ...`); stop
58
+ break;
59
+ }
60
+ }
61
+ }
62
+ return path;
63
+ }
64
+ /** Extract sibling keys already present at `indent` within the doc bounds. */
65
+ export function extractKeysAtIndent(lines, start, end, indent) {
66
+ const keys = new Set();
67
+ const prefix = " ".repeat(indent);
68
+ for (let i = start; i < end; i++) {
69
+ const line = lines[i] ?? "";
70
+ if (!line.startsWith(prefix))
71
+ continue;
72
+ const rest = line.slice(indent);
73
+ const m = rest.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
74
+ if (m && line.length - line.trimStart().length === indent) {
75
+ keys.add(m[1]);
76
+ }
77
+ }
78
+ return keys;
79
+ }
80
+ /** Navigate a JSON Schema hierarchy following `path`, auto-descending into array items. */
81
+ export function navigateSchema(schema, path) {
82
+ let current = schema;
83
+ for (const segment of path) {
84
+ // Auto-descend through arrays before looking up the next key
85
+ while (current.type === "array" && current.items) {
86
+ current = current.items;
87
+ }
88
+ const props = current.properties;
89
+ if (!props?.[segment])
90
+ return undefined;
91
+ current = props[segment];
92
+ }
93
+ // Auto-descend through a trailing array at the leaf (e.g. cursor inside `mounts:` items)
94
+ while (current.type === "array" && current.items) {
95
+ current = current.items;
96
+ }
97
+ return current;
98
+ }
99
+ /**
100
+ * For blank lines (and lines with only whitespace), infer the intended indent
101
+ * from context rather than relying on the cursor column, which is often 0
102
+ * even when the cursor is semantically inside a nested block.
103
+ *
104
+ * Strategy: look at the previous non-empty line.
105
+ * - If it ends with `:` (bare object key, no value) → cursor is one level deeper.
106
+ * - Otherwise → cursor is a sibling of that key (same indent).
107
+ */
108
+ export function inferIndentForBlankLine(lines, cursorLine, docStart) {
109
+ for (let i = cursorLine - 1; i >= docStart; i--) {
110
+ const line = lines[i] ?? "";
111
+ if (line.trim() === "" || line.trim().startsWith("#"))
112
+ continue;
113
+ if (line.trimEnd() === "---")
114
+ break;
115
+ const lineIndent = line.length - line.trimStart().length;
116
+ if (line.trimEnd().endsWith(":")) {
117
+ return lineIndent + 2;
118
+ }
119
+ return lineIndent;
120
+ }
121
+ return 0;
122
+ }
123
+ export function detectContext(text, line, character) {
124
+ const lines = text.split("\n");
125
+ const currentLine = lines[line] ?? "";
126
+ // Kind value completion: `kind: ` or `kind: SomePrefix`
127
+ if (/^kind:\s*\S*$/.test(currentLine)) {
128
+ return { type: "kind" };
129
+ }
130
+ const { start, end } = findDocBounds(lines, line);
131
+ const docKind = extractKindFromDoc(lines, start, end);
132
+ // Capability value completion: only inside Telo.Definition docs
133
+ if (/^capability:\s*\S*$/.test(currentLine) && docKind === "Telo.Definition") {
134
+ return { type: "capability" };
135
+ }
136
+ if (!docKind)
137
+ return undefined;
138
+ const trimmed = currentLine.trim();
139
+ // Only trigger when the line looks like a key being typed (or is blank)
140
+ const isKeyLine = trimmed === "" || /^[a-zA-Z_][a-zA-Z0-9_]*:?$/.test(trimmed);
141
+ if (!isKeyLine)
142
+ return undefined;
143
+ const indent = trimmed === ""
144
+ ? inferIndentForBlankLine(lines, line, start)
145
+ : currentLine.length - currentLine.trimStart().length;
146
+ if (indent === 0) {
147
+ return { type: "prop-key", docKind, yamlPath: [], existingKeys: extractRootKeys(lines, start, end) };
148
+ }
149
+ const yamlPath = buildYamlPath(lines, line, start, indent);
150
+ if (yamlPath.length === 0)
151
+ return undefined; // couldn't resolve parent — bail
152
+ const existingKeys = extractKeysAtIndent(lines, start, end, indent);
153
+ return { type: "prop-key", docKind, yamlPath, existingKeys };
154
+ }
@@ -0,0 +1,3 @@
1
+ export { buildCompletions } from "./build.js";
2
+ export type { CompletionCtx } from "./detect-context.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/completions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1 @@
1
+ export { buildCompletions } from "./build.js";
@@ -0,0 +1,4 @@
1
+ import type { AnalysisRegistry } from "@telorun/analyzer";
2
+ import type { CompletionResult } from "../types.js";
3
+ export declare function propKeyCompletions(kind: string, yamlPath: string[], existingKeys: Set<string>, registry: AnalysisRegistry | undefined): CompletionResult[];
4
+ //# sourceMappingURL=prop-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prop-keys.d.ts","sourceRoot":"","sources":["../../src/completions/prop-keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAAE,EAClB,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,EACzB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,GACrC,gBAAgB,EAAE,CA+CpB"}
@@ -0,0 +1,45 @@
1
+ import { navigateSchema } from "./detect-context.js";
2
+ export function propKeyCompletions(kind, yamlPath, existingKeys, registry) {
3
+ if (!registry)
4
+ return [];
5
+ const definition = registry.resolveDefinition(kind);
6
+ if (!definition?.schema)
7
+ return [];
8
+ const targetSchema = yamlPath.length === 0
9
+ ? definition.schema
10
+ : navigateSchema(definition.schema, yamlPath);
11
+ if (!targetSchema?.properties)
12
+ return [];
13
+ const required = new Set(Array.isArray(targetSchema.required) ? targetSchema.required : []);
14
+ const items = [];
15
+ for (const [prop, propSchema] of Object.entries(targetSchema.properties)) {
16
+ if (existingKeys.has(prop))
17
+ continue;
18
+ if (yamlPath.length === 0 && (prop === "kind" || prop === "metadata"))
19
+ continue;
20
+ const item = {
21
+ label: prop,
22
+ kind: "property",
23
+ insertText: `${prop}: $0`,
24
+ snippet: true,
25
+ };
26
+ const parts = [];
27
+ if (propSchema.type)
28
+ parts.push(propSchema.type);
29
+ if (propSchema.default !== undefined)
30
+ parts.push(`default: ${JSON.stringify(propSchema.default)}`);
31
+ if (parts.length)
32
+ item.detail = parts.join(" ");
33
+ if (propSchema.description)
34
+ item.documentation = propSchema.description;
35
+ if (required.has(prop)) {
36
+ item.preselect = true;
37
+ item.sortText = `0_${prop}`;
38
+ }
39
+ else {
40
+ item.sortText = `1_${prop}`;
41
+ }
42
+ items.push(item);
43
+ }
44
+ return items;
45
+ }
@@ -0,0 +1,2 @@
1
+ export declare const CAPABILITY_VALUES: readonly ["Telo.Service", "Telo.Runnable", "Telo.Invocable", "Telo.Provider", "Telo.Mount", "Telo.Type"];
2
+ //# sourceMappingURL=valid-capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"valid-capabilities.d.ts","sourceRoot":"","sources":["../../src/completions/valid-capabilities.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,0GAOpB,CAAC"}
@@ -0,0 +1,8 @@
1
+ export const CAPABILITY_VALUES = [
2
+ "Telo.Service",
3
+ "Telo.Runnable",
4
+ "Telo.Invocable",
5
+ "Telo.Provider",
6
+ "Telo.Mount",
7
+ "Telo.Type",
8
+ ];
@@ -0,0 +1,4 @@
1
+ export { normalizeDiagnostic } from "./normalize.js";
2
+ export { resolveRange } from "./range-resolver.js";
3
+ export { resolveSeverity } from "./severity.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/diagnostics/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { normalizeDiagnostic } from "./normalize.js";
2
+ export { resolveRange } from "./range-resolver.js";
3
+ export { resolveSeverity } from "./severity.js";
@@ -0,0 +1,11 @@
1
+ import type { AnalysisDiagnostic } from "@telorun/analyzer";
2
+ import type { DiagnosticContext, NormalizedDiagnostic } from "../types.js";
3
+ /** Converts a raw analyzer diagnostic into a host-ready shape:
4
+ * - Guarantees `range` and `severity`.
5
+ * - Surfaces `data.suggestedKind` (stamped by the analyzer for UNDEFINED_KIND)
6
+ * as a structured `{ kind: "replace-kind", replacement }` entry in
7
+ * `suggestions`, which editor hosts can wire into CodeActions.
8
+ * Does not rewrite the message — the analyzer already formatted the human-readable
9
+ * "Did you mean '…'?" hint, keeping CLI and IDE output in sync. */
10
+ export declare function normalizeDiagnostic(d: AnalysisDiagnostic, ctx: DiagnosticContext): NormalizedDiagnostic;
11
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../src/diagnostics/normalize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAI3E;;;;;;oEAMoE;AACpE,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,kBAAkB,EACrB,GAAG,EAAE,iBAAiB,GACrB,oBAAoB,CActB"}
@@ -0,0 +1,23 @@
1
+ import { resolveRange } from "./range-resolver.js";
2
+ import { resolveSeverity } from "./severity.js";
3
+ /** Converts a raw analyzer diagnostic into a host-ready shape:
4
+ * - Guarantees `range` and `severity`.
5
+ * - Surfaces `data.suggestedKind` (stamped by the analyzer for UNDEFINED_KIND)
6
+ * as a structured `{ kind: "replace-kind", replacement }` entry in
7
+ * `suggestions`, which editor hosts can wire into CodeActions.
8
+ * Does not rewrite the message — the analyzer already formatted the human-readable
9
+ * "Did you mean '…'?" hint, keeping CLI and IDE output in sync. */
10
+ export function normalizeDiagnostic(d, ctx) {
11
+ const suggestedKind = d.data?.suggestedKind;
12
+ const suggestions = suggestedKind
13
+ ? [{ kind: "replace-kind", replacement: suggestedKind }]
14
+ : undefined;
15
+ return {
16
+ range: resolveRange(d, ctx),
17
+ severity: resolveSeverity(d),
18
+ code: d.code !== undefined ? String(d.code) : "",
19
+ source: d.source ?? "telo",
20
+ message: d.message,
21
+ ...(suggestions ? { suggestions } : {}),
22
+ };
23
+ }
@@ -0,0 +1,10 @@
1
+ import type { AnalysisDiagnostic, Range } from "@telorun/analyzer";
2
+ import type { DiagnosticContext } from "../types.js";
3
+ /** Falls back through the chain from the VS Code extension's inline resolver
4
+ * (ide/vscode/src/extension.ts:203-216 before this package existed):
5
+ * 1. `d.range` if present.
6
+ * 2. `positionIndex.get(d.data.path)` when both are available.
7
+ * 3. Whole-line span at `sourceLine` when known.
8
+ * 4. `(0,0)-(0,0)` as a last resort. Never undefined. */
9
+ export declare function resolveRange(d: AnalysisDiagnostic, ctx: DiagnosticContext): Range;
10
+ //# sourceMappingURL=range-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"range-resolver.d.ts","sourceRoot":"","sources":["../../src/diagnostics/range-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOrD;;;;;4DAK4D;AAC5D,wBAAgB,YAAY,CAAC,CAAC,EAAE,kBAAkB,EAAE,GAAG,EAAE,iBAAiB,GAAG,KAAK,CAiBjF"}
@@ -0,0 +1,27 @@
1
+ const ZERO_RANGE = {
2
+ start: { line: 0, character: 0 },
3
+ end: { line: 0, character: 0 },
4
+ };
5
+ /** Falls back through the chain from the VS Code extension's inline resolver
6
+ * (ide/vscode/src/extension.ts:203-216 before this package existed):
7
+ * 1. `d.range` if present.
8
+ * 2. `positionIndex.get(d.data.path)` when both are available.
9
+ * 3. Whole-line span at `sourceLine` when known.
10
+ * 4. `(0,0)-(0,0)` as a last resort. Never undefined. */
11
+ export function resolveRange(d, ctx) {
12
+ if (d.range)
13
+ return d.range;
14
+ const fieldPath = d.data?.path;
15
+ if (fieldPath !== undefined && ctx.positionIndex) {
16
+ const fieldRange = ctx.positionIndex.get(fieldPath);
17
+ if (fieldRange)
18
+ return fieldRange;
19
+ }
20
+ if (ctx.sourceLine !== undefined) {
21
+ return {
22
+ start: { line: ctx.sourceLine, character: 0 },
23
+ end: { line: ctx.sourceLine, character: Number.MAX_SAFE_INTEGER },
24
+ };
25
+ }
26
+ return ZERO_RANGE;
27
+ }
@@ -0,0 +1,6 @@
1
+ import type { AnalysisDiagnostic } from "@telorun/analyzer";
2
+ import { DiagnosticSeverity } from "@telorun/analyzer";
3
+ /** Resolves a possibly-undefined analyzer severity to a concrete level.
4
+ * `Warning` is the default — matches VS Code adapter's prior inline behavior. */
5
+ export declare function resolveSeverity(d: AnalysisDiagnostic): DiagnosticSeverity;
6
+ //# sourceMappingURL=severity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"severity.d.ts","sourceRoot":"","sources":["../../src/diagnostics/severity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;kFACkF;AAClF,wBAAgB,eAAe,CAAC,CAAC,EAAE,kBAAkB,GAAG,kBAAkB,CAEzE"}
@@ -0,0 +1,6 @@
1
+ import { DiagnosticSeverity } from "@telorun/analyzer";
2
+ /** Resolves a possibly-undefined analyzer severity to a concrete level.
3
+ * `Warning` is the default — matches VS Code adapter's prior inline behavior. */
4
+ export function resolveSeverity(d) {
5
+ return d.severity ?? DiagnosticSeverity.Warning;
6
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./types.js";
2
+ export * from "./completions/index.js";
3
+ export * from "./diagnostics/index.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./completions/index.js";
3
+ export * from "./diagnostics/index.js";
@@ -0,0 +1,31 @@
1
+ export { AnalysisRegistry, DiagnosticSeverity } from "@telorun/analyzer";
2
+ export type { Position, Range, AnalysisDiagnostic, PositionIndex, } from "@telorun/analyzer";
3
+ import type { AnalysisRegistry, DiagnosticSeverity, PositionIndex, Range } from "@telorun/analyzer";
4
+ export type CompletionKind = "class" | "enumMember" | "property";
5
+ export interface CompletionResult {
6
+ label: string;
7
+ kind: CompletionKind;
8
+ detail?: string;
9
+ documentation?: string;
10
+ insertText?: string;
11
+ snippet?: boolean;
12
+ preselect?: boolean;
13
+ sortText?: string;
14
+ }
15
+ export interface NormalizedDiagnostic {
16
+ range: Range;
17
+ severity: DiagnosticSeverity;
18
+ code: string;
19
+ source: string;
20
+ message: string;
21
+ suggestions?: Array<{
22
+ kind: "replace-kind";
23
+ replacement: string;
24
+ }>;
25
+ }
26
+ export interface DiagnosticContext {
27
+ registry: AnalysisRegistry;
28
+ positionIndex?: PositionIndex;
29
+ sourceLine?: number;
30
+ }
31
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGzE,YAAY,EACV,QAAQ,EACR,KAAK,EACL,kBAAkB,EAClB,aAAa,GACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEpG,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,CAAC;AAEjE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpE;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ // Runtime values (classes, enums) — consumers who need `new AnalysisRegistry()`
2
+ // or `DiagnosticSeverity.Error` import from here.
3
+ export { AnalysisRegistry, DiagnosticSeverity } from "@telorun/analyzer";
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@telorun/ide-support",
3
+ "version": "0.2.0",
4
+ "description": "Editor-host-agnostic IDE support (completions, diagnostic normalization) for Telo manifests.",
5
+ "keywords": [
6
+ "telo",
7
+ "ide",
8
+ "completions",
9
+ "diagnostics",
10
+ "yaml"
11
+ ],
12
+ "author": "Bartosz Pasiński <bartosz.pasinski@codenet.pl>",
13
+ "license": "SEE LICENSE IN LICENSE",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/telorun/telo.git",
17
+ "directory": "packages/ide-support"
18
+ },
19
+ "homepage": "https://github.com/telorun/telo#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/telorun/telo/issues"
22
+ },
23
+ "type": "module",
24
+ "main": "./dist/index.js",
25
+ "exports": {
26
+ ".": {
27
+ "source": "./src/index.ts",
28
+ "types": "./dist/index.d.ts",
29
+ "bun": "./src/index.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist/**",
36
+ "src/**"
37
+ ],
38
+ "dependencies": {
39
+ "@telorun/analyzer": "0.3.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.0.0",
43
+ "typescript": "^5.0.0"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.lib.json"
47
+ }
48
+ }
@@ -0,0 +1,39 @@
1
+ import type { AnalysisRegistry } from "@telorun/analyzer";
2
+ import type { CompletionResult } from "../types.js";
3
+ import { detectContext } from "./detect-context.js";
4
+ import { propKeyCompletions } from "./prop-keys.js";
5
+ import { CAPABILITY_VALUES } from "./valid-capabilities.js";
6
+
7
+ function kindCompletions(registry: AnalysisRegistry | undefined): CompletionResult[] {
8
+ const kinds = new Set<string>(
9
+ registry
10
+ ? registry.validUserFacingKinds()
11
+ : ["Telo.Application", "Telo.Library", "Telo.Import", "Telo.Definition"],
12
+ );
13
+ return Array.from(kinds).map((kind) => ({
14
+ label: kind,
15
+ kind: "class",
16
+ detail: "Telo resource kind",
17
+ }));
18
+ }
19
+
20
+ function capabilityCompletions(): CompletionResult[] {
21
+ return CAPABILITY_VALUES.map((cap) => ({
22
+ label: cap,
23
+ kind: "enumMember",
24
+ detail: "Telo capability",
25
+ }));
26
+ }
27
+
28
+ export function buildCompletions(
29
+ text: string,
30
+ line: number,
31
+ character: number,
32
+ registry: AnalysisRegistry | undefined,
33
+ ): CompletionResult[] {
34
+ const ctx = detectContext(text, line, character);
35
+ if (!ctx) return [];
36
+ if (ctx.type === "kind") return kindCompletions(registry);
37
+ if (ctx.type === "capability") return capabilityCompletions();
38
+ return propKeyCompletions(ctx.docKind, ctx.yamlPath, ctx.existingKeys, registry);
39
+ }
@@ -0,0 +1,185 @@
1
+ export type CompletionCtx =
2
+ | { type: "kind" }
3
+ | { type: "capability" }
4
+ | { type: "prop-key"; docKind: string; yamlPath: string[]; existingKeys: Set<string> };
5
+
6
+ export function findDocBounds(lines: string[], cursorLine: number): { start: number; end: number } {
7
+ let start = 0;
8
+ for (let i = cursorLine; i >= 0; i--) {
9
+ if (lines[i]?.trimEnd() === "---") {
10
+ start = i + 1;
11
+ break;
12
+ }
13
+ }
14
+ let end = lines.length;
15
+ for (let i = cursorLine + 1; i < lines.length; i++) {
16
+ if (lines[i]?.trimEnd() === "---") {
17
+ end = i;
18
+ break;
19
+ }
20
+ }
21
+ return { start, end };
22
+ }
23
+
24
+ export function extractKindFromDoc(lines: string[], start: number, end: number): string | undefined {
25
+ for (let i = start; i < end; i++) {
26
+ const m = lines[i]?.match(/^kind:\s*(\S+)/);
27
+ if (m) return m[1];
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ export function extractRootKeys(lines: string[], start: number, end: number): Set<string> {
33
+ const keys = new Set<string>();
34
+ for (let i = start; i < end; i++) {
35
+ const m = lines[i]?.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
36
+ if (m) keys.add(m[1]);
37
+ }
38
+ return keys;
39
+ }
40
+
41
+ /** Walk backward from cursorLine to build the chain of parent YAML keys. */
42
+ export function buildYamlPath(
43
+ lines: string[],
44
+ cursorLine: number,
45
+ docStart: number,
46
+ cursorIndent: number,
47
+ ): string[] {
48
+ if (cursorIndent === 0) return [];
49
+
50
+ const path: string[] = [];
51
+ let targetIndent = cursorIndent;
52
+
53
+ for (let i = cursorLine - 1; i >= docStart; i--) {
54
+ const line = lines[i] ?? "";
55
+ const trimmed = line.trimStart();
56
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
57
+
58
+ const lineIndent = line.length - trimmed.length;
59
+ if (lineIndent < targetIndent) {
60
+ // Match a plain object key (not a list item marker)
61
+ const m = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
62
+ if (m) {
63
+ path.unshift(m[1]);
64
+ targetIndent = lineIndent;
65
+ if (lineIndent === 0) break;
66
+ } else {
67
+ // Hit something we can't parse (e.g. a list item `- ...`); stop
68
+ break;
69
+ }
70
+ }
71
+ }
72
+
73
+ return path;
74
+ }
75
+
76
+ /** Extract sibling keys already present at `indent` within the doc bounds. */
77
+ export function extractKeysAtIndent(
78
+ lines: string[],
79
+ start: number,
80
+ end: number,
81
+ indent: number,
82
+ ): Set<string> {
83
+ const keys = new Set<string>();
84
+ const prefix = " ".repeat(indent);
85
+ for (let i = start; i < end; i++) {
86
+ const line = lines[i] ?? "";
87
+ if (!line.startsWith(prefix)) continue;
88
+ const rest = line.slice(indent);
89
+ const m = rest.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
90
+ if (m && line.length - line.trimStart().length === indent) {
91
+ keys.add(m[1]);
92
+ }
93
+ }
94
+ return keys;
95
+ }
96
+
97
+ /** Navigate a JSON Schema hierarchy following `path`, auto-descending into array items. */
98
+ export function navigateSchema(
99
+ schema: Record<string, any>,
100
+ path: string[],
101
+ ): Record<string, any> | undefined {
102
+ let current = schema;
103
+ for (const segment of path) {
104
+ // Auto-descend through arrays before looking up the next key
105
+ while (current.type === "array" && current.items) {
106
+ current = current.items as Record<string, any>;
107
+ }
108
+ const props = current.properties as Record<string, any> | undefined;
109
+ if (!props?.[segment]) return undefined;
110
+ current = props[segment] as Record<string, any>;
111
+ }
112
+ // Auto-descend through a trailing array at the leaf (e.g. cursor inside `mounts:` items)
113
+ while (current.type === "array" && current.items) {
114
+ current = current.items as Record<string, any>;
115
+ }
116
+ return current;
117
+ }
118
+
119
+ /**
120
+ * For blank lines (and lines with only whitespace), infer the intended indent
121
+ * from context rather than relying on the cursor column, which is often 0
122
+ * even when the cursor is semantically inside a nested block.
123
+ *
124
+ * Strategy: look at the previous non-empty line.
125
+ * - If it ends with `:` (bare object key, no value) → cursor is one level deeper.
126
+ * - Otherwise → cursor is a sibling of that key (same indent).
127
+ */
128
+ export function inferIndentForBlankLine(lines: string[], cursorLine: number, docStart: number): number {
129
+ for (let i = cursorLine - 1; i >= docStart; i--) {
130
+ const line = lines[i] ?? "";
131
+ if (line.trim() === "" || line.trim().startsWith("#")) continue;
132
+ if (line.trimEnd() === "---") break;
133
+ const lineIndent = line.length - line.trimStart().length;
134
+ if (line.trimEnd().endsWith(":")) {
135
+ return lineIndent + 2;
136
+ }
137
+ return lineIndent;
138
+ }
139
+ return 0;
140
+ }
141
+
142
+ export function detectContext(
143
+ text: string,
144
+ line: number,
145
+ character: number,
146
+ ): CompletionCtx | undefined {
147
+ const lines = text.split("\n");
148
+ const currentLine = lines[line] ?? "";
149
+
150
+ // Kind value completion: `kind: ` or `kind: SomePrefix`
151
+ if (/^kind:\s*\S*$/.test(currentLine)) {
152
+ return { type: "kind" };
153
+ }
154
+
155
+ const { start, end } = findDocBounds(lines, line);
156
+ const docKind = extractKindFromDoc(lines, start, end);
157
+
158
+ // Capability value completion: only inside Telo.Definition docs
159
+ if (/^capability:\s*\S*$/.test(currentLine) && docKind === "Telo.Definition") {
160
+ return { type: "capability" };
161
+ }
162
+
163
+ if (!docKind) return undefined;
164
+
165
+ const trimmed = currentLine.trim();
166
+
167
+ // Only trigger when the line looks like a key being typed (or is blank)
168
+ const isKeyLine = trimmed === "" || /^[a-zA-Z_][a-zA-Z0-9_]*:?$/.test(trimmed);
169
+ if (!isKeyLine) return undefined;
170
+
171
+ const indent =
172
+ trimmed === ""
173
+ ? inferIndentForBlankLine(lines, line, start)
174
+ : currentLine.length - currentLine.trimStart().length;
175
+
176
+ if (indent === 0) {
177
+ return { type: "prop-key", docKind, yamlPath: [], existingKeys: extractRootKeys(lines, start, end) };
178
+ }
179
+
180
+ const yamlPath = buildYamlPath(lines, line, start, indent);
181
+ if (yamlPath.length === 0) return undefined; // couldn't resolve parent — bail
182
+
183
+ const existingKeys = extractKeysAtIndent(lines, start, end, indent);
184
+ return { type: "prop-key", docKind, yamlPath, existingKeys };
185
+ }
@@ -0,0 +1,2 @@
1
+ export { buildCompletions } from "./build.js";
2
+ export type { CompletionCtx } from "./detect-context.js";
@@ -0,0 +1,57 @@
1
+ import type { AnalysisRegistry } from "@telorun/analyzer";
2
+ import type { CompletionResult } from "../types.js";
3
+ import { navigateSchema } from "./detect-context.js";
4
+
5
+ export function propKeyCompletions(
6
+ kind: string,
7
+ yamlPath: string[],
8
+ existingKeys: Set<string>,
9
+ registry: AnalysisRegistry | undefined,
10
+ ): CompletionResult[] {
11
+ if (!registry) return [];
12
+
13
+ const definition = registry.resolveDefinition(kind);
14
+ if (!definition?.schema) return [];
15
+
16
+ const targetSchema = yamlPath.length === 0
17
+ ? (definition.schema as Record<string, any>)
18
+ : navigateSchema(definition.schema as Record<string, any>, yamlPath);
19
+
20
+ if (!targetSchema?.properties) return [];
21
+
22
+ const required = new Set<string>(
23
+ Array.isArray(targetSchema.required) ? targetSchema.required : [],
24
+ );
25
+ const items: CompletionResult[] = [];
26
+
27
+ for (const [prop, propSchema] of Object.entries(
28
+ targetSchema.properties as Record<string, any>,
29
+ )) {
30
+ if (existingKeys.has(prop)) continue;
31
+ if (yamlPath.length === 0 && (prop === "kind" || prop === "metadata")) continue;
32
+
33
+ const item: CompletionResult = {
34
+ label: prop,
35
+ kind: "property",
36
+ insertText: `${prop}: $0`,
37
+ snippet: true,
38
+ };
39
+
40
+ const parts: string[] = [];
41
+ if (propSchema.type) parts.push(propSchema.type);
42
+ if (propSchema.default !== undefined) parts.push(`default: ${JSON.stringify(propSchema.default)}`);
43
+ if (parts.length) item.detail = parts.join(" ");
44
+ if (propSchema.description) item.documentation = propSchema.description;
45
+
46
+ if (required.has(prop)) {
47
+ item.preselect = true;
48
+ item.sortText = `0_${prop}`;
49
+ } else {
50
+ item.sortText = `1_${prop}`;
51
+ }
52
+
53
+ items.push(item);
54
+ }
55
+
56
+ return items;
57
+ }
@@ -0,0 +1,8 @@
1
+ export const CAPABILITY_VALUES = [
2
+ "Telo.Service",
3
+ "Telo.Runnable",
4
+ "Telo.Invocable",
5
+ "Telo.Provider",
6
+ "Telo.Mount",
7
+ "Telo.Type",
8
+ ] as const;
@@ -0,0 +1,3 @@
1
+ export { normalizeDiagnostic } from "./normalize.js";
2
+ export { resolveRange } from "./range-resolver.js";
3
+ export { resolveSeverity } from "./severity.js";
@@ -0,0 +1,30 @@
1
+ import type { AnalysisDiagnostic } from "@telorun/analyzer";
2
+ import type { DiagnosticContext, NormalizedDiagnostic } from "../types.js";
3
+ import { resolveRange } from "./range-resolver.js";
4
+ import { resolveSeverity } from "./severity.js";
5
+
6
+ /** Converts a raw analyzer diagnostic into a host-ready shape:
7
+ * - Guarantees `range` and `severity`.
8
+ * - Surfaces `data.suggestedKind` (stamped by the analyzer for UNDEFINED_KIND)
9
+ * as a structured `{ kind: "replace-kind", replacement }` entry in
10
+ * `suggestions`, which editor hosts can wire into CodeActions.
11
+ * Does not rewrite the message — the analyzer already formatted the human-readable
12
+ * "Did you mean '…'?" hint, keeping CLI and IDE output in sync. */
13
+ export function normalizeDiagnostic(
14
+ d: AnalysisDiagnostic,
15
+ ctx: DiagnosticContext,
16
+ ): NormalizedDiagnostic {
17
+ const suggestedKind = (d.data as { suggestedKind?: string } | undefined)?.suggestedKind;
18
+ const suggestions = suggestedKind
19
+ ? [{ kind: "replace-kind" as const, replacement: suggestedKind }]
20
+ : undefined;
21
+
22
+ return {
23
+ range: resolveRange(d, ctx),
24
+ severity: resolveSeverity(d),
25
+ code: d.code !== undefined ? String(d.code) : "",
26
+ source: d.source ?? "telo",
27
+ message: d.message,
28
+ ...(suggestions ? { suggestions } : {}),
29
+ };
30
+ }
@@ -0,0 +1,32 @@
1
+ import type { AnalysisDiagnostic, Range } from "@telorun/analyzer";
2
+ import type { DiagnosticContext } from "../types.js";
3
+
4
+ const ZERO_RANGE: Range = {
5
+ start: { line: 0, character: 0 },
6
+ end: { line: 0, character: 0 },
7
+ };
8
+
9
+ /** Falls back through the chain from the VS Code extension's inline resolver
10
+ * (ide/vscode/src/extension.ts:203-216 before this package existed):
11
+ * 1. `d.range` if present.
12
+ * 2. `positionIndex.get(d.data.path)` when both are available.
13
+ * 3. Whole-line span at `sourceLine` when known.
14
+ * 4. `(0,0)-(0,0)` as a last resort. Never undefined. */
15
+ export function resolveRange(d: AnalysisDiagnostic, ctx: DiagnosticContext): Range {
16
+ if (d.range) return d.range;
17
+
18
+ const fieldPath = (d.data as { path?: string } | undefined)?.path;
19
+ if (fieldPath !== undefined && ctx.positionIndex) {
20
+ const fieldRange = ctx.positionIndex.get(fieldPath);
21
+ if (fieldRange) return fieldRange;
22
+ }
23
+
24
+ if (ctx.sourceLine !== undefined) {
25
+ return {
26
+ start: { line: ctx.sourceLine, character: 0 },
27
+ end: { line: ctx.sourceLine, character: Number.MAX_SAFE_INTEGER },
28
+ };
29
+ }
30
+
31
+ return ZERO_RANGE;
32
+ }
@@ -0,0 +1,8 @@
1
+ import type { AnalysisDiagnostic } from "@telorun/analyzer";
2
+ import { DiagnosticSeverity } from "@telorun/analyzer";
3
+
4
+ /** Resolves a possibly-undefined analyzer severity to a concrete level.
5
+ * `Warning` is the default — matches VS Code adapter's prior inline behavior. */
6
+ export function resolveSeverity(d: AnalysisDiagnostic): DiagnosticSeverity {
7
+ return d.severity ?? DiagnosticSeverity.Warning;
8
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./completions/index.js";
3
+ export * from "./diagnostics/index.js";
package/src/types.ts ADDED
@@ -0,0 +1,41 @@
1
+ // Runtime values (classes, enums) — consumers who need `new AnalysisRegistry()`
2
+ // or `DiagnosticSeverity.Error` import from here.
3
+ export { AnalysisRegistry, DiagnosticSeverity } from "@telorun/analyzer";
4
+
5
+ // Pure types.
6
+ export type {
7
+ Position,
8
+ Range,
9
+ AnalysisDiagnostic,
10
+ PositionIndex,
11
+ } from "@telorun/analyzer";
12
+
13
+ import type { AnalysisRegistry, DiagnosticSeverity, PositionIndex, Range } from "@telorun/analyzer";
14
+
15
+ export type CompletionKind = "class" | "enumMember" | "property";
16
+
17
+ export interface CompletionResult {
18
+ label: string;
19
+ kind: CompletionKind;
20
+ detail?: string;
21
+ documentation?: string;
22
+ insertText?: string;
23
+ snippet?: boolean;
24
+ preselect?: boolean;
25
+ sortText?: string;
26
+ }
27
+
28
+ export interface NormalizedDiagnostic {
29
+ range: Range;
30
+ severity: DiagnosticSeverity;
31
+ code: string;
32
+ source: string;
33
+ message: string;
34
+ suggestions?: Array<{ kind: "replace-kind"; replacement: string }>;
35
+ }
36
+
37
+ export interface DiagnosticContext {
38
+ registry: AnalysisRegistry;
39
+ positionIndex?: PositionIndex;
40
+ sourceLine?: number;
41
+ }