From 8d40cd98acaecb4a95f2b669cf7e91bad93334d9 Mon Sep 17 00:00:00 2001 From: anthonyrawlins Date: Wed, 27 Aug 2025 09:39:31 +1000 Subject: [PATCH] Initial commit - UCXL VS Code extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added UCXL VS Code extension with syntax highlighting - Implemented language configuration and grammar definitions - Created extension package with examples and documentation - Added syntax highlighting for UCXL code structures 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- LICENSE.md | 5 + README.md | 27 +++ examples/ucxl_example.py | 17 ++ examples/ucxl_example.rs | 5 + extension.js | 261 +++++++++++++++++++++++++++ language-configuration.json | 18 ++ package.json | 84 +++++++++ syntaxes/ucxl.tmLanguage.backup.json | 33 ++++ syntaxes/ucxl.tmLanguage.json | 36 ++++ ucxl-syntax-linter-0.0.1.vsix | Bin 0 -> 7737 bytes 10 files changed, 486 insertions(+) create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 examples/ucxl_example.py create mode 100644 examples/ucxl_example.rs create mode 100644 extension.js create mode 100644 language-configuration.json create mode 100644 package.json create mode 100644 syntaxes/ucxl.tmLanguage.backup.json create mode 100644 syntaxes/ucxl.tmLanguage.json create mode 100644 ucxl-syntax-linter-0.0.1.vsix diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..ecea80c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,5 @@ +MIT License + +Copyright (c) 2025 Anthony Rawlins + +Permission is hereby granted, free of charge, to any person obtaining a copy... diff --git a/README.md b/README.md new file mode 100644 index 0000000..1317301 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# UCXL Syntax + Linter + +Highlights and lints UCXL addresses anywhere in your files. + +## Features +- Syntax colouring for UCXL scheme, agent, topic, project, context, temporal segments, and path +- Inline linting for invalid UCXL syntax with red squiggly underlines +- Hover tooltips showing parsed components + +## Install +1. Clone this repo +2. Install `vsce` if you don’t have it: + ```bash + npm install -g vsce + + +Then you can see the syntax-colouring in action right here by reading this file and hovering over: + + +ucxl://AB001:dev@project:task/~~/policies/linting/details.md + + +``` + + ucxl://F0131:finance@project:any/~~/policies/linting/details.md + +``` \ No newline at end of file diff --git a/examples/ucxl_example.py b/examples/ucxl_example.py new file mode 100644 index 0000000..452f2c4 --- /dev/null +++ b/examples/ucxl_example.py @@ -0,0 +1,17 @@ +import posixpath + +def ucxl_path_join(*args): + """ + Join multiple path components into a single UCXL path. + + Args: + *args: Path components to join. + + Returns: + str: A single UCXL path. + """ + ucxl = "ucxl://AB001:dev@project:task/~~/policies/linting/details.md" + + return posixpath.join(*args) + + diff --git a/examples/ucxl_example.rs b/examples/ucxl_example.rs new file mode 100644 index 0000000..df96b3b --- /dev/null +++ b/examples/ucxl_example.rs @@ -0,0 +1,5 @@ + + +fn main() { + let context = ucxl::Context::from(ucxl://any:developer@chorus:website-redesign/#/db/connection/credentials).expect("Failed to create UCXL context"); +} \ No newline at end of file diff --git a/extension.js b/extension.js new file mode 100644 index 0000000..a8c22e7 --- /dev/null +++ b/extension.js @@ -0,0 +1,261 @@ +const vscode = require("vscode"); + +/* const UCXL_REGEX = /^ucxl:\/\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)@([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)((\/(\^\^|~~))?\/[^\\s]*)?$/; */ +// Updated regex to match the new UCXL address format with optional temporal and context path +// The regex captures: +// - Agent and Role: alphanumeric with underscores and hyphens +// - Project and Task: alphanumeric with underscores and hyphens +// - Temporal: optional, can be `#`, `~*`, `^*`, `~~`, `^^`, `~`, or `^` +// - Context Path: optional, can be any string after the temporal part +// The regex ensures that the address starts with `ucxl://` and is followed by the correct structure of components. +// The regex is designed to be flexible while ensuring that the components are valid according to the UCXL specification. +// The regex also allows for the context path to be any valid string, ensuring it captures the full address correctly. +// The regex is designed to be used in a VSCode extension for validating and providing hover information +// about UCXL addresses in text documents. +//const UCXL_REGEX = /^ucxl:\/\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)@([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)\/(#|~\*|\^\*|~~|\^\^|~\d+|\^\d+)\/(.+)$/; + +// Agent ID regex for UCXL addresses +const UCXL_REGEX = /^ucxl:\/\/([A-Z0-9]{3,5}|any|none):([a-zA-Z0-9_-]+)@([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)\/(#|~\*|\^\*|~~|\^\^|~\d+|\^\d+)\/(.+)$/; + +function activate(context) { + const diagnosticCollection = vscode.languages.createDiagnosticCollection("ucxl"); + context.subscriptions.push(diagnosticCollection); + + function updateDiagnostics(doc) { + if (!doc) return; + const diagnostics = []; + const text = doc.getText(); + const lines = text.split(/\r?\n/); + + lines.forEach((line, idx) => { + const matches = line.matchAll(/ucxl:\/\/[^\s]+/g); + for (const match of matches) { + const addr = match[0]; + if (!UCXL_REGEX.test(addr)) { + diagnostics.push( + new vscode.Diagnostic( + new vscode.Range(idx, match.index, idx, match.index + addr.length), + "Invalid UCXL address syntax", + vscode.DiagnosticSeverity.Error + ) + ); + } + } + }); + + diagnosticCollection.set(doc.uri, diagnostics); + } + + vscode.workspace.onDidOpenTextDocument(updateDiagnostics); + vscode.workspace.onDidChangeTextDocument(e => updateDiagnostics(e.document)); + vscode.workspace.onDidCloseTextDocument(doc => diagnosticCollection.delete(doc.uri)); + + // Hover Provider + const hoverProvider = vscode.languages.registerHoverProvider( + { scheme: "file", language: "*" }, + { + provideHover(document, position) { + const range = document.getWordRangeAtPosition(position, /ucxl:\/\/[^\s]+/); + if (!range) return; + const address = document.getText(range); + + const match = address.match(UCXL_REGEX); + if (!match) return new vscode.Hover(`$(error) **Invalid UCXL address**`); + + const [, agent, role, project, task, , temporal, , context_path] = match; + const md = new vscode.MarkdownString(); + md.isTrusted = true; + md.appendMarkdown(`### UCXL Address Components\n`); + md.appendMarkdown(`- **Agent**: \`${agent}\`\n`); + md.appendMarkdown(`- **Role**: \`${role}\`\n`); + md.appendMarkdown(`- **Project**: \`${project}\`\n`); + md.appendMarkdown(`- **Task**: \`${task}\`\n`); + if (temporal) md.appendMarkdown(`- **Temporal**: \`${temporal}\`\n`); + if (context_path) md.appendMarkdown(`- **Context Path**: \`${context_path}\`\n`); + + return new vscode.Hover(md, range); + } + } + ); + + context.subscriptions.push(hoverProvider); +} + +function lintUcxlLine(lineText, lineNum, diagnostics) { + const match = lineText.match(UCXL_REGEX); + if (!match) return; + + const fullAddress = match[0]; + const agentToken = match[1]; + + if (!isValidAgent(agentToken)) { + const agentStart = lineText.indexOf(agentToken); + const agentEnd = agentStart + agentToken.length; + diagnostics.push( + new vscode.Diagnostic( + new vscode.Range(lineNum, agentStart, lineNum, agentEnd), + "Invalid agent token (checksum or field error)", + vscode.DiagnosticSeverity.Error + ) + ); + } +} + +function updateDiagnostics(doc, diagnosticCollection) { + const diagnostics = []; + for (let i = 0; i < doc.lineCount; i++) { + const lineText = doc.lineAt(i).text; + lintUcxlLine(lineText, i, diagnostics); + } + diagnosticCollection.set(doc.uri, diagnostics); +} + + +vscode.languages.registerHoverProvider("ucxl", { + provideHover(document, position) { + const wordRange = document.getWordRangeAtPosition(position, /[A-Z0-9]{5}/); + if (!wordRange) return null; + + const token = document.getText(wordRange); + if (!isValidAgent(token)) return null; + + const agentId = decodeToken(token); + return new vscode.Hover(`**AgentID**: Version=${agentId.version}, Host=${agentId.hostId}, GPU=${agentId.gpuSlot}`); + } +}); + + + +const crypto = require("crypto"); + +const CROCKFORD_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; +const VERSION_BITS = 3; +const HOST_ID_BITS = 10; +const GPU_SLOT_BITS = 4; +const RESERVED_BITS = 2; +const CHECKSUM_BITS = 6; + +const PREFIX_BITS = VERSION_BITS + HOST_ID_BITS + GPU_SLOT_BITS + RESERVED_BITS; // 19 +const TOTAL_BITS = PREFIX_BITS + CHECKSUM_BITS; // 25 + +const MAX_HOST_ID = (1 << HOST_ID_BITS) - 1; +const MAX_GPU_SLOT = (1 << GPU_SLOT_BITS) - 1; + +// ------------------- +// Base32 helpers +// ------------------- +function intToBase32(n, length) { + let chars = Array(length).fill('0'); + for (let i = length - 1; i >= 0; i--) { + chars[i] = CROCKFORD_ALPHABET[n & 0x1F]; + n >>= 5; + } + return chars.join(''); +} + +function base32ToInt(s) { + const charMap = {}; + for (let i = 0; i < CROCKFORD_ALPHABET.length; i++) { + charMap[CROCKFORD_ALPHABET[i]] = i; + } + + if (s.length !== 5) throw new Error(`token length must be 5, got ${s.length}`); + + let n = 0; + for (let ch of s.toUpperCase()) { + if ("ILOU".includes(ch)) throw new Error(`invalid character ${ch}`); + const val = charMap[ch]; + if (val === undefined) throw new Error(`invalid character ${ch}`); + n = (n << 5) | val; + } + return n; +} + +// ------------------- +// SHA256 first bits +// ------------------- +function sha256FirstBits(value, bits) { + const bytes = Buffer.from([(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]); + const hash = crypto.createHash('sha256').update(bytes).digest(); + return hash[0] >> (8 - bits); +} + +// ------------------- +// Pack/Unpack +// ------------------- +function packFields(version, hostId, gpuSlot, reserved) { + if (version >= (1 << VERSION_BITS)) throw new Error("version out of range"); + if (hostId > MAX_HOST_ID) throw new Error("host_id out of range"); + if (gpuSlot > MAX_GPU_SLOT) throw new Error("gpu_slot out of range"); + if (reserved >= (1 << RESERVED_BITS)) throw new Error("reserved out of range"); + + let bits = 0; + bits = (bits << VERSION_BITS) | version; + bits = (bits << HOST_ID_BITS) | hostId; + bits = (bits << GPU_SLOT_BITS) | gpuSlot; + bits = (bits << RESERVED_BITS) | reserved; + + const checksum = sha256FirstBits(bits, CHECKSUM_BITS); + bits = (bits << CHECKSUM_BITS) | checksum; + + if (bits >= (1 << TOTAL_BITS)) throw new Error("packed value exceeds allowed bit length"); + return bits; +} + +function unpackFields(packed) { + if (packed >= (1 << TOTAL_BITS)) throw new Error("packed value exceeds allowed bit length"); + + const checksum = packed & ((1 << CHECKSUM_BITS) - 1); + const prefix = packed >> CHECKSUM_BITS; + + let tmp = prefix; + const reserved = tmp & ((1 << RESERVED_BITS) - 1); tmp >>= RESERVED_BITS; + const gpuSlot = tmp & ((1 << GPU_SLOT_BITS) - 1); tmp >>= GPU_SLOT_BITS; + const hostId = tmp & ((1 << HOST_ID_BITS) - 1); tmp >>= HOST_ID_BITS; + const version = tmp & ((1 << VERSION_BITS) - 1); + + const expected = sha256FirstBits(prefix, CHECKSUM_BITS); + if (expected !== checksum) throw new Error("checksum mismatch"); + + return { version, hostId, gpuSlot, reserved, checksum }; +} + +// ------------------- +// Encode / Decode Token +// ------------------- +function encodeToken(version, hostId, gpuSlot, reserved) { + const packed = packFields(version, hostId, gpuSlot, reserved); + return intToBase32(packed, 5); +} + +function decodeToken(token) { + const packed = base32ToInt(token); + return unpackFields(packed); +} + +// ------------------- +// AgentID validator +// ------------------- +function isValidAgent(agent) { + try { + decodeToken(agent); + return true; + } catch (e) { + return false; + } +} + +// Example usage: +// console.log(isValidAgent(encodeToken(1, 42, 3, 0))); // true +// console.log(isValidAgent("ABCDE")); // false if checksum fails + +function isValidUCXLAddress(address) { + return UCXL_REGEX.test(address); +} + + +function deactivate() {} + + + +module.exports = { activate, deactivate }; diff --git a/language-configuration.json b/language-configuration.json new file mode 100644 index 0000000..8365e55 --- /dev/null +++ b/language-configuration.json @@ -0,0 +1,18 @@ +{ + "comments": { + "lineComment": "#", + "blockComment": ["/*", "*/"] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "\"", "close": "\"" }, + { "open": "'", "close": "'" } + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..90a1338 --- /dev/null +++ b/package.json @@ -0,0 +1,84 @@ +{ + "name": "ucxl-syntax-linter", + "displayName": "UCXL Syntax + Linter", + "description": "Syntax highlighting and linting for UCXL addresses inline in any file.", + "version": "0.0.1", + "publisher": "Anthony Rawlins - CHORUS.services", + "repository": { + "type": "git", + "url": "https://github.com/anthonyrawlins/ucxl-vscode-linter.git" + }, + "engines": { + "vscode": "^1.70.0" + }, + "categories": [ + "Linters", + "Programming Languages" + ], + "activationEvents": [ + "onLanguage:ucxl", + "onLanguage:go", + "onLanguage:rust", + "onLanguage:javascript", + "onLanguage:python", + "onLanguage:markdown" + ], + "main": "./extension.js", + "contributes": { + "languages": [ + { + "id": "ucxl", + "aliases": ["UCXL"], + "extensions": [".ucxl"] + } + ], + "grammars": [ + { + "language": "ucxl", + "scopeName": "source.ucxl", + "path": "./syntaxes/ucxl.tmLanguage.json", + "injectTo": [ + "source", + "text", + "source.go", + "source.rust", + "source.js", + "source.py" + ] + }, + { + "language": "go", + "scopeName": "source.go.ucxl", + "path": "./syntaxes/ucxl.tmLanguage.json", + "injectTo": ["source.go"] + }, + { + "language": "rust", + "scopeName": "source.rust.ucxl", + "path": "./syntaxes/ucxl.tmLanguage.json", + "injectTo": ["source.rust"] + }, + { + "language": "javascript", + "scopeName": "source.js.ucxl", + "path": "./syntaxes/ucxl.tmLanguage.json", + "injectTo": ["source.js"] + }, + { + "language": "python", + "scopeName": "source.python.ucxl", + "path": "./syntaxes/ucxl.tmLanguage.json", + "injectTo": ["source.py"] + }, + { + "language": "markdown", + "scopeName": "source.markdown.ucxl", + "path": "./syntaxes/ucxl.tmLanguage.json", + "injectTo": ["source.md"] + } + ] + }, + "scripts": { + "package": "vsce package" + } +} diff --git a/syntaxes/ucxl.tmLanguage.backup.json b/syntaxes/ucxl.tmLanguage.backup.json new file mode 100644 index 0000000..bfe83a7 --- /dev/null +++ b/syntaxes/ucxl.tmLanguage.backup.json @@ -0,0 +1,33 @@ +{ + "scopeName": "source.ucxl", + "patterns": [ + { + "name": "keyword.scheme.ucxl", + "match": "\\bucxl://" + }, + { + "name": "entity.name.agent.ucxl", + "match": "(?<=ucxl://)[a-zA-Z0-9_-]+(?=:)" + }, + { + "name": "support.class.topic.ucxl", + "match": "(?<=:)[a-zA-Z0-9_-]+(?=@)" + }, + { + "name": "entity.name.project.ucxl", + "match": "(?<=@)[a-zA-Z0-9_-]+(?=:)" + }, + { + "name": "variable.parameter.context.ucxl", + "match": "(?<=:)[a-zA-Z0-9_-]+(?=/)" + }, + { + "name": "constant.language.temporal.ucxl", + "match": "(?<=/)(#|~\\*|\\^\\*|~~|\\^\\^|~\\d+|\\^\\d+)(?=/)" + }, + { + "name": "string.path.ucxl", + "match": "(?<=/)[^\\s]+" + } + ] +} diff --git a/syntaxes/ucxl.tmLanguage.json b/syntaxes/ucxl.tmLanguage.json new file mode 100644 index 0000000..9c07473 --- /dev/null +++ b/syntaxes/ucxl.tmLanguage.json @@ -0,0 +1,36 @@ +{ + "scopeName": "source.ucxl", + "name": "UCXL", + "patterns": [ + { + "name": "meta.ucxl", + "match": "^ucxl://([A-Z0-9]{5})" , + "captures": { + "1": { "name": "constant.language.agent.ucxl" } + } + }, + { + "match": "^ucxl://[A-Z0-9]{5}:([a-zA-Z0-9_-]+)", + "captures": { + "1": { "name": "entity.name.role.ucxl" } + } + }, + { + "match": "@([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)", + "captures": { + "1": { "name": "entity.name.project.ucxl" }, + "2": { "name": "entity.name.task.ucxl" } + } + }, + { + "match": "\\/(#|~\\*|\\^\\*|~~|\\^\\^|~\\d+|\\^\\d+)\\/", + "captures": { + "1": { "name": "constant.language.temporal.ucxl" } + } + }, + { + "match": "\\/([^\\s]+)$", + "name": "string.path.ucxl" + } + ] +} diff --git a/ucxl-syntax-linter-0.0.1.vsix b/ucxl-syntax-linter-0.0.1.vsix new file mode 100644 index 0000000000000000000000000000000000000000..7da688ffd5c88f531066cb6ff19b7a2c67ecef89 GIT binary patch literal 7737 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 a>?d+j1i*r)83F