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 <noreply@anthropic.com>
This commit is contained in:
261
extension.js
Normal file
261
extension.js
Normal file
@@ -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 `#`, `~*`, `^*`, `~~`, `^^`, `~<number>`, or `^<number>`
|
||||
// - 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 };
|
||||
Reference in New Issue
Block a user