- Add full Telegram bot functionality with Z.AI API integration
- Implement 4 tools: Bash, FileEdit, WebSearch, Git
- Add 3 agents: Code Reviewer, Architect, DevOps Engineer
- Add 6 skills for common coding tasks
- Add systemd service file for 24/7 operation
- Add nginx configuration for HTTPS webhook
- Add comprehensive documentation
- Implement WebSocket server for real-time updates
- Add logging system with Winston
- Add environment validation
🤖 zCode CLI X - Agentic coder with Z.AI + Telegram integration
232 lines
6.6 KiB
JavaScript
232 lines
6.6 KiB
JavaScript
/**
|
|
* Expression - Parses and stores a tag pattern expression
|
|
*
|
|
* Patterns are parsed once and stored in an optimized structure for fast matching.
|
|
*
|
|
* @example
|
|
* const expr = new Expression("root.users.user");
|
|
* const expr2 = new Expression("..user[id]:first");
|
|
* const expr3 = new Expression("root/users/user", { separator: '/' });
|
|
*/
|
|
export default class Expression {
|
|
/**
|
|
* Create a new Expression
|
|
* @param {string} pattern - Pattern string (e.g., "root.users.user", "..user[id]")
|
|
* @param {Object} options - Configuration options
|
|
* @param {string} options.separator - Path separator (default: '.')
|
|
*/
|
|
constructor(pattern, options = {}) {
|
|
this.pattern = pattern;
|
|
this.separator = options.separator || '.';
|
|
this.segments = this._parse(pattern);
|
|
|
|
// Cache expensive checks for performance (O(1) instead of O(n))
|
|
this._hasDeepWildcard = this.segments.some(seg => seg.type === 'deep-wildcard');
|
|
this._hasAttributeCondition = this.segments.some(seg => seg.attrName !== undefined);
|
|
this._hasPositionSelector = this.segments.some(seg => seg.position !== undefined);
|
|
}
|
|
|
|
/**
|
|
* Parse pattern string into segments
|
|
* @private
|
|
* @param {string} pattern - Pattern to parse
|
|
* @returns {Array} Array of segment objects
|
|
*/
|
|
_parse(pattern) {
|
|
const segments = [];
|
|
|
|
// Split by separator but handle ".." specially
|
|
let i = 0;
|
|
let currentPart = '';
|
|
|
|
while (i < pattern.length) {
|
|
if (pattern[i] === this.separator) {
|
|
// Check if next char is also separator (deep wildcard)
|
|
if (i + 1 < pattern.length && pattern[i + 1] === this.separator) {
|
|
// Flush current part if any
|
|
if (currentPart.trim()) {
|
|
segments.push(this._parseSegment(currentPart.trim()));
|
|
currentPart = '';
|
|
}
|
|
// Add deep wildcard
|
|
segments.push({ type: 'deep-wildcard' });
|
|
i += 2; // Skip both separators
|
|
} else {
|
|
// Regular separator
|
|
if (currentPart.trim()) {
|
|
segments.push(this._parseSegment(currentPart.trim()));
|
|
}
|
|
currentPart = '';
|
|
i++;
|
|
}
|
|
} else {
|
|
currentPart += pattern[i];
|
|
i++;
|
|
}
|
|
}
|
|
|
|
// Flush remaining part
|
|
if (currentPart.trim()) {
|
|
segments.push(this._parseSegment(currentPart.trim()));
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
/**
|
|
* Parse a single segment
|
|
* @private
|
|
* @param {string} part - Segment string (e.g., "user", "ns::user", "user[id]", "ns::user:first")
|
|
* @returns {Object} Segment object
|
|
*/
|
|
_parseSegment(part) {
|
|
const segment = { type: 'tag' };
|
|
|
|
// NEW NAMESPACE SYNTAX (v2.0):
|
|
// ============================
|
|
// Namespace uses DOUBLE colon (::)
|
|
// Position uses SINGLE colon (:)
|
|
//
|
|
// Examples:
|
|
// "user" → tag
|
|
// "user:first" → tag + position
|
|
// "user[id]" → tag + attribute
|
|
// "user[id]:first" → tag + attribute + position
|
|
// "ns::user" → namespace + tag
|
|
// "ns::user:first" → namespace + tag + position
|
|
// "ns::user[id]" → namespace + tag + attribute
|
|
// "ns::user[id]:first" → namespace + tag + attribute + position
|
|
// "ns::first" → namespace + tag named "first" (NO ambiguity!)
|
|
//
|
|
// This eliminates all ambiguity:
|
|
// :: = namespace separator
|
|
// : = position selector
|
|
// [] = attributes
|
|
|
|
// Step 1: Extract brackets [attr] or [attr=value]
|
|
let bracketContent = null;
|
|
let withoutBrackets = part;
|
|
|
|
const bracketMatch = part.match(/^([^\[]+)(\[[^\]]*\])(.*)$/);
|
|
if (bracketMatch) {
|
|
withoutBrackets = bracketMatch[1] + bracketMatch[3];
|
|
if (bracketMatch[2]) {
|
|
const content = bracketMatch[2].slice(1, -1);
|
|
if (content) {
|
|
bracketContent = content;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 2: Check for namespace (double colon ::)
|
|
let namespace = undefined;
|
|
let tagAndPosition = withoutBrackets;
|
|
|
|
if (withoutBrackets.includes('::')) {
|
|
const nsIndex = withoutBrackets.indexOf('::');
|
|
namespace = withoutBrackets.substring(0, nsIndex).trim();
|
|
tagAndPosition = withoutBrackets.substring(nsIndex + 2).trim(); // Skip ::
|
|
|
|
if (!namespace) {
|
|
throw new Error(`Invalid namespace in pattern: ${part}`);
|
|
}
|
|
}
|
|
|
|
// Step 3: Parse tag and position (single colon :)
|
|
let tag = undefined;
|
|
let positionMatch = null;
|
|
|
|
if (tagAndPosition.includes(':')) {
|
|
const colonIndex = tagAndPosition.lastIndexOf(':'); // Use last colon for position
|
|
const tagPart = tagAndPosition.substring(0, colonIndex).trim();
|
|
const posPart = tagAndPosition.substring(colonIndex + 1).trim();
|
|
|
|
// Verify position is a valid keyword
|
|
const isPositionKeyword = ['first', 'last', 'odd', 'even'].includes(posPart) ||
|
|
/^nth\(\d+\)$/.test(posPart);
|
|
|
|
if (isPositionKeyword) {
|
|
tag = tagPart;
|
|
positionMatch = posPart;
|
|
} else {
|
|
// Not a valid position keyword, treat whole thing as tag
|
|
tag = tagAndPosition;
|
|
}
|
|
} else {
|
|
tag = tagAndPosition;
|
|
}
|
|
|
|
if (!tag) {
|
|
throw new Error(`Invalid segment pattern: ${part}`);
|
|
}
|
|
|
|
segment.tag = tag;
|
|
if (namespace) {
|
|
segment.namespace = namespace;
|
|
}
|
|
|
|
// Step 4: Parse attributes
|
|
if (bracketContent) {
|
|
if (bracketContent.includes('=')) {
|
|
const eqIndex = bracketContent.indexOf('=');
|
|
segment.attrName = bracketContent.substring(0, eqIndex).trim();
|
|
segment.attrValue = bracketContent.substring(eqIndex + 1).trim();
|
|
} else {
|
|
segment.attrName = bracketContent.trim();
|
|
}
|
|
}
|
|
|
|
// Step 5: Parse position selector
|
|
if (positionMatch) {
|
|
const nthMatch = positionMatch.match(/^nth\((\d+)\)$/);
|
|
if (nthMatch) {
|
|
segment.position = 'nth';
|
|
segment.positionValue = parseInt(nthMatch[1], 10);
|
|
} else {
|
|
segment.position = positionMatch;
|
|
}
|
|
}
|
|
|
|
return segment;
|
|
}
|
|
|
|
/**
|
|
* Get the number of segments
|
|
* @returns {number}
|
|
*/
|
|
get length() {
|
|
return this.segments.length;
|
|
}
|
|
|
|
/**
|
|
* Check if expression contains deep wildcard
|
|
* @returns {boolean}
|
|
*/
|
|
hasDeepWildcard() {
|
|
return this._hasDeepWildcard;
|
|
}
|
|
|
|
/**
|
|
* Check if expression has attribute conditions
|
|
* @returns {boolean}
|
|
*/
|
|
hasAttributeCondition() {
|
|
return this._hasAttributeCondition;
|
|
}
|
|
|
|
/**
|
|
* Check if expression has position selectors
|
|
* @returns {boolean}
|
|
*/
|
|
hasPositionSelector() {
|
|
return this._hasPositionSelector;
|
|
}
|
|
|
|
/**
|
|
* Get string representation
|
|
* @returns {string}
|
|
*/
|
|
toString() {
|
|
return this.pattern;
|
|
}
|
|
} |