import * as plugins from './plugins.js'; import * as aiDocsClasses from './aidocs_classes/index.js'; import { logger } from './logging.js'; export type TAiDocAuthMode = plugins.tsagent.TTsAgentAuthMode; export interface IAiDocConfig extends plugins.tsagent.ITsAgentConfig {} export interface IAiDocResolvedConfig extends plugins.tsagent.ITsAgentResolvedConfig {} export const defaultAiDocConfig: IAiDocResolvedConfig = { ...plugins.tsagent.defaultTsAgentConfig, }; type TAiDocRunAgentOptions = Parameters[0]; type TAiDocRunAgentResult = Awaited>; const normalizeAuthMode = (value: unknown): TAiDocAuthMode | undefined => { return value === 'auto' || value === 'chatgpt' || value === 'apiKey' ? value : undefined; }; const normalizeProvider = (value: unknown): plugins.smartai.TProvider | undefined => { return ['anthropic', 'openai', 'google', 'groq', 'mistral', 'xai', 'perplexity', 'ollama'].includes(String(value)) ? value as plugins.smartai.TProvider : undefined; }; const normalizeChatGptSources = (value: unknown): plugins.smartaiOpenAiChatGptAuth.TOpenAiChatGptAuthSource[] | undefined => { const input = typeof value === 'string' ? value.split(',').map(source => source.trim()) : value; if (!Array.isArray(input)) return undefined; const sources = input.filter((source): source is plugins.smartaiOpenAiChatGptAuth.TOpenAiChatGptAuthSource => { return source === 'opencode' || source === 'codex' || source === 'smartai'; }); return sources.length > 0 ? sources : undefined; }; const normalizeOpenAiReasoningEffort = (value: unknown): plugins.smartai.TOpenAiReasoningEffort | undefined => { return ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(String(value)) ? value as plugins.smartai.TOpenAiReasoningEffort : undefined; }; export class AiDoc { private tsAgent?: plugins.tsagent.TsAgent; private openaiToken = ''; public smartconfigKV?: plugins.smartconfig.KeyValueStore<{ OPENAI_TOKEN: string }>; public qenvInstance?: plugins.qenv.Qenv; public model!: plugins.tsagent.TsAgent['model']; public providerOptions?: IAiDocResolvedConfig['providerOptions']; public config: IAiDocResolvedConfig = defaultAiDocConfig; public selectedAuthSource: string = 'none'; public argvArg: any; constructor(argvArg?: any) { this.argvArg = argvArg || {}; } private get activeTsAgent(): plugins.tsagent.TsAgent { if (!this.tsAgent) { throw new Error('AiDoc.start() must be called before running AI tasks.'); } return this.tsAgent; } private async migrateKeyValueStore(): Promise { const homeDir = plugins.smartpath.get.home(); const oldKvPath = plugins.path.join(homeDir, '.smartconfig/kv/tsdoc.json'); const newKvDir = plugins.path.join(homeDir, '.smartconfig/kv/@git.zone'); const newKvPath = plugins.path.join(newKvDir, 'tsdoc.json'); if ( await plugins.fsInstance.file(oldKvPath).exists() && !(await plugins.fsInstance.file(newKvPath).exists()) ) { logger.log('info', 'Migrating tsdoc KeyValueStore to @git.zone/tsdoc...'); await plugins.fsInstance.directory(newKvDir).recursive().create(); await plugins.fsInstance.file(oldKvPath).copy(newKvPath); await plugins.fsInstance.file(oldKvPath).delete(); logger.log('success', 'Migration complete: tsdoc.json -> @git.zone/tsdoc.json'); } } private async resolveTsdocEnvConfig(): Promise { // this Qenv reads the environment of the project tsdoc was invoked in, not tsdoc's own. // That project legitimately has no qenv.yml, and none of the TSDOC_* names below is // required - they merely override defaults - so a missing contract file must not fail. this.qenvInstance = new plugins.qenv.Qenv(process.cwd(), undefined, false); const envProvider = normalizeProvider(await this.qenvInstance.getEnvVarOnDemand('TSDOC_AI_PROVIDER')); const envModel = await this.qenvInstance.getEnvVarOnDemand('TSDOC_AI_MODEL'); const envAuthMode = normalizeAuthMode(await this.qenvInstance.getEnvVarOnDemand('TSDOC_AUTH_MODE')); const envChatGptSources = normalizeChatGptSources(await this.qenvInstance.getEnvVarOnDemand('TSDOC_CHATGPT_AUTH_SOURCES')); const envReasoningEffort = normalizeOpenAiReasoningEffort(await this.qenvInstance.getEnvVarOnDemand('TSDOC_OPENAI_REASONING_EFFORT')); return { provider: envProvider, model: envModel || undefined, authMode: envAuthMode, chatGptAuthSources: envChatGptSources, providerOptions: envReasoningEffort ? { openai: { reasoningEffort: envReasoningEffort, }, } : undefined, }; } public async start() { await this.migrateKeyValueStore(); this.smartconfigKV = new plugins.smartconfig.KeyValueStore({ typeArg: 'userHomeDir', identityArg: '@git.zone/tsdoc', mandatoryKeys: ['OPENAI_TOKEN'], }); this.tsAgent = new plugins.tsagent.TsAgent({ argvArg: this.argvArg, projectDir: process.cwd(), projectConfigKeys: ['@git.zone/tsagent', 'tsdoc', '@git.zone/tsdoc'], legacyTokenStoreIdentities: ['@git.zone/tsdoc'], config: await this.resolveTsdocEnvConfig(), }); await this.tsAgent.start(); this.model = this.tsAgent.model; this.providerOptions = this.tsAgent.providerOptions; this.config = this.tsAgent.config as IAiDocResolvedConfig; this.selectedAuthSource = this.tsAgent.selectedAuthSource; this.openaiToken = this.tsAgent.getOpenaiToken(); if (this.selectedAuthSource !== 'none') { logger.log('info', `Using AI auth from ${this.selectedAuthSource}.`); } } public async stop() { await this.tsAgent?.stop(); } public getOpenaiToken(): string { return this.openaiToken; } public async runAgent(options: TAiDocRunAgentOptions): Promise { const result = await this.activeTsAgent.runAgent({ ...options, sessionId: options.sessionId ?? `tsdoc:${options.taskName}:${plugins.path.resolve(options.projectDir)}`, }); logger.log( 'info', `[${options.taskName}] tokens input=${result.usage.inputTokens} output=${result.usage.outputTokens} total=${result.usage.totalTokens} cacheRead=${result.usage.cacheReadTokens} cacheWrite=${result.usage.cacheWriteTokens}`, ); return result; } public async buildReadme(projectDirArg: string) { const readmeInstance = new aiDocsClasses.Readme(this, projectDirArg); return await readmeInstance.build(); } public async buildDescription(projectDirArg: string) { const descriptionInstance = new aiDocsClasses.Description(this, projectDirArg); return await descriptionInstance.build(); } public async buildNextCommitObject(projectDirArg: string) { const commitInstance = new aiDocsClasses.Commit(this, projectDirArg); return await commitInstance.buildNextCommitObject(); } public async getProjectContext(projectDirArg: string) { const projectContextInstance = new aiDocsClasses.ProjectContext(projectDirArg); return await projectContextInstance.gatherFiles(); } /** * Get the context with token count information * @param projectDirArg The path to the project directory * @returns An object containing the context string and its token count */ public async getProjectContextWithTokenCount(projectDirArg: string) { const projectContextInstance = new aiDocsClasses.ProjectContext(projectDirArg); await projectContextInstance.update(); return projectContextInstance.getContextWithTokenCount(); } /** * Get just the token count for a project's context * @param projectDirArg The path to the project directory * @returns The number of tokens in the project context */ public async getProjectContextTokenCount(projectDirArg: string) { const projectContextInstance = new aiDocsClasses.ProjectContext(projectDirArg); await projectContextInstance.update(); return projectContextInstance.getTokenCount(); } /** * Estimate token count in a text string * @param text The text to estimate tokens for * @returns Estimated number of tokens */ public countTokens(text: string): number { const projectContextInstance = new aiDocsClasses.ProjectContext(''); return projectContextInstance.countTokens(text); } }