commit 8d40cd98acaecb4a95f2b669cf7e91bad93334d9 Author: anthonyrawlins Date: Wed Aug 27 09:39:31 2025 +1000 Initial commit - UCXL VS Code extension - 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 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 0000000..7da688f Binary files /dev/null and b/ucxl-syntax-linter-0.0.1.vsix differ