diff --git a/legacy/examples/event-handling.ts b/legacy/examples/event-handling.ts new file mode 100644 index 00000000..acd25db9 --- /dev/null +++ b/legacy/examples/event-handling.ts @@ -0,0 +1,22 @@ +import 'dotenv/config' +import { OpenAIClient } from 'openai-fetch' +import { z } from 'zod' + +import { Agentic, TaskStatus } from '@/index' + +async function main() { + const openai = new OpenAIClient({ apiKey: process.env.OPENAI_API_KEY! }) + const $ = new Agentic({ openai }) + + const ai = $.gpt4(`generate fake data`).output( + z.object({ foo: z.string(), bar: z.number() }) + ) + + ai.eventEmitter.on(TaskStatus.COMPLETED, (event) => { + console.log('Task completed successfully:', event) + }) + + ai.call() +} + +main() diff --git a/legacy/packages/core/package.json b/legacy/packages/core/package.json index a353efa7..4392ec74 100644 --- a/legacy/packages/core/package.json +++ b/legacy/packages/core/package.json @@ -45,6 +45,7 @@ "@types/json-schema": "^7.0.12", "colorette": "^2.0.20", "debug": "^4.3.4", + "eventemitter3": "^5.0.1", "expr-eval": "^2.0.2", "handlebars": "^4.7.7", "is-relative-url": "^4.0.0", @@ -62,6 +63,7 @@ "p-timeout": "^6.1.2", "quick-lru": "^6.1.1", "replicate": "^0.12.3", + "tree-model": "^1.0.7", "ts-dedent": "^2.2.0", "type-fest": "^3.12.0", "zod": "^3.21.4", diff --git a/legacy/packages/core/src/agentic.ts b/legacy/packages/core/src/agentic.ts index 5f242961..e543eb57 100644 --- a/legacy/packages/core/src/agentic.ts +++ b/legacy/packages/core/src/agentic.ts @@ -1,17 +1,20 @@ +import { EventEmitter } from 'eventemitter3' import defaultKy from 'ky' import { SetOptional } from 'type-fest' import * as types from './types' import { DEFAULT_OPENAI_MODEL } from './constants' +import { TerminalTaskTracker } from './events' import { HumanFeedbackOptions, HumanFeedbackType } from './human-feedback' import { HumanFeedbackMechanismCLI } from './human-feedback/cli' import { OpenAIChatCompletion } from './llms/openai' import { defaultLogger } from './logger' import { defaultIDGeneratorFn, isFunction, isString } from './utils' -export class Agentic { +export class Agentic extends EventEmitter { protected _ky: types.KyInstance protected _logger: types.Logger + protected _taskTracker: TerminalTaskTracker protected _openai?: types.openai.OpenAIClient protected _anthropic?: types.anthropic.Client @@ -35,7 +38,10 @@ export class Agentic { idGeneratorFn?: types.IDGeneratorFunction logger?: types.Logger ky?: types.KyInstance + taskTracker?: TerminalTaskTracker }) { + super() + // TODO: This is a bit hacky, but we're doing it to have a slightly nicer API // for the end developer when creating subclasses of `BaseTask` to use as // tools. @@ -48,6 +54,7 @@ export class Agentic { this._ky = opts.ky ?? defaultKy this._logger = opts.logger ?? defaultLogger + this._taskTracker = opts.taskTracker ?? new TerminalTaskTracker() this._openaiModelDefaults = { provider: 'openai', @@ -98,6 +105,10 @@ export class Agentic { return this._humanFeedbackDefaults } + public get taskTracker(): TerminalTaskTracker { + return this._taskTracker + } + public get idGeneratorFn(): types.IDGeneratorFunction { return this._idGeneratorFn } diff --git a/legacy/packages/core/src/constants.ts b/legacy/packages/core/src/constants.ts index dc85269f..207c67de 100644 --- a/legacy/packages/core/src/constants.ts +++ b/legacy/packages/core/src/constants.ts @@ -2,3 +2,4 @@ export const DEFAULT_OPENAI_MODEL = 'gpt-3.5-turbo' export const DEFAULT_ANTHROPIC_MODEL = 'claude-instant-v1' export const DEFAULT_BOT_NAME = 'Agentic Bot' export const SKIP_HOOKS = Symbol('SKIP_HOOKS') +export const SPACE = ' ' diff --git a/legacy/packages/core/src/events/event.ts b/legacy/packages/core/src/events/event.ts new file mode 100644 index 00000000..37a3eeb5 --- /dev/null +++ b/legacy/packages/core/src/events/event.ts @@ -0,0 +1,158 @@ +import { defaultIDGeneratorFn } from '@/utils' + +/** + * Payload of an event. + */ +export interface EventPayload { + [key: string]: unknown +} + +/** + * Data required to create a new Event object. + */ +export interface EventData { + /** + * Event identifier + */ + id?: string + + /** + * Event timestamp + */ + timestamp?: Date + + /** + * Key-value pairs holding event data. + */ + payload?: T + + /** + * Version of the event. + */ + version?: number + + /** + * Event type. + */ + type?: string +} + +/** + * Events that occur within the library (should be treated as immutable). + */ +export class Event { + public readonly id: string + public readonly timestamp: Date + public readonly payload?: T + public readonly version: number + + constructor(data: EventData = {}) { + this.id = defaultIDGeneratorFn() + this.timestamp = data.timestamp ?? new Date() + this.payload = data.payload + ? JSON.parse(JSON.stringify(data.payload)) + : undefined + this.version = data.version ?? 1 + } + + /** + * Converts a JSON string representation of an event back into an Event object. + */ + static fromJSON(json: string): Event { + const data = JSON.parse(json) + data.timestamp = new Date(data.timestamp) + let Type + switch (data.type) { + case 'TaskEvent': + Type = TaskEvent + break + case 'Event': + Type = Event + break + default: + throw new Error(`Unknown event type: ${data.type}`) + } + + return new Type(data) + } + + /** + * Serializes an event into a JSON string. + */ + toJSON(): string { + return JSON.stringify({ + id: this.id, + timestamp: this.timestamp.toISOString(), + payload: this.payload, + version: this.version, + type: this.constructor.name + }) + } + + /** + * Returns a human-readable string representation of an event. + */ + toString(): string { + return `Event { id: ${ + this.id + }, timestamp: ${this.timestamp.toISOString()}, payload: ${JSON.stringify( + this.payload + )} }` + } +} + +/** + * Payload of a task event. + */ +export interface TaskEventPayload extends EventPayload { + taskName: string + taskId: string + taskStatus: TaskStatus + taskInputs?: TInput + taskOutput?: TOutput + parentTaskId?: string +} + +/** + * Status of a task. + */ +export enum TaskStatus { + COMPLETED = 'COMPLETED', + FAILED = 'FAILED', + PENDING = 'PENDING', + RETRYING = 'RETRYING', + SKIPPED = 'SKIPPED', + RUNNING = 'RUNNING', + CANCELLED = 'CANCELLED' +} + +/** + * Events that occur within the library related to tasks. + */ +export class TaskEvent extends Event< + TaskEventPayload +> { + get name(): string { + return this.payload?.taskName ?? '' + } + + get taskId(): string { + return this.payload?.taskId ?? '' + } + + get status(): TaskStatus { + return this.payload?.taskStatus ?? TaskStatus.RUNNING + } + + get inputs(): any { + return this.payload?.taskInputs ?? '' + } + + get output(): any { + return this.payload?.taskOutput ?? '' + } + + get parentTaskId(): string { + return this.payload?.parentTaskId ?? 'root' + } +} diff --git a/legacy/packages/core/src/events/index.ts b/legacy/packages/core/src/events/index.ts new file mode 100644 index 00000000..b15f5f24 --- /dev/null +++ b/legacy/packages/core/src/events/index.ts @@ -0,0 +1,2 @@ +export * from './event' +export * from './tracker' diff --git a/legacy/packages/core/src/events/symbols.ts b/legacy/packages/core/src/events/symbols.ts new file mode 100644 index 00000000..02fbda76 --- /dev/null +++ b/legacy/packages/core/src/events/symbols.ts @@ -0,0 +1,32 @@ +import isUnicodeSupported from 'is-unicode-supported' + +const UNICODE_SYMBOLS = { + ARROW_RIGHT: '→', + CIRCLE: '●', + WARNING: '▲', + CROSS: '⨯', + SQUARE_SMALL_FILLED: '◼', + SPINNER: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], + BAR_START: '┌', + BAR: '│', + BAR_END: '└', + ACTIVE: '◆', + LEFT_ARROW: '←', + RIGHT_ARROW: '→' +} +const ASCII_SYMBOLS = { + ARROW_RIGHT: '→', + CIRCLE: '•', + WARNING: '‼', + CROSS: '×', + SQUARE_SMALL_FILLED: '■', + SPINNER: ['-', '\\', '|', '/'], + BAR_START: 'T', + BAR: '|', + BAR_END: '—', + ACTIVE: '*', + LEFT_ARROW: '<', + RIGHT_ARROW: '>' +} + +export const SYMBOLS = isUnicodeSupported() ? UNICODE_SYMBOLS : ASCII_SYMBOLS diff --git a/legacy/packages/core/src/events/tracker.ts b/legacy/packages/core/src/events/tracker.ts new file mode 100644 index 00000000..1414acfc --- /dev/null +++ b/legacy/packages/core/src/events/tracker.ts @@ -0,0 +1,422 @@ +import process from 'node:process' +import readline from 'node:readline' + +import { bgWhite, black, bold, cyan, gray, green, red, yellow } from 'colorette' +import TreeModel from 'tree-model' + +import { SPACE } from '@/constants' +import { capitalize } from '@/utils' + +import { TaskEvent, TaskStatus } from './event' +import { SYMBOLS } from './symbols' + +export const MAGIC_STRING = '__INSIDE_TRACKER__' // a unique "magic" string that used to identify the output of the tracker + +const TWO_SPACES = `${SPACE}${SPACE}` + +// eslint-disable-next-line no-control-regex +const RE_ANSI_ESCAPES = /^(\x1b\[[0-9;]*[ABCDHJK]|[\r\n])+$/ // cursor movement, screen clearing, etc. + +const originalStdoutWrite = process.stdout.write +const originalStderrWrite = process.stderr.write + +export interface TerminalTaskTrackerOptions { + spinnerInterval?: number + inactivityInterval?: number +} + +export class TerminalTaskTracker { + protected _tree = new TreeModel() + protected _root = this._tree.parse({ id: 'root' }) + protected _interval: NodeJS.Timeout | null = null + protected _inactivityTimeout: NodeJS.Timeout | null = null + protected _truncateOutput = false + protected _viewMode = 'tasks' + protected _outputs: Array = [] + protected _renderingPaused = false + protected _isClosed = false + + protected _spinnerInterval: number + protected _inactivityInterval: number + + private _stdoutBuffer: string[] = [] + private _stderrBuffer: string[] = [] + + constructor({ + spinnerInterval = 100, + inactivityInterval = 2_000 + }: TerminalTaskTrackerOptions = {}) { + this._spinnerInterval = spinnerInterval + this._inactivityInterval = inactivityInterval + + if (!process.stderr.isTTY) { + // If stderr is not a TTY, don't render any dynamic output... + return + } + + process.stdout.write = (buffer: string | Uint8Array) => { + if (buffer instanceof Uint8Array) { + buffer = Buffer.from(buffer).toString('utf-8') + } + + if (!this._renderingPaused) { + this._stdoutBuffer.push(buffer) + } + + return originalStdoutWrite.call(process.stdout, buffer) + } + + process.stderr.write = (buffer: string | Uint8Array) => { + if (buffer instanceof Uint8Array) { + buffer = Buffer.from(buffer).toString('utf-8') + } + + if (typeof buffer === 'string' && buffer.startsWith(MAGIC_STRING)) { + // This write is from inside the tracker, remove the magic string and write to stderr: + return originalStderrWrite.call( + process.stderr, + buffer.replace(MAGIC_STRING, '') + ) + } else { + if (!this._renderingPaused && !RE_ANSI_ESCAPES.test(buffer)) { + // If an ANSI escape sequence is written to stderr, it will mess up the output, so we need to write it to stdout instead: + // This write is from outside the tracker, add it to stderrBuffer and write to stderr: + this._stderrBuffer.push(buffer) + } + + return originalStderrWrite.call(process.stderr, buffer) + } + } + + this.start() + } + + handleKeyPress = (str, key) => { + if (key.ctrl && key.name === 'c') { + process.exit() + } + + if (key.ctrl && key.name === 'e') { + this.toggleOutputTruncation() + } + + if (key.ctrl && key.name === 'right') { + this.toggleView('next') + } + + if (key.ctrl && key.name === 'left') { + this.toggleView('prev') + } + } + + start() { + this._interval = setInterval(() => { + this.render() + }, this._spinnerInterval) + + readline.emitKeypressEvents(process.stdin) + + process.stdin.setRawMode(true) + + process.stdin.on('keypress', this.handleKeyPress) + + this.startInactivityTimeout() + + this._isClosed = false + } + + close() { + if (this._isClosed) { + return + } + + if (this._interval) { + clearInterval(this._interval) + } + + if (this._inactivityTimeout) { + clearTimeout(this._inactivityTimeout) + } + + process.stdin.setRawMode(false) + + // Remove the keypress listener: + process.stdin.off('keypress', this.handleKeyPress) + + // Restore the original `process.stdout.write()` and `process.stderr.write()` functions: + process.stdout.write = originalStdoutWrite + process.stderr.write = originalStderrWrite + + const finalLines = [ + '', + '', + bgWhite(black(' Completed all tasks. ')), + '', + '', + bgWhite(black(' stderr: ')), + '', + this._stderrBuffer.join(''), + '', + bgWhite(black(' stdout: ')), + '', + this._stdoutBuffer.join(''), + '' + ] + + process.stderr.write(finalLines.join('\n')) + + // Pause the reading of stdin so that the Node.js process will exit once done: + process.stdin.pause() + + this._isClosed = true + } + + pause() { + this.clearAndSetCursorPosition() + this._renderingPaused = true + } + + resume() { + this._renderingPaused = false + this.render() + } + + stringify(value: any): string { + if (this._truncateOutput) { + const json = JSON.stringify(value) + if (json.length < 40) { + return json + } + + return json.slice(0, 20) + '...' + json.slice(-20) + } + + return JSON.stringify(value) + } + + toggleOutputTruncation() { + this._truncateOutput = !this._truncateOutput + } + + startInactivityTimeout() { + this._inactivityTimeout = setTimeout(() => { + const unfinishedTasks = this._root.all((node) => { + return ( + node.model.status == TaskStatus.RUNNING || + node.model.status == TaskStatus.RETRYING || + node.model.status == TaskStatus.PENDING + ) + }) + + if (unfinishedTasks.length === 0) { + this.close() + } else { + this.startInactivityTimeout() + } + }, this._inactivityInterval) + } + + addEvent(event: TaskEvent) { + const { + parentTaskId = 'root', + taskId: id, + name, + status, + inputs, + output + } = event + const parentNode = this._root.first( + (node) => node.model.id === parentTaskId + ) + + const existingEventNode = parentNode + ? parentNode.first((node) => node.model.id === id) + : null + + if (existingEventNode) { + existingEventNode.model.status = status + existingEventNode.model.output = output + } else { + const node = this._tree.parse({ id, name, status, inputs, output: null }) + if (parentNode) { + parentNode.addChild(node) + } else { + this._root.addChild(node) + } + } + } + + private getStatusSymbolColor( + status: TaskStatus + ): [string, (text: string) => string] { + switch (status) { + case TaskStatus.COMPLETED: + return [SYMBOLS.CIRCLE, green] + case TaskStatus.FAILED: + return [SYMBOLS.CROSS, red] + case TaskStatus.RETRYING: + return [this.getSpinnerSymbol(), yellow] + case TaskStatus.RUNNING: + default: + return [this.getSpinnerSymbol(), cyan] + } + } + + renderTree(id?: string, level = 0): string[] { + const indent = SPACE.repeat(level * 2) + let lines: string[] = [] + + const root = id + ? this._root.first((node) => node.model.id === id) + : this._root + + if (root?.children) { + root.children.forEach( + ({ model: { id, name, status, output, inputs } }) => { + const [statusSymbol, color] = this.getStatusSymbolColor(status) + + lines.push( + indent + + color(statusSymbol) + + SPACE + + bold(name) + + gray('(' + this.stringify(inputs) + ')') + ) + + const hasChildren = root.hasChildren() + + if (hasChildren) { + lines = lines.concat( + this.renderTree(id, level + 1).map((line, index, arr) => { + if (index === arr.length - 1) { + return indent + gray(SYMBOLS.BAR) + line + } + + return indent + gray(SYMBOLS.BAR) + line + }) + ) + } + + let line = '' + if (hasChildren) { + line = indent + gray(SYMBOLS.BAR_END) + } + + const formattedOutput = this.stringify(output || '') + if (status === TaskStatus.COMPLETED) { + line += + indent + + ' ' + + gray(SYMBOLS.RIGHT_ARROW + SPACE + formattedOutput) + } else if (status === TaskStatus.FAILED) { + line += + indent + + ' ' + + gray(SYMBOLS.RIGHT_ARROW) + + SPACE + + red(formattedOutput) + } else if (status === TaskStatus.RETRYING) { + line += + indent + + ' ' + + yellow(SYMBOLS.WARNING) + + SPACE + + gray(formattedOutput) + } + + lines.push(line) + } + ) + } + + return lines + } + + clearAndSetCursorPosition() { + process.stderr.cursorTo(0, 0) + process.stderr.clearScreenDown() + } + + clearPreviousRender(linesCount: number) { + for (let i = 0; i < linesCount; i++) { + process.stderr.moveCursor(0, -1) + process.stderr.clearLine(1) + } + } + + private writeWithMagicString(content: string | string[]) { + let output + + if (Array.isArray(content)) { + if (content.length === 0) { + return + } + + output = content.join('\n') + } else { + output = content + } + + process.stderr.write(MAGIC_STRING + output) + } + + toggleView(direction: string) { + const viewModes = ['tasks', 'stdout', 'stderr'] + const currentIdx = viewModes.indexOf(this._viewMode) + + if (direction === 'next') { + this._viewMode = viewModes[(currentIdx + 1) % viewModes.length] + } else if (direction === 'prev') { + this._viewMode = + viewModes[(currentIdx - 1 + viewModes.length) % viewModes.length] + } + + this.render() + } + + getSpinnerSymbol(): string { + return SYMBOLS.SPINNER[ + Math.floor(Date.now() / this._spinnerInterval) % SYMBOLS.SPINNER.length + ] + } + + renderHeader() { + const commands = [ + 'ctrl+c: exit', + `ctrl+e: ${ + this._truncateOutput ? TWO_SPACES + 'expand' : 'truncate' + } output`, + 'ctrl+left/right: switch view' + ].join(' | ') + + const header = [ + ` Agentic - ${capitalize(this._viewMode)} View`, + ' ' + commands + ' ', + '', + '' + ].join('\n') + this.writeWithMagicString(bgWhite(black(header))) + } + + render() { + if (this._renderingPaused) { + return // Do not render if paused + } + + this.clearAndSetCursorPosition() + if (this._viewMode === 'tasks') { + const lines = this.renderTree('root') + this.clearPreviousRender(lines.length) + this.renderHeader() + this.writeWithMagicString(lines) + } else if (this._viewMode === 'stdout') { + this.clearPreviousRender(this._stdoutBuffer.length) + this.renderHeader() + this.writeWithMagicString(this._stdoutBuffer) + } else if (this._viewMode === 'stderr') { + this.clearPreviousRender(this._stderrBuffer.length) + this.renderHeader() + this.writeWithMagicString(this._stderrBuffer) + } + } +} diff --git a/legacy/packages/core/src/index.ts b/legacy/packages/core/src/index.ts index 4df46984..3287cc3d 100644 --- a/legacy/packages/core/src/index.ts +++ b/legacy/packages/core/src/index.ts @@ -1,9 +1,10 @@ export * from './agentic' -export * from './task' export * from './constants' export * from './errors' -export * from './tokenizer' +export * from './events' export * from './human-feedback' +export * from './task' +export * from './tokenizer' export * from './llms' export * from './services' diff --git a/legacy/packages/core/src/logger.ts b/legacy/packages/core/src/logger.ts index 9611cc4a..546e171a 100644 --- a/legacy/packages/core/src/logger.ts +++ b/legacy/packages/core/src/logger.ts @@ -1,6 +1,7 @@ import { cyan, green, magenta, red, yellow } from 'colorette' import logger from 'debug' +import { SPACE } from '@/constants' import { identity } from '@/utils' import { getEnv } from './env' @@ -57,7 +58,6 @@ if (LOG_LEVEL === undefined) { const debug = logger('agentic') -const SPACE = ' ' const INDENT = SPACE.repeat(23) // Override the default logger to add a timestamp and severity level to the logged arguments: diff --git a/legacy/packages/core/src/task.ts b/legacy/packages/core/src/task.ts index a8eb3ee3..8942a6a4 100644 --- a/legacy/packages/core/src/task.ts +++ b/legacy/packages/core/src/task.ts @@ -1,3 +1,4 @@ +import EventEmitter from 'eventemitter3' import pRetry, { FailedAttemptError } from 'p-retry' import QuickLRU from 'quick-lru' import { ZodType } from 'zod' @@ -6,6 +7,7 @@ import * as errors from './errors' import * as types from './types' import type { Agentic } from './agentic' import { SKIP_HOOKS } from './constants' +import { TaskEvent, TaskStatus } from './events' import { HumanFeedbackMechanismCLI, HumanFeedbackOptions, @@ -29,7 +31,7 @@ import { defaultIDGeneratorFn, isValidTaskIdentifier } from './utils' export abstract class BaseTask< TInput extends types.TaskInput = void, TOutput extends types.TaskOutput = string -> { +> extends EventEmitter { protected _agentic: Agentic protected _id: string @@ -48,6 +50,8 @@ export abstract class BaseTask< }> = [] constructor(options: types.BaseTaskOptions = {}) { + super() + this._agentic = options.agentic ?? globalThis.__agentic?.deref() this._timeoutMs = options.timeoutMs @@ -190,7 +194,9 @@ export abstract class BaseTask< }) this.addAfterCallHook(async (output, ctx) => { + this._agentic.taskTracker.pause() const feedback = await feedbackMechanism.interact(output) + this._agentic.taskTracker.resume() ctx.metadata = { ...ctx.metadata, feedback } if (feedback.editedOutput) { return feedback.editedOutput @@ -272,6 +278,11 @@ export abstract class BaseTask< } } + this.emit(TaskStatus.RUNNING, { + taskInputs: input, + ...ctx.metadata + }) + for (const { hook: preHook } of this._preHooks) { const preHookResult = await preHook(ctx) if (preHookResult === SKIP_HOOKS) { @@ -342,6 +353,12 @@ export abstract class BaseTask< ctx.attemptNumber = err.attemptNumber + 1 ctx.metadata.error = err + this.emit(TaskStatus.RETRYING, { + taskInputs: input, + taskOutput: err, + ...ctx.metadata + }) + if (err instanceof errors.ZodOutputValidationError) { ctx.retryMessage = err.message return @@ -365,6 +382,12 @@ export abstract class BaseTask< // task for now. return } else { + this.emit(TaskStatus.FAILED, { + taskInputs: input, + taskOutput: err, + ...ctx.metadata + }) + throw err } } @@ -381,6 +404,12 @@ export abstract class BaseTask< // ctx.tracker.setOutput(stringifyForDebugging(result, { maxLength: 100 })) + this.emit(TaskStatus.COMPLETED, { + taskInputs: input, + taskOutput: result, + ...ctx.metadata + }) + return { result, metadata: ctx.metadata @@ -397,4 +426,33 @@ export abstract class BaseTask< // input: TInput, // onProgress: types.ProgressFunction // }): Promise + + on( + takStatus: T, + fn: (event: TaskEvent) => void, + context?: any + ): this { + return super.on(takStatus, fn, context) + } + + emit(taskStatus: string | symbol, payload: object = {}): boolean { + if (!Object.values(TaskStatus).includes(taskStatus as TaskStatus)) { + throw new Error(`Invalid task status: ${String(taskStatus)}`) + } + + const { id, nameForModel } = this + const event = new TaskEvent({ + payload: { + taskStatus: taskStatus as TaskStatus, + taskId: id, + taskName: nameForModel, + ...payload + } + }) + this._agentic.taskTracker.addEvent(event) + + this._agentic.emit(taskStatus, event) + + return super.emit(taskStatus, event) + } } diff --git a/legacy/packages/core/src/utils.ts b/legacy/packages/core/src/utils.ts index 64b92ef3..b58bec62 100644 --- a/legacy/packages/core/src/utils.ts +++ b/legacy/packages/core/src/utils.ts @@ -254,3 +254,7 @@ export function isArray(value: any): value is any[] { export function identity(x: T): T { return x } + +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1) +} diff --git a/legacy/pnpm-lock.yaml b/legacy/pnpm-lock.yaml index 834c2c49..9949707c 100644 --- a/legacy/pnpm-lock.yaml +++ b/legacy/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -125,6 +125,9 @@ importers: debug: specifier: ^4.3.4 version: 4.3.4 + eventemitter3: + specifier: ^5.0.1 + version: 5.0.1 expr-eval: specifier: ^2.0.2 version: 2.0.2 @@ -176,6 +179,9 @@ importers: replicate: specifier: ^0.12.3 version: 0.12.3 + tree-model: + specifier: ^1.0.7 + version: 1.0.7 ts-dedent: specifier: ^2.2.0 version: 2.2.0 @@ -3222,6 +3228,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + dev: false + /execa@0.8.0: resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==} engines: {node: '>=4'} @@ -3359,6 +3369,10 @@ packages: to-regex-range: 5.0.1 dev: true + /find-insert-index@0.0.1: + resolution: {integrity: sha512-eIqFuQzY7XwpAJ3sHWKFNGLx1nm3w/IhmFASETcx5sUuCaOUd3xDqRK/376SzXMVVJQaJUCPlS7L841T0xpFjQ==} + dev: false + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -4834,6 +4848,10 @@ packages: engines: {node: '>= 8'} dev: true + /mergesort@0.0.1: + resolution: {integrity: sha512-WKghTBzqAvTt9rG5TWS78Dmk2kCCL9VkkX8Zi9kKfJ4iqYpvcGGpeYtkhPHa9NZAPLivZiZsdO/LBG3ENayDmQ==} + dev: false + /mermaid@10.2.3: resolution: {integrity: sha512-cMVE5s9PlQvOwfORkyVpr5beMsLdInrycAosdr+tpZ0WFjG4RJ/bUHST7aTgHNJbujHkdBRAm+N50P3puQOfPw==} dependencies: @@ -6928,6 +6946,13 @@ packages: hasBin: true dev: true + /tree-model@1.0.7: + resolution: {integrity: sha512-oP4LUbCVtD2gcjcRaeI4L5hY60tHzB+AK/bthIJ2Pq1EUUOio5/xFzPWnGoBZlhtqpqbOkhFDzKIwKLOn0kccQ==} + dependencies: + find-insert-index: 0.0.1 + mergesort: 0.0.1 + dev: false + /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} dev: false