@git.zone/tsdoc 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,12 @@
1
1
  import type { AiDoc } from '../classes.aidoc.js';
2
2
  import * as plugins from '../plugins.js';
3
- import * as paths from '../paths.js';
4
3
  import { ProjectContext } from './projectcontext.js';
5
4
  import { logger } from '../logging.js';
5
+ import { createReadOnlyFileSystemTools } from '../helpers.agenttools.js';
6
+
7
+ const getLegalInfo = (smartconfigJson: any): string | undefined => {
8
+ return smartconfigJson?.['@git.zone/tsdoc']?.legal ?? smartconfigJson?.tsdoc?.legal;
9
+ };
6
10
 
7
11
  export class Readme {
8
12
  // INSTANCE
@@ -19,17 +23,16 @@ export class Readme {
19
23
 
20
24
  // First check legal info before introducing any cost
21
25
  const projectContext = new ProjectContext(this.projectDir);
22
- const smartconfigJson = JSON.parse(
23
- (await projectContext.gatherFiles()).smartfilesSmartconfigJSON.contents.toString()
24
- );
25
- const legalInfo = smartconfigJson?.['tsdoc']?.legal;
26
+ const smartconfigFile = (await projectContext.gatherFiles()).smartfilesSmartconfigJSON;
27
+ const smartconfigJson = smartconfigFile.contents.length > 0
28
+ ? JSON.parse(smartconfigFile.contents.toString())
29
+ : {};
30
+ const legalInfo = getLegalInfo(smartconfigJson);
26
31
  if (!legalInfo) {
27
- const error = new Error(`No legal information found in .smartconfig.json`);
28
- console.log(error);
32
+ throw new Error('No legal information found in .smartconfig.json under @git.zone/tsdoc.legal or tsdoc.legal.');
29
33
  }
30
34
 
31
- // Use runAgent with filesystem tool for agent-driven exploration
32
- const fsTools = plugins.smartagentTools.filesystemTool({ rootDir: this.projectDir });
35
+ const fsTools = createReadOnlyFileSystemTools(this.projectDir);
33
36
 
34
37
  const readmeSystemPrompt = `
35
38
  You create markdown READMEs for npm projects. You only output the markdown readme.
@@ -42,7 +45,7 @@ IMPORTANT RULES:
42
45
  - README must follow proper markdown format
43
46
  - Must contain Install and Usage sections
44
47
  - Code examples must use correct TypeScript/ESM syntax
45
- - Documentation must be comprehensive and helpful
48
+ - Documentation must be comprehensive and helpful without unnecessary filler
46
49
  - Do NOT include licensing information (added separately)
47
50
  - Do NOT use CommonJS syntax - only ESM
48
51
  - Do NOT include "in conclusion" or similar filler
@@ -56,7 +59,7 @@ Use the filesystem tools to explore the project and understand what it does:
56
59
  2. Read package.json to understand the package name, description, and dependencies
57
60
  3. Read the existing readme.md if it exists (use it as a base, improve and expand)
58
61
  4. Read readme.hints.md if it exists (contains hints for documentation)
59
- 5. Read key source files in ts/ directory to understand the API and implementation
62
+ 5. Read key source files in ts/ and ts_web/ directories to understand the API and implementation
60
63
  6. Focus on exported classes, interfaces, and functions
61
64
 
62
65
  Then generate a comprehensive README following this template:
@@ -74,7 +77,7 @@ Then generate a comprehensive README following this template:
74
77
  Make sure to show a complete set of features of the module.
75
78
  Don't omit use cases.
76
79
  ALWAYS USE ESM SYNTAX AND TYPESCRIPT.
77
- Write at least 4000 words. More if necessary.
80
+ Size the documentation to the actual project. Do not pad with filler.
78
81
  If there is already a readme, take the Usage section as base. Remove outdated content, expand and improve.
79
82
  Check for completeness.
80
83
  Don't include any licensing information. This will be added later.
@@ -84,12 +87,14 @@ Then generate a comprehensive README following this template:
84
87
 
85
88
  logger.log('info', 'Starting README generation with agent...');
86
89
 
87
- const readmeResult = await plugins.smartagent.runAgent({
88
- model: this.aiDocsRef.model,
90
+ const readmeResult = await this.aiDocsRef.runAgent({
91
+ taskName: 'readme',
92
+ projectDir: this.projectDir,
89
93
  prompt: readmeTaskPrompt,
90
94
  system: readmeSystemPrompt,
91
95
  tools: fsTools,
92
96
  maxSteps: 25,
97
+ useCompaction: true,
93
98
  onToolCall: (toolName) => logger.log('info', `[README] Tool call: ${toolName}`),
94
99
  });
95
100
 
@@ -110,19 +115,19 @@ Then generate a comprehensive README following this template:
110
115
 
111
116
  // lets care about monorepo aspects
112
117
  const tsPublishInstance = new plugins.tspublish.TsPublish();
113
- const subModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
118
+ const subModules = await tsPublishInstance.getModuleSubDirs(this.projectDir);
114
119
  logger.log('info', `Found ${Object.keys(subModules).length} sub modules`);
115
120
 
116
121
  for (const subModule of Object.keys(subModules)) {
117
122
  logger.log('info', `Building readme for ${subModule}`);
118
123
 
119
- const subModulePath = plugins.path.join(paths.cwd, subModule);
124
+ const subModulePath = plugins.path.join(this.projectDir, subModule);
120
125
  const tspublishData = await plugins.fsInstance
121
126
  .file(plugins.path.join(subModulePath, 'tspublish.json'))
122
127
  .encoding('utf8')
123
128
  .read();
124
129
 
125
- const subModuleFsTools = plugins.smartagentTools.filesystemTool({ rootDir: subModulePath });
130
+ const subModuleFsTools = createReadOnlyFileSystemTools(subModulePath);
126
131
 
127
132
  const subModuleSystemPrompt = `
128
133
  You create markdown READMEs for npm projects. You only output the markdown readme.
@@ -130,7 +135,7 @@ You create markdown READMEs for npm projects. You only output the markdown readm
130
135
  IMPORTANT RULES:
131
136
  - Only READ files within the submodule directory
132
137
  - Do NOT write, delete, or modify any files
133
- - README must be comprehensive, well-formatted markdown with ESM TypeScript examples
138
+ - README must be comprehensive, well-formatted markdown with ESM TypeScript examples and no filler
134
139
  - Do NOT include licensing information (added separately)
135
140
  `;
136
141
 
@@ -159,7 +164,7 @@ Generate a README following the template:
159
164
  [
160
165
  Code examples with complete features.
161
166
  ESM TypeScript syntax only.
162
- Write at least 4000 words.
167
+ Size the documentation to the submodule. Do not pad with filler.
163
168
  No licensing information.
164
169
  No "in conclusion".
165
170
  ]
@@ -167,12 +172,14 @@ Generate a README following the template:
167
172
  Don't use \`\`\` at the beginning or end. Only for code blocks.
168
173
  `;
169
174
 
170
- const subModuleResult = await plugins.smartagent.runAgent({
171
- model: this.aiDocsRef.model,
175
+ const subModuleResult = await this.aiDocsRef.runAgent({
176
+ taskName: `readme:${subModule}`,
177
+ projectDir: subModulePath,
172
178
  prompt: subModulePrompt,
173
179
  system: subModuleSystemPrompt,
174
180
  tools: subModuleFsTools,
175
181
  maxSteps: 20,
182
+ useCompaction: true,
176
183
  onToolCall: (toolName) => logger.log('info', `[README:${subModule}] Tool call: ${toolName}`),
177
184
  });
178
185
 
@@ -1,6 +1,118 @@
1
1
  import * as plugins from './plugins.js';
2
2
 
3
3
  import * as aiDocsClasses from './aidocs_classes/index.js';
4
+ import { logger } from './logging.js';
5
+
6
+ export type TAiDocAuthMode = 'auto' | 'chatgpt' | 'apiKey';
7
+
8
+ export interface IAiDocConfig {
9
+ provider?: plugins.smartai.TProvider;
10
+ model?: string;
11
+ authMode?: TAiDocAuthMode;
12
+ chatGptAuthSources?: plugins.smartaiOpenAiChatGptAuth.TOpenAiChatGptAuthSource[];
13
+ chatGptAuthWriteBack?: Partial<Record<plugins.smartaiOpenAiChatGptAuth.TOpenAiChatGptAuthSource, boolean>>;
14
+ providerOptions?: plugins.smartai.TSmartAiProviderOptions;
15
+ cache?: plugins.smartagent.TAgentCacheSetting;
16
+ promptCaching?: boolean | plugins.smartai.ISmartAiCacheOptions;
17
+ apiKey?: string;
18
+ baseUrl?: string;
19
+ ollamaOptions?: plugins.smartai.IOllamaModelOptions;
20
+ }
21
+
22
+ export interface IAiDocResolvedConfig extends Required<Pick<IAiDocConfig,
23
+ 'provider' | 'model' | 'authMode' | 'chatGptAuthSources'
24
+ >> {
25
+ chatGptAuthWriteBack: Partial<Record<plugins.smartaiOpenAiChatGptAuth.TOpenAiChatGptAuthSource, boolean>>;
26
+ providerOptions?: plugins.smartai.TSmartAiProviderOptions;
27
+ cache: plugins.smartagent.TAgentCacheSetting;
28
+ promptCaching?: boolean | plugins.smartai.ISmartAiCacheOptions;
29
+ apiKey?: string;
30
+ baseUrl?: string;
31
+ ollamaOptions?: plugins.smartai.IOllamaModelOptions;
32
+ }
33
+
34
+ export const defaultAiDocConfig: IAiDocResolvedConfig = {
35
+ provider: 'openai',
36
+ model: 'gpt-5.5',
37
+ authMode: 'auto',
38
+ chatGptAuthSources: ['opencode', 'codex', 'smartai'],
39
+ chatGptAuthWriteBack: {
40
+ smartai: true,
41
+ opencode: false,
42
+ codex: false,
43
+ },
44
+ providerOptions: {
45
+ openai: {
46
+ reasoningEffort: 'xhigh',
47
+ },
48
+ },
49
+ cache: 'auto',
50
+ };
51
+
52
+ const isPlainObject = (value: unknown): value is Record<string, any> => {
53
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
54
+ };
55
+
56
+ const mergeProviderOptions = (
57
+ base?: plugins.smartai.TSmartAiProviderOptions,
58
+ override?: plugins.smartai.TSmartAiProviderOptions,
59
+ ): plugins.smartai.TSmartAiProviderOptions | undefined => {
60
+ if (!base && !override) return undefined;
61
+ const result: Record<string, any> = { ...(base ?? {}) };
62
+ for (const [provider, options] of Object.entries(override ?? {})) {
63
+ result[provider] = {
64
+ ...(isPlainObject(result[provider]) ? result[provider] : {}),
65
+ ...(isPlainObject(options) ? options : {}),
66
+ };
67
+ }
68
+ return result as plugins.smartai.TSmartAiProviderOptions;
69
+ };
70
+
71
+ const normalizeAuthMode = (value: unknown): TAiDocAuthMode | undefined => {
72
+ return value === 'auto' || value === 'chatgpt' || value === 'apiKey' ? value : undefined;
73
+ };
74
+
75
+ const normalizeProvider = (value: unknown): plugins.smartai.TProvider | undefined => {
76
+ return ['anthropic', 'openai', 'google', 'groq', 'mistral', 'xai', 'perplexity', 'ollama'].includes(String(value))
77
+ ? value as plugins.smartai.TProvider
78
+ : undefined;
79
+ };
80
+
81
+ const normalizeChatGptSources = (value: unknown): plugins.smartaiOpenAiChatGptAuth.TOpenAiChatGptAuthSource[] | undefined => {
82
+ const input = typeof value === 'string' ? value.split(',').map(source => source.trim()) : value;
83
+ if (!Array.isArray(input)) return undefined;
84
+ const sources = input.filter((source): source is plugins.smartaiOpenAiChatGptAuth.TOpenAiChatGptAuthSource => {
85
+ return source === 'opencode' || source === 'codex' || source === 'smartai';
86
+ });
87
+ return sources.length > 0 ? sources : undefined;
88
+ };
89
+
90
+ const normalizeOpenAiReasoningEffort = (value: unknown): plugins.smartai.TOpenAiReasoningEffort | undefined => {
91
+ return ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(String(value))
92
+ ? value as plugins.smartai.TOpenAiReasoningEffort
93
+ : undefined;
94
+ };
95
+
96
+ const getProjectTsdocConfig = async (projectDirArg: string): Promise<IAiDocConfig> => {
97
+ const smartconfigPath = plugins.path.join(projectDirArg, '.smartconfig.json');
98
+ if (!(await plugins.fsInstance.file(smartconfigPath).exists())) {
99
+ return {};
100
+ }
101
+ try {
102
+ const smartconfigJson = JSON.parse(String(await plugins.fsInstance.file(smartconfigPath).encoding('utf8').read()));
103
+ const legacyConfig = isPlainObject(smartconfigJson.tsdoc) ? smartconfigJson.tsdoc : {};
104
+ const namespacedConfig = isPlainObject(smartconfigJson['@git.zone/tsdoc']) ? smartconfigJson['@git.zone/tsdoc'] : {};
105
+ const config = {
106
+ ...legacyConfig,
107
+ ...namespacedConfig,
108
+ ...(isPlainObject(legacyConfig.ai) ? legacyConfig.ai : {}),
109
+ ...(isPlainObject(namespacedConfig.ai) ? namespacedConfig.ai : {}),
110
+ };
111
+ return config as IAiDocConfig;
112
+ } catch (error) {
113
+ throw new Error(`Could not parse .smartconfig.json: ${error instanceof Error ? error.message : String(error)}`);
114
+ }
115
+ };
4
116
 
5
117
  export class AiDoc {
6
118
  private openaiToken = '';
@@ -9,6 +121,9 @@ export class AiDoc {
9
121
  public qenvInstance!: plugins.qenv.Qenv;
10
122
  public aidocInteract!: plugins.smartinteract.SmartInteract;
11
123
  public model!: plugins.smartai.LanguageModelV3;
124
+ public providerOptions?: plugins.smartai.TSmartAiProviderOptions;
125
+ public config: IAiDocResolvedConfig = defaultAiDocConfig;
126
+ public selectedAuthSource: string = 'none';
12
127
 
13
128
  argvArg: any;
14
129
 
@@ -16,83 +131,191 @@ export class AiDoc {
16
131
  this.argvArg = argvArg;
17
132
  }
18
133
 
19
- private printSanitizedToken() {
20
- // Check if the token length is greater than the sum of startLength and endLength
21
- let printToken: string;
22
- if (this.openaiToken.length > 6) {
23
- // Extract the beginning and end parts of the token
24
- const start = this.openaiToken.substring(0, 3);
25
- const end = this.openaiToken.substring(this.openaiToken.length - 3);
26
- printToken = `${start}...${end}`;
27
- } else {
28
- // If the token is not long enough, return it as is
29
- printToken = this.openaiToken;
134
+ private get isInteractive(): boolean {
135
+ return process.stdin.isTTY === true && process.stdout.isTTY === true && !this.argvArg?.ci && !this.argvArg?.json;
136
+ }
137
+
138
+ private async migrateKeyValueStore(): Promise<void> {
139
+ const homeDir = plugins.smartpath.get.home();
140
+ const oldKvPath = plugins.path.join(homeDir, '.smartconfig/kv/tsdoc.json');
141
+ const newKvDir = plugins.path.join(homeDir, '.smartconfig/kv/@git.zone');
142
+ const newKvPath = plugins.path.join(newKvDir, 'tsdoc.json');
143
+ if (
144
+ await plugins.fsInstance.file(oldKvPath).exists() &&
145
+ !(await plugins.fsInstance.file(newKvPath).exists())
146
+ ) {
147
+ logger.log('info', 'Migrating tsdoc KeyValueStore to @git.zone/tsdoc...');
148
+ await plugins.fsInstance.directory(newKvDir).recursive().create();
149
+ await plugins.fsInstance.file(oldKvPath).copy(newKvPath);
150
+ await plugins.fsInstance.file(oldKvPath).delete();
151
+ logger.log('success', 'Migration complete: tsdoc.json -> @git.zone/tsdoc.json');
152
+ }
153
+ }
154
+
155
+ private async getStoredOpenAiToken(): Promise<string | undefined> {
156
+ if (!this.smartconfigKV) return undefined;
157
+ try {
158
+ const token = await this.smartconfigKV.readKey('OPENAI_TOKEN');
159
+ return token || undefined;
160
+ } catch {
161
+ return undefined;
162
+ }
163
+ }
164
+
165
+ private async resolveApiKey(provider: plugins.smartai.TProvider): Promise<string | undefined> {
166
+ const argvToken = this.argvArg?.OPENAI_TOKEN || this.argvArg?.OPENAI_API_KEY || this.argvArg?.apiKey;
167
+ if (argvToken) return argvToken;
168
+ if (this.config.apiKey) return this.config.apiKey;
169
+
170
+ const envNames = provider === 'openai'
171
+ ? ['OPENAI_TOKEN', 'OPENAI_API_KEY']
172
+ : [`${provider.toUpperCase()}_TOKEN`, `${provider.toUpperCase()}_API_KEY`];
173
+ for (const envName of envNames) {
174
+ const value = await this.qenvInstance.getEnvVarOnDemand(envName);
175
+ if (value) return value;
176
+ }
177
+
178
+ if (provider === 'openai') {
179
+ const storedToken = await this.getStoredOpenAiToken();
180
+ if (storedToken) return storedToken;
181
+ }
182
+
183
+ return undefined;
184
+ }
185
+
186
+ private async promptForOpenAiToken(): Promise<string> {
187
+ if (!this.isInteractive) {
188
+ throw new Error('OpenAI API key is required. Set OPENAI_TOKEN/OPENAI_API_KEY or configure ChatGPT subscription auth.');
30
189
  }
31
- console.log(`OpenAI Token on record: ${printToken}`);
190
+ await plugins.smartdelay.delayFor(1000);
191
+ const answerObject = await this.aidocInteract.askQuestion({
192
+ type: 'input',
193
+ message: 'Please provide your OpenAI token. This will be persisted in your home directory.',
194
+ name: 'OPENAI_TOKEN',
195
+ default: '',
196
+ });
197
+ const token = answerObject.value;
198
+ await this.smartconfigKV?.writeKey('OPENAI_TOKEN', token);
199
+ return token;
200
+ }
201
+
202
+ private async runChatGptDeviceLogin(): Promise<plugins.smartai.IOpenAiChatGptTokenData> {
203
+ if (!this.isInteractive) {
204
+ throw new Error('ChatGPT subscription auth is required, but no usable auth file was found in non-interactive mode.');
205
+ }
206
+
207
+ const deviceCode = await plugins.smartai.requestOpenAiChatGptDeviceCode();
208
+ console.log(`Open ${deviceCode.verificationUrl} and enter code ${deviceCode.userCode}`);
209
+ const tokenData = await plugins.smartai.completeOpenAiChatGptDeviceCodeLogin(deviceCode);
210
+ const authFilePath = plugins.smartaiOpenAiChatGptAuth.getDefaultOpenAiChatGptAuthPath('smartai');
211
+ await plugins.smartaiOpenAiChatGptAuth.writeOpenAiChatGptAuthFile(authFilePath, tokenData, 'smartai');
212
+ return tokenData;
213
+ }
214
+
215
+ private async resolveConfig(): Promise<IAiDocResolvedConfig> {
216
+ const projectConfig = await getProjectTsdocConfig(process.cwd());
217
+ const envProvider = normalizeProvider(await this.qenvInstance.getEnvVarOnDemand('TSDOC_AI_PROVIDER'));
218
+ const envModel = await this.qenvInstance.getEnvVarOnDemand('TSDOC_AI_MODEL');
219
+ const envAuthMode = normalizeAuthMode(await this.qenvInstance.getEnvVarOnDemand('TSDOC_AUTH_MODE'));
220
+ const envChatGptSources = normalizeChatGptSources(await this.qenvInstance.getEnvVarOnDemand('TSDOC_CHATGPT_AUTH_SOURCES'));
221
+ const envReasoningEffort = normalizeOpenAiReasoningEffort(await this.qenvInstance.getEnvVarOnDemand('TSDOC_OPENAI_REASONING_EFFORT'));
222
+ const envProviderOptions = envReasoningEffort ? {
223
+ openai: {
224
+ reasoningEffort: envReasoningEffort,
225
+ },
226
+ } as plugins.smartai.TSmartAiProviderOptions : undefined;
227
+ const argvConfig: IAiDocConfig = {
228
+ provider: normalizeProvider(this.argvArg?.provider),
229
+ model: typeof this.argvArg?.model === 'string' ? this.argvArg.model : undefined,
230
+ authMode: normalizeAuthMode(this.argvArg?.authMode),
231
+ chatGptAuthSources: normalizeChatGptSources(this.argvArg?.chatGptAuthSources),
232
+ };
233
+ const provider = argvConfig.provider ?? envProvider ?? normalizeProvider(projectConfig.provider) ?? defaultAiDocConfig.provider;
234
+ const model = argvConfig.model ?? envModel ?? projectConfig.model ?? defaultAiDocConfig.model;
235
+ const authMode = argvConfig.authMode ?? envAuthMode ?? normalizeAuthMode(projectConfig.authMode) ?? defaultAiDocConfig.authMode;
236
+ const chatGptAuthSources = argvConfig.chatGptAuthSources
237
+ ?? envChatGptSources
238
+ ?? normalizeChatGptSources(projectConfig.chatGptAuthSources)
239
+ ?? defaultAiDocConfig.chatGptAuthSources;
240
+ const providerOptions = mergeProviderOptions(
241
+ mergeProviderOptions(defaultAiDocConfig.providerOptions, projectConfig.providerOptions),
242
+ envProviderOptions,
243
+ );
244
+
245
+ return {
246
+ ...defaultAiDocConfig,
247
+ ...projectConfig,
248
+ provider,
249
+ model,
250
+ authMode,
251
+ chatGptAuthSources,
252
+ chatGptAuthWriteBack: {
253
+ ...defaultAiDocConfig.chatGptAuthWriteBack,
254
+ ...(projectConfig.chatGptAuthWriteBack ?? {}),
255
+ },
256
+ providerOptions,
257
+ cache: projectConfig.cache ?? defaultAiDocConfig.cache,
258
+ };
32
259
  }
33
260
 
34
261
  public async start() {
35
- // lets care about prerequisites
36
262
  this.aidocInteract = new plugins.smartinteract.SmartInteract();
37
263
  this.qenvInstance = new plugins.qenv.Qenv();
38
- const openaiTokenFromEnv = await this.qenvInstance.getEnvVarOnDemand('OPENAI_TOKEN');
39
- if (openaiTokenFromEnv) {
40
- this.openaiToken = openaiTokenFromEnv;
41
- } else {
42
- // Migrate old KV store path to new path if needed
43
- const homeDir = plugins.smartpath.get.home();
44
- const oldKvPath = plugins.path.join(homeDir, '.smartconfig/kv/tsdoc.json');
45
- const newKvDir = plugins.path.join(homeDir, '.smartconfig/kv/@git.zone');
46
- const newKvPath = plugins.path.join(newKvDir, 'tsdoc.json');
47
- if (
48
- await plugins.fsInstance.file(oldKvPath).exists() &&
49
- !(await plugins.fsInstance.file(newKvPath).exists())
50
- ) {
51
- console.log('Migrating tsdoc KeyValueStore to @git.zone/tsdoc...');
52
- await plugins.fsInstance.directory(newKvDir).recursive().create();
53
- await plugins.fsInstance.file(oldKvPath).copy(newKvPath);
54
- await plugins.fsInstance.file(oldKvPath).delete();
55
- console.log('Migration complete: tsdoc.json -> @git.zone/tsdoc.json');
56
- }
264
+ await this.migrateKeyValueStore();
265
+ this.smartconfigKV = new plugins.smartconfig.KeyValueStore({
266
+ typeArg: 'userHomeDir',
267
+ identityArg: '@git.zone/tsdoc',
268
+ mandatoryKeys: ['OPENAI_TOKEN'],
269
+ });
270
+ this.config = await this.resolveConfig();
57
271
 
58
- this.smartconfigKV = new plugins.smartconfig.KeyValueStore({
59
- typeArg: 'userHomeDir',
60
- identityArg: '@git.zone/tsdoc',
61
- mandatoryKeys: ['OPENAI_TOKEN'],
272
+ let openAiChatGptAuth: plugins.smartai.IOpenAiChatGptTokenData | undefined;
273
+ if (this.config.provider === 'openai' && this.config.authMode !== 'apiKey') {
274
+ const resolvedAuth = await plugins.smartaiOpenAiChatGptAuth.resolveOpenAiChatGptAuth({
275
+ sources: this.config.chatGptAuthSources,
276
+ refresh: 'ifNeeded',
277
+ writeBack: this.config.chatGptAuthWriteBack,
62
278
  });
279
+ if (resolvedAuth) {
280
+ openAiChatGptAuth = resolvedAuth.tokenData;
281
+ this.selectedAuthSource = resolvedAuth.source;
282
+ logger.log('info', `Using OpenAI ChatGPT auth from ${resolvedAuth.source}.`);
283
+ } else if (this.config.authMode === 'chatgpt') {
284
+ openAiChatGptAuth = await this.runChatGptDeviceLogin();
285
+ this.selectedAuthSource = 'smartai';
286
+ }
287
+ }
63
288
 
64
- const missingKeys = await this.smartconfigKV.getMissingMandatoryKeys();
65
- if (missingKeys.length > 0) {
66
- // lets try argv
67
- if (this.argvArg?.OPENAI_TOKEN) {
68
- this.openaiToken = this.argvArg.OPENAI_TOKEN;
69
- } else {
70
- // lets try smartinteract
71
- // wait for a second until OpenAI fixes punycode problem...
72
- await plugins.smartdelay.delayFor(1000);
73
- const answerObject = await this.aidocInteract.askQuestion({
74
- type: 'input',
75
- message: `Please provide your OpenAI token. This will be persisted in your home directory.`,
76
- name: 'OPENAI_TOKEN',
77
- default: '',
78
- });
79
- this.openaiToken = answerObject.value;
289
+ let apiKey: string | undefined;
290
+ if (!openAiChatGptAuth) {
291
+ apiKey = await this.resolveApiKey(this.config.provider);
292
+ if (!apiKey && this.config.provider === 'openai' && this.config.authMode === 'auto' && this.isInteractive) {
293
+ openAiChatGptAuth = await this.runChatGptDeviceLogin();
294
+ this.selectedAuthSource = 'smartai';
295
+ }
296
+ if (!apiKey && !openAiChatGptAuth && this.config.provider === 'openai' && this.config.authMode !== 'chatgpt') {
297
+ apiKey = await this.promptForOpenAiToken();
298
+ }
299
+ if (apiKey) {
300
+ this.selectedAuthSource = 'apiKey';
301
+ if (this.config.provider === 'openai') {
302
+ this.openaiToken = apiKey;
80
303
  }
81
-
82
- this.printSanitizedToken();
83
- await this.smartconfigKV.writeKey('OPENAI_TOKEN', this.openaiToken);
84
304
  }
85
305
  }
86
- if (!this.openaiToken && this.smartconfigKV) {
87
- this.openaiToken = await this.smartconfigKV.readKey('OPENAI_TOKEN');
88
- }
89
306
 
90
- // Create model using getModel()
91
- this.model = plugins.smartai.getModel({
92
- provider: 'openai',
93
- model: 'gpt-5.4',
94
- apiKey: this.openaiToken,
307
+ const setup = plugins.smartai.getModelSetup({
308
+ provider: this.config.provider,
309
+ model: this.config.model,
310
+ apiKey,
311
+ openAiChatGptAuth,
312
+ providerOptions: this.config.providerOptions,
313
+ promptCaching: this.config.promptCaching,
314
+ baseUrl: this.config.baseUrl,
315
+ ollamaOptions: this.config.ollamaOptions,
95
316
  });
317
+ this.model = setup.model;
318
+ this.providerOptions = setup.providerOptions;
96
319
  }
97
320
 
98
321
  public async stop() {
@@ -103,6 +326,35 @@ export class AiDoc {
103
326
  return this.openaiToken;
104
327
  }
105
328
 
329
+ public async runAgent(options: Omit<plugins.smartagent.IAgentRunOptions, 'model'> & {
330
+ projectDir: string;
331
+ taskName: string;
332
+ useCompaction?: boolean;
333
+ }): Promise<plugins.smartagent.IAgentRunResult> {
334
+ const providerOptions = this.config.provider === 'openai' && this.selectedAuthSource !== 'apiKey' && options.system
335
+ ? mergeProviderOptions(this.providerOptions, {
336
+ openai: {
337
+ instructions: options.system,
338
+ },
339
+ })
340
+ : this.providerOptions;
341
+ const result = await plugins.smartagent.runAgent({
342
+ ...options,
343
+ model: this.model,
344
+ providerOptions,
345
+ cache: this.config.cache,
346
+ sessionId: options.sessionId ?? `tsdoc:${options.taskName}:${plugins.path.resolve(options.projectDir)}`,
347
+ onContextOverflow: options.useCompaction
348
+ ? async (messages) => plugins.smartagentCompaction.compactMessages(this.model, messages)
349
+ : options.onContextOverflow,
350
+ });
351
+ logger.log(
352
+ 'info',
353
+ `[${options.taskName}] tokens input=${result.usage.inputTokens} output=${result.usage.outputTokens} total=${result.usage.totalTokens} cacheRead=${result.usage.cacheReadTokens} cacheWrite=${result.usage.cacheWriteTokens}`,
354
+ );
355
+ return result;
356
+ }
357
+
106
358
  public async buildReadme(projectDirArg: string) {
107
359
  const readmeInstance = new aiDocsClasses.Readme(this, projectDirArg);
108
360
  return await readmeInstance.build();
@@ -160,24 +160,18 @@ export class DiffProcessor {
160
160
 
161
161
  const lines = diffString.split('\n');
162
162
  let filepath = '';
163
+ let oldPath = '';
164
+ let newPath = '';
163
165
  let status: 'added' | 'modified' | 'deleted' = 'modified';
164
166
  let linesAdded = 0;
165
167
  let linesRemoved = 0;
166
168
 
167
169
  // Parse diff header to extract filepath and status
168
170
  for (const line of lines) {
169
- if (line.startsWith('--- a/')) {
170
- filepath = line.substring(6);
171
- } else if (line.startsWith('+++ b/')) {
172
- const newPath = line.substring(6);
173
- if (newPath === '/dev/null') {
174
- status = 'deleted';
175
- } else if (filepath === '/dev/null') {
176
- status = 'added';
177
- filepath = newPath;
178
- } else {
179
- filepath = newPath;
180
- }
171
+ if (line.startsWith('--- ')) {
172
+ oldPath = this.normalizeDiffPath(line.substring(4));
173
+ } else if (line.startsWith('+++ ')) {
174
+ newPath = this.normalizeDiffPath(line.substring(4));
181
175
  } else if (line.startsWith('+') && !line.startsWith('+++')) {
182
176
  linesAdded++;
183
177
  } else if (line.startsWith('-') && !line.startsWith('---')) {
@@ -185,6 +179,16 @@ export class DiffProcessor {
185
179
  }
186
180
  }
187
181
 
182
+ if (oldPath === '/dev/null') {
183
+ status = 'added';
184
+ filepath = newPath;
185
+ } else if (newPath === '/dev/null') {
186
+ status = 'deleted';
187
+ filepath = oldPath;
188
+ } else {
189
+ filepath = newPath || oldPath;
190
+ }
191
+
188
192
  const totalLines = linesAdded + linesRemoved;
189
193
  const estimatedTokens = Math.ceil(diffString.length / 4);
190
194
 
@@ -199,6 +203,12 @@ export class DiffProcessor {
199
203
  };
200
204
  }
201
205
 
206
+ private normalizeDiffPath(pathArg: string): string {
207
+ const cleanPath = pathArg.trim().split('\t')[0].split(' ')[0];
208
+ if (cleanPath === '/dev/null') return cleanPath;
209
+ return cleanPath.replace(/^a\//, '').replace(/^b\//, '');
210
+ }
211
+
202
212
  /**
203
213
  * Prioritize files by importance (source files before build artifacts)
204
214
  */
@@ -215,7 +225,7 @@ export class DiffProcessor {
215
225
  */
216
226
  private getFileImportanceScore(filepath: string): number {
217
227
  // Source files - highest priority
218
- if (filepath.match(/^(src|lib|app|components|pages|api)\//)) {
228
+ if (filepath.match(/^(ts|ts_web|src|lib|app|components|pages|api)\//)) {
219
229
  return 100;
220
230
  }
221
231