feat: Complete zCode CLI X with Telegram bot integration

- 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
This commit is contained in:
admin
2026-05-05 09:01:26 +00:00
Unverified
parent 4a7035dd92
commit 875c7f9b91
24688 changed files with 3224957 additions and 221 deletions

View File

@@ -0,0 +1,232 @@
import type { GrowthBook } from "../GrowthBook";
import type {
UserScopedGrowthBook,
GrowthBookClient,
} from "../GrowthBookClient";
export type AutoAttributeSettings = {
uuidCookieName?: string;
uuidKey?: string;
uuid?: string;
uuidAutoPersist?: boolean;
};
function getBrowserDevice(ua: string): { browser: string; deviceType: string } {
const browser = ua.match(/Edg/)
? "edge"
: ua.match(/Chrome/)
? "chrome"
: ua.match(/Firefox/)
? "firefox"
: ua.match(/Safari/)
? "safari"
: "unknown";
const deviceType = ua.match(/Mobi/) ? "mobile" : "desktop";
return { browser, deviceType };
}
function getURLAttributes(url: URL | Location | undefined) {
if (!url) return {};
return {
url: url.href,
path: url.pathname,
host: url.host,
query: url.search,
};
}
export function autoAttributesPlugin(settings: AutoAttributeSettings = {}) {
// Browser only
if (typeof window === "undefined") {
throw new Error("autoAttributesPlugin only works in the browser");
}
const COOKIE_NAME = settings.uuidCookieName || "gbuuid";
const uuidKey = settings.uuidKey || "id";
let uuid = settings.uuid || "";
function persistUUID() {
setCookie(COOKIE_NAME, uuid);
}
function getUUID() {
// Already stored in memory, return
if (uuid) return uuid;
// If cookie is already set, return
uuid = getCookie(COOKIE_NAME);
if (uuid) return uuid;
// Generate a new UUID
uuid = genUUID(window.crypto);
return uuid;
}
// Listen for a custom event to persist the UUID cookie
document.addEventListener("growthbookpersist", () => {
persistUUID();
});
function getAutoAttributes(settings: AutoAttributeSettings) {
const ua = navigator.userAgent;
const _uuid = getUUID();
// If a uuid is provided, default persist to false, otherwise default to true
if (settings.uuidAutoPersist ?? !settings.uuid) {
persistUUID();
}
const url = location;
return {
...getDataLayerVariables(),
[uuidKey]: _uuid,
...getURLAttributes(url),
pageTitle: document.title,
...getBrowserDevice(ua),
...getUtmAttributes(url),
};
}
return (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => {
// Only works for instances with user attributes
if ("createScopedInstance" in gb) {
return;
}
// Set initial attributes
const attributes = getAutoAttributes(settings);
attributes.url && gb.setURL(attributes.url);
gb.updateAttributes(attributes);
// Poll for URL changes and update GrowthBook
let currentUrl = attributes.url;
const intervalTimer = setInterval(() => {
if (location.href !== currentUrl) {
currentUrl = location.href;
gb.setURL(currentUrl);
gb.updateAttributes(getAutoAttributes(settings));
}
}, 500);
// Listen for a custom event to update URL and attributes
const refreshListener = () => {
if (location.href !== currentUrl) {
currentUrl = location.href;
gb.setURL(currentUrl);
}
gb.updateAttributes(getAutoAttributes(settings));
};
document.addEventListener("growthbookrefresh", refreshListener);
if ("onDestroy" in gb) {
gb.onDestroy(() => {
clearInterval(intervalTimer);
document.removeEventListener("growthbookrefresh", refreshListener);
});
}
};
}
function setCookie(name: string, value: string) {
const d = new Date();
const COOKIE_DAYS = 400; // 400 days is the max cookie duration for chrome
d.setTime(d.getTime() + 24 * 60 * 60 * 1000 * COOKIE_DAYS);
document.cookie = name + "=" + value + ";path=/;expires=" + d.toUTCString();
}
function getCookie(name: string): string {
const value = "; " + document.cookie;
const parts = value.split(`; ${name}=`);
return parts.length === 2 ? parts[1].split(";")[0] : "";
}
// Use the browsers crypto.randomUUID if set to generate a UUID
function genUUID(crypto?: Crypto) {
if (crypto && crypto.randomUUID) return crypto.randomUUID();
return ("" + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => {
const n =
crypto && crypto.getRandomValues
? crypto.getRandomValues(new Uint8Array(1))[0]
: Math.floor(Math.random() * 256);
return (
(c as unknown as number) ^
(n & (15 >> ((c as unknown as number) / 4)))
).toString(16);
});
}
function getUtmAttributes(url: URL | Location | undefined) {
// Store utm- params in sessionStorage for future page loads
let utms: Record<string, string> = {};
try {
const existing = sessionStorage.getItem("utm_params");
if (existing) {
utms = JSON.parse(existing);
}
} catch (e) {
// Do nothing if sessionStorage is disabled (e.g. incognito window)
}
// Add utm params from querystring
if (url && url.search) {
const params = new URLSearchParams(url.search);
let hasChanges = false;
["source", "medium", "campaign", "term", "content"].forEach((k) => {
// Querystring is in snake_case
const param = `utm_${k}`;
// Attribute keys are camelCase
const attr = `utm` + k[0].toUpperCase() + k.slice(1);
if (params.has(param)) {
utms[attr] = params.get(param) || "";
hasChanges = true;
}
});
// Write back to sessionStorage
if (hasChanges) {
try {
sessionStorage.setItem("utm_params", JSON.stringify(utms));
} catch (e) {
// Do nothing if sessionStorage is disabled (e.g. incognito window)
}
}
}
return utms;
}
function getDataLayerVariables() {
if (
typeof window === "undefined" ||
!window.dataLayer ||
!window.dataLayer.forEach
) {
return {};
}
const obj: Record<string, unknown> = {};
window.dataLayer.forEach((item: unknown) => {
// Skip empty and non-object entries
if (!item || typeof item !== "object" || "length" in item) return;
// Skip events
if ("event" in item) return;
Object.keys(item).forEach((k) => {
// Filter out known properties that aren't useful
if (typeof k !== "string" || k.match(/^(gtm)/)) return;
const val = (item as Record<string, unknown>)[k];
// Only add primitive variable values
const valueType = typeof val;
if (["string", "number", "boolean"].includes(valueType)) {
obj[k] = val;
}
});
});
return obj;
}

View File

@@ -0,0 +1,244 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { GrowthBook } from "../GrowthBook";
import {
Attributes,
FeatureApiResponse,
LogUnion,
Plugin,
} from "../types/growthbook";
import { GrowthBookClient, UserScopedGrowthBook } from "../GrowthBookClient";
export type DevtoolsState = {
attributes?: Record<string, any>;
features?: Record<string, any>;
experiments?: Record<string, number>;
};
export interface NextjsReadonlyRequestCookiesCompat {
get: (name: string) => { name: string; value: string } | undefined;
}
export interface NextjsRequestCompat {
nextUrl: {
searchParams: URLSearchParams;
};
cookies: {
get: (name: string) => { name: string; value: string } | undefined;
};
}
export interface ExpressRequestCompat {
cookies: Record<string, string | string[]>;
query: Record<string, string>;
[key: string]: unknown;
}
function applyDevtoolsState(
devtoolsState: DevtoolsState,
gb: GrowthBook | UserScopedGrowthBook,
) {
// Only enable in dev mode
if (!gb.inDevMode()) {
return;
}
if (
devtoolsState.attributes &&
typeof devtoolsState.attributes === "object"
) {
gb.setAttributeOverrides(devtoolsState.attributes);
}
if (devtoolsState.features && typeof devtoolsState.features === "object") {
const map = new Map(Object.entries(devtoolsState.features));
gb.setForcedFeatures(map);
}
if (
devtoolsState.experiments &&
typeof devtoolsState.experiments === "object"
) {
gb.setForcedVariations(devtoolsState.experiments);
}
}
export function devtoolsPlugin(devtoolsState?: DevtoolsState): Plugin {
return (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => {
// Only works for user-scoped GrowthBook instances
if ("createScopedInstance" in gb) {
throw new Error(
"devtoolsPlugin can only be set on a user-scoped instance",
);
}
if (devtoolsState) {
applyDevtoolsState(devtoolsState, gb);
}
};
}
/**
* For NextJS environments.
* When using server components, use the `searchParams` and `requestCookies` fields.
* - Note: In NextJS 15+, you should await these values before passing them to the plugin
* When using middleware / api routes, provide the `request` field instead.
*/
export function devtoolsNextjsPlugin({
searchParams,
requestCookies,
request,
}: {
searchParams?: { _gbdebug?: string };
requestCookies?: NextjsReadonlyRequestCookiesCompat;
request?: NextjsRequestCompat;
}): Plugin {
function extractGbDebugPayload({
searchParams,
requestCookies,
}: {
searchParams?: { _gbdebug?: string } | URLSearchParams;
requestCookies?: NextjsReadonlyRequestCookiesCompat;
}): string | undefined {
if (searchParams) {
if ("_gbdebug" in searchParams) {
return searchParams._gbdebug;
}
if (searchParams instanceof URLSearchParams) {
return searchParams.get("_gbdebug") ?? undefined;
}
}
return requestCookies?.get("_gbdebug")?.value;
}
return (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => {
let payload = extractGbDebugPayload({ searchParams, requestCookies });
if (!payload && request) {
payload = extractGbDebugPayload({
searchParams: request.nextUrl.searchParams,
requestCookies: request.cookies,
});
}
let state: DevtoolsState = {};
if (payload) {
try {
state = JSON.parse(payload);
} catch (e) {
console.error("cannot parse devtools payload", e);
}
}
devtoolsPlugin(state)(gb);
};
}
/**
* Intended to be used with cookieParser() middleware from npm: 'cookie-parser'.
*/
export function devtoolsExpressPlugin({
request,
}: {
request?: ExpressRequestCompat;
}): Plugin {
return (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => {
let payload =
typeof request?.query?.["_gbdebug"] === "string"
? request.query["_gbdebug"]
: undefined;
if (!payload) {
payload =
typeof request?.cookies?.["_gbdebug"] === "string"
? request.cookies["_gbdebug"]
: undefined;
}
let state: DevtoolsState = {};
if (payload) {
try {
state = JSON.parse(payload);
} catch (e) {
console.error("cannot parse devtools payload", e);
}
}
devtoolsPlugin(state)(gb);
};
}
export type SdkInfo = {
apiHost: string;
clientKey: string;
source?: string;
version?: string;
payload?: FeatureApiResponse;
attributes?: Attributes;
};
export type LogEvent = {
logs: LogUnion[];
sdkInfo?: SdkInfo;
};
/**
* Helper method to get debug script contents for DevTools
* @param gb - GrowthBook instance. DevMode must be enabled to view log events.
* @param {string} [source] - Label these events for ease of reading in DevTools
* @example
* A React logger component (implement yourself):
```
return (
<script dangerouslySetInnerHTML={{
__html: getDebugScriptContents(gb, "nextjs")
}} />
);
```
*/
export function getDebugScriptContents(
gb: GrowthBook,
source?: string,
): string {
const event = getDebugEvent(gb, source);
if (!event) return "";
return `(window._gbdebugEvents = (window._gbdebugEvents || [])).push(${JSON.stringify(
event,
)});`;
}
export function getDebugEvent(
gb: GrowthBook | UserScopedGrowthBook,
source?: string,
): LogEvent | null {
if (!("logs" in gb)) return null;
// Only enable in dev mode
if (!gb.inDevMode()) {
return null;
}
if (gb instanceof GrowthBook) {
// GrowthBook SDK
const [apiHost, clientKey] = gb.getApiInfo();
return {
logs: gb.logs,
sdkInfo: {
apiHost,
clientKey,
source,
version: gb.version,
payload: gb.getDecryptedPayload(),
attributes: gb.getAttributes(),
},
};
} else if (gb instanceof UserScopedGrowthBook) {
// UserScopedGrowthBook SDK
const userContext = gb.getUserContext();
const [apiHost, clientKey] = gb.getApiInfo();
return {
logs: gb.logs,
sdkInfo: {
apiHost,
clientKey,
source,
version: gb.getVersion(),
payload: gb.getDecryptedPayload(),
attributes: {
...userContext.attributes,
...userContext.attributeOverrides,
},
},
};
}
return null;
}

View File

@@ -0,0 +1,301 @@
import { loadSDKVersion } from "../util";
import type { Attributes, EventProperties } from "../types/growthbook";
import type { GrowthBook } from "../GrowthBook";
import type {
GrowthBookClient,
UserScopedGrowthBook,
} from "../GrowthBookClient";
import { EVENT_EXPERIMENT_VIEWED, EVENT_FEATURE_EVALUATED } from "../core";
const SDK_VERSION = loadSDKVersion();
type GlobalTrackedEvent = {
eventName: string;
properties: Record<string, unknown>;
};
declare global {
interface Window {
gbEvents?:
| (GlobalTrackedEvent | string)[]
| {
push: (event: GlobalTrackedEvent | string) => void;
};
}
}
type EventPayload = {
event_name: string;
properties_json: Record<string, unknown>;
sdk_language: string;
sdk_version: string;
url: string;
context_json: Record<string, unknown>;
user_id: string | null;
device_id: string | null;
page_id: string | null;
session_id: string | null;
page_title?: string;
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
utm_term?: string;
utm_content?: string;
};
function parseString(value: unknown): null | string {
return typeof value === "string" ? value : null;
}
function parseAttributes(attributes: Attributes): {
nested: Attributes;
topLevel: {
user_id: string | null;
device_id: string | null;
page_id: string | null;
session_id: string | null;
page_title?: string;
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
utm_term?: string;
utm_content?: string;
};
} {
const {
user_id,
device_id,
anonymous_id,
id,
page_id,
session_id,
utmCampaign,
utmContent,
utmMedium,
utmSource,
utmTerm,
pageTitle,
...nested
} = attributes;
return {
nested,
topLevel: {
user_id: parseString(user_id),
device_id: parseString(device_id || anonymous_id || id),
page_id: parseString(page_id),
session_id: parseString(session_id),
utm_campaign: parseString(utmCampaign) || undefined,
utm_content: parseString(utmContent) || undefined,
utm_medium: parseString(utmMedium) || undefined,
utm_source: parseString(utmSource) || undefined,
utm_term: parseString(utmTerm) || undefined,
page_title: parseString(pageTitle) || undefined,
},
};
}
type EventData = {
eventName: string;
properties: EventProperties;
attributes: Attributes;
url: string;
};
function getEventPayload({
eventName,
properties,
attributes,
url,
}: EventData): EventPayload {
const { nested, topLevel } = parseAttributes(attributes || {});
return {
event_name: eventName,
properties_json: properties || {},
...topLevel,
sdk_language: "js",
sdk_version: SDK_VERSION,
url: url,
context_json: nested,
};
}
async function track({
clientKey,
ingestorHost,
events,
}: {
events: EventPayload[];
clientKey: string;
ingestorHost?: string;
}) {
if (!events.length) return;
const endpoint = `${
ingestorHost || "https://us1.gb-ingest.com"
}/track?client_key=${clientKey}`;
const body = JSON.stringify(events);
try {
await fetch(endpoint, {
method: "POST",
body,
headers: {
Accept: "application/json",
"Content-Type": "text/plain",
},
credentials: "omit",
});
} catch (e) {
console.error("Failed to track event", e);
}
}
export function growthbookTrackingPlugin({
queueFlushInterval = 100,
ingestorHost,
enable = true,
debug,
dedupeCacheSize = 1000,
dedupeKeyAttributes = [],
eventFilter,
}: {
// TODO: add option to allow filtering out certain attributes that contain PII
queueFlushInterval?: number;
ingestorHost?: string;
enable?: boolean;
debug?: boolean;
dedupeCacheSize?: number;
dedupeKeyAttributes?: string[];
eventFilter?: (event: EventData) => boolean;
} = {}) {
return (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => {
const clientKey = gb.getClientKey();
if (!clientKey) {
throw new Error("clientKey must be specified to use event logging");
}
// LRU cache for events to avoid duplicates
const eventCache = new Set<string>();
if ("setEventLogger" in gb) {
let _q: EventPayload[] = [];
let timer: NodeJS.Timeout | null = null;
const flush = async () => {
const events = _q;
_q = [];
timer && clearTimeout(timer);
timer = null;
events.length && (await track({ clientKey, events, ingestorHost }));
};
let promise: Promise<void> | null = null;
gb.setEventLogger(async (eventName, properties, userContext) => {
const data: EventData = {
eventName,
properties,
attributes: userContext.attributes || {},
url: userContext.url || "",
};
// Skip logging if the event is being filtered
if (eventFilter && !eventFilter(data)) {
return;
}
// De-dupe Feature Evaluated and Experiment Viewed events
if (
eventName === EVENT_FEATURE_EVALUATED ||
eventName === EVENT_EXPERIMENT_VIEWED
) {
// Build the key for de-duping
const dedupeKeyData: Record<string, unknown> = {
eventName,
properties,
};
for (const key of dedupeKeyAttributes) {
dedupeKeyData["attr:" + key] = data.attributes[key];
}
const k = JSON.stringify(dedupeKeyData);
// Duplicate event fired recently, move to end of LRU cache and skip
if (eventCache.has(k)) {
eventCache.delete(k);
eventCache.add(k);
return;
}
eventCache.add(k);
// If the cache is too big, remove the oldest item
if (eventCache.size > dedupeCacheSize) {
const oldest = eventCache.values().next().value;
oldest && eventCache.delete(oldest);
}
}
const payload = getEventPayload(data);
debug &&
console.log(
"Logging event to GrowthBook",
JSON.parse(JSON.stringify(payload)),
);
if (!enable) return;
_q.push(payload);
// Only one in-progress promise at a time
if (!promise) {
promise = new Promise((resolve, reject) => {
// Flush the queue after a delay
timer = setTimeout(() => {
flush().then(resolve).catch(reject);
promise = null;
}, queueFlushInterval);
});
}
await promise;
});
// Flush the queue on page unload
if (typeof document !== "undefined" && document.visibilityState) {
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
flush().catch(console.error);
}
});
}
// Flush the queue when the growthbook instance is destroyed
"onDestroy" in gb &&
gb.onDestroy(() => {
flush().catch(console.error);
});
}
// Listen on window.gbEvents.push if in a browser
// This makes it easier to integrate with Segment, GTM, etc.
if (typeof window !== "undefined" && !("createScopedInstance" in gb)) {
const prevEvents = Array.isArray(window.gbEvents) ? window.gbEvents : [];
window.gbEvents = {
push: (event: GlobalTrackedEvent | string) => {
if ("isDestroyed" in gb && gb.isDestroyed()) {
// If trying to log and the instance has been destroyed, switch back to just an array
// This will let the next GrowthBook instance pick it up
window.gbEvents = [event];
return;
}
if (typeof event === "string") {
gb.logEvent(event);
} else if (event) {
gb.logEvent(event.eventName, event.properties);
}
},
};
for (const event of prevEvents) {
window.gbEvents.push(event);
}
}
};
}

View File

@@ -0,0 +1,20 @@
export { autoAttributesPlugin } from "./auto-attributes";
export { growthbookTrackingPlugin } from "./growthbook-tracking";
export { thirdPartyTrackingPlugin } from "./third-party-tracking";
export {
devtoolsPlugin,
devtoolsNextjsPlugin,
devtoolsExpressPlugin,
getDebugScriptContents,
getDebugEvent,
} from "./devtools";
// Types must be exported separately, otherwise rollup includes them in the javascript output which breaks things
export type {
DevtoolsState,
ExpressRequestCompat,
NextjsReadonlyRequestCookiesCompat,
NextjsRequestCompat,
LogEvent,
SdkInfo,
} from "./devtools";

View File

@@ -0,0 +1,74 @@
import type { TrackingCallback } from "../types/growthbook";
import type { GrowthBook } from "../GrowthBook";
import type {
GrowthBookClient,
UserScopedGrowthBook,
} from "../GrowthBookClient";
export type Trackers = "gtag" | "gtm" | "segment";
export function thirdPartyTrackingPlugin({
additionalCallback,
trackers = ["gtag", "gtm", "segment"],
}: {
additionalCallback?: TrackingCallback;
trackers?: Trackers[];
} = {}) {
// Browser only
if (typeof window === "undefined") {
throw new Error("thirdPartyTrackingPlugin only works in the browser");
}
return (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => {
gb.setTrackingCallback(async (e, r) => {
const promises: Promise<unknown>[] = [];
const eventParams = { experiment_id: e.key, variation_id: r.key };
if (additionalCallback) {
promises.push(Promise.resolve(additionalCallback(e, r)));
}
// GA4 - gtag
if (trackers.includes("gtag") && window.gtag) {
let gtagResolve;
const gtagPromise = new Promise((resolve) => {
gtagResolve = resolve;
});
promises.push(gtagPromise);
window.gtag("event", "experiment_viewed", {
...eventParams,
event_callback: gtagResolve,
});
}
// GTM - dataLayer
if (trackers.includes("gtm") && window.dataLayer) {
let datalayerResolve;
const datalayerPromise = new Promise((resolve) => {
datalayerResolve = resolve;
});
promises.push(datalayerPromise);
window.dataLayer.push({
event: "experiment_viewed",
...eventParams,
eventCallback: datalayerResolve,
});
}
// Segment - analytics.js
if (
trackers.includes("segment") &&
window.analytics &&
window.analytics.track
) {
window.analytics.track("Experiment Viewed", eventParams);
const segmentPromise = new Promise((resolve) =>
window.setTimeout(resolve, 300),
);
promises.push(segmentPromise);
}
await Promise.all(promises);
});
};
}