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,3 @@
# `core`
This directory holds public modules implementing non-resource-specific SDK functionality.

View File

@@ -0,0 +1,101 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { type BaseAnthropic } from '../client';
import { type PromiseOrValue } from '../internal/types';
import {
type APIResponseProps,
type WithRequestID,
defaultParseResponse,
addRequestID,
} from '../internal/parse';
/**
* A subclass of `Promise` providing additional helper methods
* for interacting with the SDK.
*/
export class APIPromise<T> extends Promise<WithRequestID<T>> {
private parsedPromise: Promise<WithRequestID<T>> | undefined;
#client: BaseAnthropic;
constructor(
client: BaseAnthropic,
private responsePromise: Promise<APIResponseProps>,
private parseResponse: (
client: BaseAnthropic,
props: APIResponseProps,
) => PromiseOrValue<WithRequestID<T>> = defaultParseResponse,
) {
super((resolve) => {
// this is maybe a bit weird but this has to be a no-op to not implicitly
// parse the response body; instead .then, .catch, .finally are overridden
// to parse the response
resolve(null as any);
});
this.#client = client;
}
_thenUnwrap<U>(transform: (data: T, props: APIResponseProps) => U): APIPromise<U> {
return new APIPromise(this.#client, this.responsePromise, async (client, props) =>
addRequestID(transform(await this.parseResponse(client, props), props), props.response),
);
}
/**
* Gets the raw `Response` instance instead of parsing the response
* data.
*
* If you want to parse the response body but still get the `Response`
* instance, you can use {@link withResponse()}.
*
* 👋 Getting the wrong TypeScript type for `Response`?
* Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
* to your `tsconfig.json`.
*/
asResponse(): Promise<Response> {
return this.responsePromise.then((p) => p.response);
}
/**
* Gets the parsed response data, the raw `Response` instance and the ID of the request,
* returned via the `request-id` header which is useful for debugging requests and resporting
* issues to Anthropic.
*
* If you just want to get the raw `Response` instance without parsing it,
* you can use {@link asResponse()}.
*
* 👋 Getting the wrong TypeScript type for `Response`?
* Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
* to your `tsconfig.json`.
*/
async withResponse(): Promise<{ data: T; response: Response; request_id: string | null | undefined }> {
const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
return { data, response, request_id: response.headers.get('request-id') };
}
private parse(): Promise<WithRequestID<T>> {
if (!this.parsedPromise) {
this.parsedPromise = this.responsePromise.then(
(data) => this.parseResponse(this.#client, data) as any as Promise<WithRequestID<T>>,
);
}
return this.parsedPromise;
}
override then<TResult1 = WithRequestID<T>, TResult2 = never>(
onfulfilled?: ((value: WithRequestID<T>) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
): Promise<TResult1 | TResult2> {
return this.parse().then(onfulfilled, onrejected);
}
override catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
): Promise<WithRequestID<T> | TResult> {
return this.parse().catch(onrejected);
}
override finally(onfinally?: (() => void) | undefined | null): Promise<WithRequestID<T>> {
return this.parse().finally(onfinally);
}
}

View File

@@ -0,0 +1,145 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { castToError } from '../internal/errors';
import type { ErrorType } from '../resources/shared';
export class AnthropicError extends Error {}
export class APIError<
TStatus extends number | undefined = number | undefined,
THeaders extends Headers | undefined = Headers | undefined,
TError extends Object | undefined = Object | undefined,
> extends AnthropicError {
/** HTTP status for the response that caused the error */
readonly status: TStatus;
/** HTTP headers for the response that caused the error */
readonly headers: THeaders;
/** JSON body of the response that caused the error */
readonly error: TError;
readonly requestID: string | null | undefined;
/** The `error.type` from the API response body, e.g. `"rate_limit_error"` */
readonly type: ErrorType | null;
constructor(
status: TStatus,
error: TError,
message: string | undefined,
headers: THeaders,
type?: ErrorType | null,
) {
super(`${APIError.makeMessage(status, error, message)}`);
this.status = status;
this.headers = headers;
this.requestID = headers?.get('request-id');
this.error = error;
this.type = type ?? null;
}
private static makeMessage(status: number | undefined, error: any, message: string | undefined) {
const msg =
error?.message ?
typeof error.message === 'string' ?
error.message
: JSON.stringify(error.message)
: error ? JSON.stringify(error)
: message;
if (status && msg) {
return `${status} ${msg}`;
}
if (status) {
return `${status} status code (no body)`;
}
if (msg) {
return msg;
}
return '(no status code or body)';
}
static generate(
status: number | undefined,
errorResponse: Object | undefined,
message: string | undefined,
headers: Headers | undefined,
): APIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}
const error = errorResponse as Record<string, any>;
const type = error?.['error']?.['type'] as ErrorType | undefined;
if (status === 400) {
return new BadRequestError(status, error, message, headers, type);
}
if (status === 401) {
return new AuthenticationError(status, error, message, headers, type);
}
if (status === 403) {
return new PermissionDeniedError(status, error, message, headers, type);
}
if (status === 404) {
return new NotFoundError(status, error, message, headers, type);
}
if (status === 409) {
return new ConflictError(status, error, message, headers, type);
}
if (status === 422) {
return new UnprocessableEntityError(status, error, message, headers, type);
}
if (status === 429) {
return new RateLimitError(status, error, message, headers, type);
}
if (status >= 500) {
return new InternalServerError(status, error, message, headers, type);
}
return new APIError(status, error, message, headers, type);
}
}
export class APIUserAbortError extends APIError<undefined, undefined, undefined> {
constructor({ message }: { message?: string } = {}) {
super(undefined, undefined, message || 'Request was aborted.', undefined);
}
}
export class APIConnectionError extends APIError<undefined, undefined, undefined> {
constructor({ message, cause }: { message?: string | undefined; cause?: Error | undefined }) {
super(undefined, undefined, message || 'Connection error.', undefined);
// in some environments the 'cause' property is already declared
// @ts-ignore
if (cause) this.cause = cause;
}
}
export class APIConnectionTimeoutError extends APIConnectionError {
constructor({ message }: { message?: string } = {}) {
super({ message: message ?? 'Request timed out.' });
}
}
export class BadRequestError extends APIError<400, Headers> {}
export class AuthenticationError extends APIError<401, Headers> {}
export class PermissionDeniedError extends APIError<403, Headers> {}
export class NotFoundError extends APIError<404, Headers> {}
export class ConflictError extends APIError<409, Headers> {}
export class UnprocessableEntityError extends APIError<422, Headers> {}
export class RateLimitError extends APIError<429, Headers> {}
export class InternalServerError extends APIError<number, Headers> {}

View File

@@ -0,0 +1,331 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { AnthropicError } from './error';
import { FinalRequestOptions } from '../internal/request-options';
import { defaultParseResponse, WithRequestID } from '../internal/parse';
import { type BaseAnthropic } from '../client';
import { APIPromise } from './api-promise';
import { type APIResponseProps } from '../internal/parse';
import { maybeObj } from '../internal/utils/values';
export type PageRequestOptions = Pick<FinalRequestOptions, 'query' | 'headers' | 'body' | 'path' | 'method'>;
export abstract class AbstractPage<Item> implements AsyncIterable<Item> {
#client: BaseAnthropic;
protected options: FinalRequestOptions;
protected response: Response;
protected body: unknown;
constructor(client: BaseAnthropic, response: Response, body: unknown, options: FinalRequestOptions) {
this.#client = client;
this.options = options;
this.response = response;
this.body = body;
}
abstract nextPageRequestOptions(): PageRequestOptions | null;
abstract getPaginatedItems(): Item[];
hasNextPage(): boolean {
const items = this.getPaginatedItems();
if (!items.length) return false;
return this.nextPageRequestOptions() != null;
}
async getNextPage(): Promise<this> {
const nextOptions = this.nextPageRequestOptions();
if (!nextOptions) {
throw new AnthropicError(
'No next page expected; please check `.hasNextPage()` before calling `.getNextPage()`.',
);
}
return await this.#client.requestAPIList(this.constructor as any, nextOptions);
}
async *iterPages(): AsyncGenerator<this> {
let page: this = this;
yield page;
while (page.hasNextPage()) {
page = await page.getNextPage();
yield page;
}
}
async *[Symbol.asyncIterator](): AsyncGenerator<Item> {
for await (const page of this.iterPages()) {
for (const item of page.getPaginatedItems()) {
yield item;
}
}
}
}
/**
* This subclass of Promise will resolve to an instantiated Page once the request completes.
*
* It also implements AsyncIterable to allow auto-paginating iteration on an unawaited list call, eg:
*
* for await (const item of client.items.list()) {
* console.log(item)
* }
*/
export class PagePromise<
PageClass extends AbstractPage<Item>,
Item = ReturnType<PageClass['getPaginatedItems']>[number],
>
extends APIPromise<PageClass>
implements AsyncIterable<Item>
{
constructor(
client: BaseAnthropic,
request: Promise<APIResponseProps>,
Page: new (...args: ConstructorParameters<typeof AbstractPage>) => PageClass,
) {
super(
client,
request,
async (client, props) =>
new Page(
client,
props.response,
await defaultParseResponse(client, props),
props.options,
) as WithRequestID<PageClass>,
);
}
/**
* Allow auto-paginating iteration on an unawaited list call, eg:
*
* for await (const item of client.items.list()) {
* console.log(item)
* }
*/
async *[Symbol.asyncIterator](): AsyncGenerator<Item> {
const page = await this;
for await (const item of page) {
yield item;
}
}
}
export interface PageResponse<Item> {
data: Array<Item>;
has_more: boolean;
first_id: string | null;
last_id: string | null;
}
export interface PageParams {
/**
* Number of items per page.
*/
limit?: number;
before_id?: string;
after_id?: string;
}
export class Page<Item> extends AbstractPage<Item> implements PageResponse<Item> {
data: Array<Item>;
has_more: boolean;
first_id: string | null;
last_id: string | null;
constructor(
client: BaseAnthropic,
response: Response,
body: PageResponse<Item>,
options: FinalRequestOptions,
) {
super(client, response, body, options);
this.data = body.data || [];
this.has_more = body.has_more || false;
this.first_id = body.first_id || null;
this.last_id = body.last_id || null;
}
getPaginatedItems(): Item[] {
return this.data ?? [];
}
override hasNextPage(): boolean {
if (this.has_more === false) {
return false;
}
return super.hasNextPage();
}
nextPageRequestOptions(): PageRequestOptions | null {
if ((this.options.query as Record<string, unknown>)?.['before_id']) {
// in reverse
const first_id = this.first_id;
if (!first_id) {
return null;
}
return {
...this.options,
query: {
...maybeObj(this.options.query),
before_id: first_id,
},
};
}
const cursor = this.last_id;
if (!cursor) {
return null;
}
return {
...this.options,
query: {
...maybeObj(this.options.query),
after_id: cursor,
},
};
}
}
export interface TokenPageResponse<Item> {
data: Array<Item>;
has_more: boolean;
next_page: string | null;
}
export interface TokenPageParams {
/**
* Number of items per page.
*/
limit?: number;
page_token?: string;
}
export class TokenPage<Item> extends AbstractPage<Item> implements TokenPageResponse<Item> {
data: Array<Item>;
has_more: boolean;
next_page: string | null;
constructor(
client: BaseAnthropic,
response: Response,
body: TokenPageResponse<Item>,
options: FinalRequestOptions,
) {
super(client, response, body, options);
this.data = body.data || [];
this.has_more = body.has_more || false;
this.next_page = body.next_page || null;
}
getPaginatedItems(): Item[] {
return this.data ?? [];
}
override hasNextPage(): boolean {
if (this.has_more === false) {
return false;
}
return super.hasNextPage();
}
nextPageRequestOptions(): PageRequestOptions | null {
const cursor = this.next_page;
if (!cursor) {
return null;
}
return {
...this.options,
query: {
...maybeObj(this.options.query),
page_token: cursor,
},
};
}
}
export interface PageCursorResponse<Item> {
data: Array<Item>;
has_more: boolean;
next_page: string | null;
}
export interface PageCursorParams {
/**
* Number of items per page.
*/
limit?: number;
page?: string | null;
}
export class PageCursor<Item> extends AbstractPage<Item> implements PageCursorResponse<Item> {
data: Array<Item>;
has_more: boolean;
next_page: string | null;
constructor(
client: BaseAnthropic,
response: Response,
body: PageCursorResponse<Item>,
options: FinalRequestOptions,
) {
super(client, response, body, options);
this.data = body.data || [];
this.has_more = body.has_more || false;
this.next_page = body.next_page || null;
}
getPaginatedItems(): Item[] {
return this.data ?? [];
}
override hasNextPage(): boolean {
if (this.has_more === false) {
return false;
}
return super.hasNextPage();
}
nextPageRequestOptions(): PageRequestOptions | null {
const cursor = this.next_page;
if (!cursor) {
return null;
}
return {
...this.options,
query: {
...maybeObj(this.options.query),
page: cursor,
},
};
}
}

View File

@@ -0,0 +1,11 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { BaseAnthropic } from '../client';
export abstract class APIResource {
protected _client: BaseAnthropic;
constructor(client: BaseAnthropic) {
this._client = client;
}
}

View File

@@ -0,0 +1,348 @@
import { AnthropicError } from './error';
import { type ReadableStream } from '../internal/shim-types';
import { makeReadableStream } from '../internal/shims';
import { findDoubleNewlineIndex, LineDecoder } from '../internal/decoders/line';
import { ReadableStreamToAsyncIterable } from '../internal/shims';
import { isAbortError } from '../internal/errors';
import { safeJSON } from '../internal/utils/values';
import { encodeUTF8 } from '../internal/utils/bytes';
import { loggerFor } from '../internal/utils/log';
import type { BaseAnthropic } from '../client';
import { APIError } from './error';
import type { ErrorType } from '../resources/shared';
type Bytes = string | ArrayBuffer | Uint8Array | null | undefined;
export type ServerSentEvent = {
event: string | null;
data: string;
raw: string[];
};
export class Stream<Item> implements AsyncIterable<Item> {
controller: AbortController;
#client: BaseAnthropic | undefined;
constructor(
private iterator: () => AsyncIterator<Item>,
controller: AbortController,
client?: BaseAnthropic,
) {
this.controller = controller;
this.#client = client;
}
static fromSSEResponse<Item>(
response: Response,
controller: AbortController,
client?: BaseAnthropic,
): Stream<Item> {
let consumed = false;
const logger = client ? loggerFor(client) : console;
async function* iterator(): AsyncIterator<Item, any, undefined> {
if (consumed) {
throw new AnthropicError('Cannot iterate over a consumed stream, use `.tee()` to split the stream.');
}
consumed = true;
let done = false;
try {
for await (const sse of _iterSSEMessages(response, controller)) {
if (sse.event === 'completion') {
try {
yield JSON.parse(sse.data) as Item;
} catch (e) {
logger.error(`Could not parse message into JSON:`, sse.data);
logger.error(`From chunk:`, sse.raw);
throw e;
}
}
if (
sse.event === 'message_start' ||
sse.event === 'message_delta' ||
sse.event === 'message_stop' ||
sse.event === 'content_block_start' ||
sse.event === 'content_block_delta' ||
sse.event === 'content_block_stop'
) {
try {
yield JSON.parse(sse.data) as Item;
} catch (e) {
logger.error(`Could not parse message into JSON:`, sse.data);
logger.error(`From chunk:`, sse.raw);
throw e;
}
}
if (sse.event === 'ping') {
continue;
}
if (sse.event === 'error') {
const body = safeJSON(sse.data) ?? sse.data;
const type = body?.error?.type as ErrorType | undefined;
throw new APIError(undefined, body, undefined, response.headers, type);
}
}
done = true;
} catch (e) {
// If the user calls `stream.controller.abort()`, we should exit without throwing.
if (isAbortError(e)) return;
throw e;
} finally {
// If the user `break`s, abort the ongoing request.
if (!done) controller.abort();
}
}
return new Stream(iterator, controller, client);
}
/**
* Generates a Stream from a newline-separated ReadableStream
* where each item is a JSON value.
*/
static fromReadableStream<Item>(
readableStream: ReadableStream,
controller: AbortController,
client?: BaseAnthropic,
): Stream<Item> {
let consumed = false;
async function* iterLines(): AsyncGenerator<string, void, unknown> {
const lineDecoder = new LineDecoder();
const iter = ReadableStreamToAsyncIterable<Bytes>(readableStream);
for await (const chunk of iter) {
for (const line of lineDecoder.decode(chunk)) {
yield line;
}
}
for (const line of lineDecoder.flush()) {
yield line;
}
}
async function* iterator(): AsyncIterator<Item, any, undefined> {
if (consumed) {
throw new AnthropicError('Cannot iterate over a consumed stream, use `.tee()` to split the stream.');
}
consumed = true;
let done = false;
try {
for await (const line of iterLines()) {
if (done) continue;
if (line) yield JSON.parse(line) as Item;
}
done = true;
} catch (e) {
// If the user calls `stream.controller.abort()`, we should exit without throwing.
if (isAbortError(e)) return;
throw e;
} finally {
// If the user `break`s, abort the ongoing request.
if (!done) controller.abort();
}
}
return new Stream(iterator, controller, client);
}
[Symbol.asyncIterator](): AsyncIterator<Item> {
return this.iterator();
}
/**
* Splits the stream into two streams which can be
* independently read from at different speeds.
*/
tee(): [Stream<Item>, Stream<Item>] {
const left: Array<Promise<IteratorResult<Item>>> = [];
const right: Array<Promise<IteratorResult<Item>>> = [];
const iterator = this.iterator();
const teeIterator = (queue: Array<Promise<IteratorResult<Item>>>): AsyncIterator<Item> => {
return {
next: () => {
if (queue.length === 0) {
const result = iterator.next();
left.push(result);
right.push(result);
}
return queue.shift()!;
},
};
};
return [
new Stream(() => teeIterator(left), this.controller, this.#client),
new Stream(() => teeIterator(right), this.controller, this.#client),
];
}
/**
* Converts this stream to a newline-separated ReadableStream of
* JSON stringified values in the stream
* which can be turned back into a Stream with `Stream.fromReadableStream()`.
*/
toReadableStream(): ReadableStream {
const self = this;
let iter: AsyncIterator<Item>;
return makeReadableStream({
async start() {
iter = self[Symbol.asyncIterator]();
},
async pull(ctrl: any) {
try {
const { value, done } = await iter.next();
if (done) return ctrl.close();
const bytes = encodeUTF8(JSON.stringify(value) + '\n');
ctrl.enqueue(bytes);
} catch (err) {
ctrl.error(err);
}
},
async cancel() {
await iter.return?.();
},
});
}
}
export async function* _iterSSEMessages(
response: Response,
controller: AbortController,
): AsyncGenerator<ServerSentEvent, void, unknown> {
if (!response.body) {
controller.abort();
if (
typeof (globalThis as any).navigator !== 'undefined' &&
(globalThis as any).navigator.product === 'ReactNative'
) {
throw new AnthropicError(
`The default react-native fetch implementation does not support streaming. Please use expo/fetch: https://docs.expo.dev/versions/latest/sdk/expo/#expofetch-api`,
);
}
throw new AnthropicError(`Attempted to iterate over a response with no body`);
}
const sseDecoder = new SSEDecoder();
const lineDecoder = new LineDecoder();
const iter = ReadableStreamToAsyncIterable<Bytes>(response.body);
for await (const sseChunk of iterSSEChunks(iter)) {
for (const line of lineDecoder.decode(sseChunk)) {
const sse = sseDecoder.decode(line);
if (sse) yield sse;
}
}
for (const line of lineDecoder.flush()) {
const sse = sseDecoder.decode(line);
if (sse) yield sse;
}
}
/**
* Given an async iterable iterator, iterates over it and yields full
* SSE chunks, i.e. yields when a double new-line is encountered.
*/
async function* iterSSEChunks(iterator: AsyncIterableIterator<Bytes>): AsyncGenerator<Uint8Array> {
let data = new Uint8Array();
for await (const chunk of iterator) {
if (chunk == null) {
continue;
}
const binaryChunk =
chunk instanceof ArrayBuffer ? new Uint8Array(chunk)
: typeof chunk === 'string' ? encodeUTF8(chunk)
: chunk;
let newData = new Uint8Array(data.length + binaryChunk.length);
newData.set(data);
newData.set(binaryChunk, data.length);
data = newData;
let patternIndex;
while ((patternIndex = findDoubleNewlineIndex(data)) !== -1) {
yield data.slice(0, patternIndex);
data = data.slice(patternIndex);
}
}
if (data.length > 0) {
yield data;
}
}
class SSEDecoder {
private data: string[];
private event: string | null;
private chunks: string[];
constructor() {
this.event = null;
this.data = [];
this.chunks = [];
}
decode(line: string) {
if (line.endsWith('\r')) {
line = line.substring(0, line.length - 1);
}
if (!line) {
// empty line and we didn't previously encounter any messages
if (!this.event && !this.data.length) return null;
const sse: ServerSentEvent = {
event: this.event,
data: this.data.join('\n'),
raw: this.chunks,
};
this.event = null;
this.data = [];
this.chunks = [];
return sse;
}
this.chunks.push(line);
if (line.startsWith(':')) {
return null;
}
let [fieldname, _, value] = partition(line, ':');
if (value.startsWith(' ')) {
value = value.substring(1);
}
if (fieldname === 'event') {
this.event = value;
} else if (fieldname === 'data') {
this.data.push(value);
}
return null;
}
}
function partition(str: string, delimiter: string): [string, string, string] {
const index = str.indexOf(delimiter);
if (index !== -1) {
return [str.substring(0, index), delimiter, str.substring(index + delimiter.length)];
}
return [str, '', ''];
}

View File

@@ -0,0 +1,2 @@
export { type Uploadable } from '../internal/uploads';
export { toFile, type ToFileInput } from '../internal/to-file';