- 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>
262 lines
9.2 KiB
JavaScript
262 lines
9.2 KiB
JavaScript
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 };
|