@git.zone/tsdoc 2.0.5 → 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.
- package/.smartconfig.json +21 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aidocs_classes/commit.d.ts +3 -0
- package/dist_ts/aidocs_classes/commit.js +122 -115
- package/dist_ts/aidocs_classes/description.js +58 -14
- package/dist_ts/aidocs_classes/projectcontext.d.ts +2 -1
- package/dist_ts/aidocs_classes/projectcontext.js +24 -8
- package/dist_ts/aidocs_classes/readme.js +28 -20
- package/dist_ts/classes.aidoc.d.ts +39 -1
- package/dist_ts/classes.aidoc.js +260 -62
- package/dist_ts/classes.diffprocessor.d.ts +1 -0
- package/dist_ts/classes.diffprocessor.js +25 -16
- package/dist_ts/classes.typedoc.js +33 -11
- package/dist_ts/cli.js +69 -11
- package/dist_ts/helpers.agenttools.d.ts +2 -0
- package/dist_ts/helpers.agenttools.js +112 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/plugins.d.ts +5 -2
- package/dist_ts/plugins.js +6 -3
- package/package.json +10 -11
- package/readme.md +35 -14
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aidocs_classes/commit.ts +134 -134
- package/ts/aidocs_classes/description.ts +60 -17
- package/ts/aidocs_classes/projectcontext.ts +24 -24
- package/ts/aidocs_classes/readme.ts +28 -21
- package/ts/classes.aidoc.ts +315 -63
- package/ts/classes.diffprocessor.ts +23 -13
- package/ts/classes.typedoc.ts +35 -12
- package/ts/cli.ts +72 -10
- package/ts/helpers.agenttools.ts +125 -0
- package/ts/index.ts +1 -0
- package/ts/plugins.ts +6 -1
|
@@ -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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
88
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
171
|
-
|
|
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
|
|
package/ts/classes.aidoc.ts
CHANGED
|
@@ -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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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('---
|
|
170
|
-
|
|
171
|
-
} else if (line.startsWith('+++
|
|
172
|
-
|
|
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
|
|