@git.zone/tsdoc 2.0.5 → 2.1.0

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,8 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import { AiDoc } from '../classes.aidoc.js';
3
- import { ProjectContext } from './projectcontext.js';
4
3
  import { DiffProcessor } from '../classes.diffprocessor.js';
5
4
  import { logger } from '../logging.js';
5
+ import { createReadOnlyFileSystemTools } from '../helpers.agenttools.js';
6
6
 
7
7
  // Token budget configuration for OpenAI API limits
8
8
  const TOKEN_BUDGET = {
@@ -32,6 +32,88 @@ export interface INextCommitObject {
32
32
  changelog?: string; // the changelog for the next version
33
33
  }
34
34
 
35
+ export class NoChangesError extends Error {
36
+ constructor(message = 'No uncommitted changes found for commit recommendation.') {
37
+ super(message);
38
+ this.name = 'NoChangesError';
39
+ }
40
+ }
41
+
42
+ const normalizeLevel = (level: unknown): INextCommitObject['recommendedNextVersionLevel'] => {
43
+ if (level === 'fix' || level === 'feat' || level === 'BREAKING CHANGE') return level;
44
+ throw new Error('recommendedNextVersionLevel must be fix, feat, or BREAKING CHANGE.');
45
+ };
46
+
47
+ const stripConventionalPrefix = (message: string): string => {
48
+ return message.replace(/^(fix|feat|BREAKING CHANGE)(\([^)]*\))?:\s*/i, '').trim();
49
+ };
50
+
51
+ const extractJsonObject = (text: string): Record<string, any> => {
52
+ const jsonString = text
53
+ .replace(/```json\n?/gi, '')
54
+ .replace(/```\n?/gi, '')
55
+ .match(/\{[\s\S]*\}/)?.[0];
56
+ if (!jsonString) {
57
+ throw new Error(`Could not find JSON object in result: ${text.substring(0, 100)}...`);
58
+ }
59
+ return JSON.parse(jsonString) as Record<string, any>;
60
+ };
61
+
62
+ const bumpVersion = (currentVersion: string, level: INextCommitObject['recommendedNextVersionLevel']): string => {
63
+ const [majorString, minorString, patchString] = currentVersion.split(/[+-]/)[0].split('.');
64
+ let major = Number.parseInt(majorString, 10);
65
+ let minor = Number.parseInt(minorString, 10);
66
+ let patch = Number.parseInt(patchString, 10);
67
+ if (!Number.isFinite(major)) major = 0;
68
+ if (!Number.isFinite(minor)) minor = 0;
69
+ if (!Number.isFinite(patch)) patch = 0;
70
+
71
+ if (level === 'BREAKING CHANGE') {
72
+ return `${major + 1}.0.0`;
73
+ }
74
+ if (level === 'feat') {
75
+ return `${major}.${minor + 1}.0`;
76
+ }
77
+ return `${major}.${minor}.${patch + 1}`;
78
+ };
79
+
80
+ const parseCommitResult = (text: string, currentVersion: string): INextCommitObject => {
81
+ const parsed = extractJsonObject(text);
82
+ const level = normalizeLevel(parsed.recommendedNextVersionLevel);
83
+ const scope = typeof parsed.recommendedNextVersionScope === 'string'
84
+ ? parsed.recommendedNextVersionScope.trim()
85
+ : '';
86
+ const message = typeof parsed.recommendedNextVersionMessage === 'string'
87
+ ? stripConventionalPrefix(parsed.recommendedNextVersionMessage)
88
+ : '';
89
+ const details = Array.isArray(parsed.recommendedNextVersionDetails)
90
+ ? parsed.recommendedNextVersionDetails.filter((detail): detail is string => typeof detail === 'string').map(detail => detail.trim()).filter(Boolean)
91
+ : [];
92
+
93
+ if (!scope) throw new Error('recommendedNextVersionScope must be a non-empty string.');
94
+ if (!message) throw new Error('recommendedNextVersionMessage must be a non-empty string.');
95
+
96
+ return {
97
+ recommendedNextVersionLevel: level,
98
+ recommendedNextVersionScope: scope,
99
+ recommendedNextVersionMessage: message,
100
+ recommendedNextVersionDetails: details,
101
+ recommendedNextVersion: bumpVersion(currentVersion, level),
102
+ };
103
+ };
104
+
105
+ const buildChangelog = (
106
+ commitObject: INextCommitObject,
107
+ oldChangelog: string,
108
+ ): string => {
109
+ const dateString = new plugins.smarttime.ExtendedDate().exportToHyphedSortableDate();
110
+ const details = commitObject.recommendedNextVersionDetails.length > 0
111
+ ? `\n\n${commitObject.recommendedNextVersionDetails.map(detail => `- ${detail}`).join('\n')}`
112
+ : '';
113
+ const previous = oldChangelog.replace(/^# Changelog\n\n?/, '').replace(/^\n+/, '');
114
+ return `# Changelog\n\n## ${dateString} - ${commitObject.recommendedNextVersion} - ${commitObject.recommendedNextVersionScope}\n${commitObject.recommendedNextVersionMessage}${details}\n\n${previous}`.trimEnd() + '\n';
115
+ };
116
+
35
117
  export class Commit {
36
118
  private aiDocsRef: AiDoc;
37
119
  private projectDir: string;
@@ -94,62 +176,45 @@ export class Commit {
94
176
 
95
177
  // Pass glob patterns directly to smartgit - it handles matching internally
96
178
  const diffStringArray = await gitRepo.getUncommittedDiff(excludePatterns);
179
+ if (diffStringArray.length === 0) {
180
+ throw new NoChangesError();
181
+ }
182
+
183
+ const packageJsonPath = plugins.path.join(this.projectDir, 'package.json');
184
+ const packageJson = await plugins.fsInstance.file(packageJsonPath).exists()
185
+ ? JSON.parse(String(await plugins.fsInstance.file(packageJsonPath).encoding('utf8').read()))
186
+ : {};
187
+ const currentVersion = typeof packageJson.version === 'string' ? packageJson.version : '0.0.0';
97
188
 
98
189
  // Process diffs intelligently using DiffProcessor
99
- let processedDiffString: string;
100
-
101
- if (diffStringArray.length > 0) {
102
- // Diagnostic logging for raw diff statistics
103
- const totalChars = diffStringArray.join('\n\n').length;
104
- const estimatedTokens = Math.ceil(totalChars / 4);
105
-
106
- console.log(`Raw git diff statistics:`);
107
- console.log(` Files changed: ${diffStringArray.length}`);
108
- console.log(` Total characters: ${totalChars.toLocaleString()}`);
109
- console.log(` Estimated tokens: ${estimatedTokens.toLocaleString()}`);
110
- console.log(` Exclusion patterns: ${excludePatterns.length}`);
111
-
112
- // Calculate available tokens for diff based on total budget
113
- const maxDiffTokens = calculateMaxDiffTokens();
114
- console.log(`Token budget: ${maxDiffTokens.toLocaleString()} tokens for diff (limit: ${TOKEN_BUDGET.OPENAI_CONTEXT_LIMIT.toLocaleString()}, overhead: ${(TOKEN_BUDGET.SMARTAGENT_OVERHEAD + TOKEN_BUDGET.TASK_PROMPT_OVERHEAD).toLocaleString()})`);
115
-
116
- // Use DiffProcessor to intelligently handle large diffs
117
- const diffProcessor = new DiffProcessor({
118
- maxDiffTokens, // Dynamic based on total budget
119
- smallFileLines: 300, // Most source files are under 300 lines
120
- mediumFileLines: 800, // Only very large files get head/tail treatment
121
- sampleHeadLines: 75, // When sampling, show more context
122
- sampleTailLines: 75, // When sampling, show more context
123
- });
124
-
125
- const processedDiff = diffProcessor.processDiffs(diffStringArray);
126
- processedDiffString = diffProcessor.formatForContext(processedDiff);
127
-
128
- console.log(`Processed diff statistics:`);
129
- console.log(` Full diffs: ${processedDiff.fullDiffs.length} files`);
130
- console.log(` Summarized: ${processedDiff.summarizedDiffs.length} files`);
131
- console.log(` Metadata only: ${processedDiff.metadataOnly.length} files`);
132
- console.log(` Final tokens: ${processedDiff.totalTokens.toLocaleString()}`);
133
-
134
- if (estimatedTokens > 50000) {
135
- console.log(`DiffProcessor reduced token usage: ${estimatedTokens.toLocaleString()} -> ${processedDiff.totalTokens.toLocaleString()}`);
136
- }
137
-
138
- // Validate total tokens won't exceed limit
139
- const totalEstimatedTokens = processedDiff.totalTokens
140
- + TOKEN_BUDGET.SMARTAGENT_OVERHEAD
141
- + TOKEN_BUDGET.TASK_PROMPT_OVERHEAD;
142
-
143
- if (totalEstimatedTokens > TOKEN_BUDGET.OPENAI_CONTEXT_LIMIT - TOKEN_BUDGET.SAFETY_MARGIN) {
144
- console.log(`Warning: Estimated tokens (${totalEstimatedTokens.toLocaleString()}) approaching limit`);
145
- console.log(` Consider splitting into smaller commits`);
146
- }
147
- } else {
148
- processedDiffString = 'No changes.';
190
+ const totalChars = diffStringArray.join('\n\n').length;
191
+ const estimatedTokens = Math.ceil(totalChars / 4);
192
+
193
+ logger.log('info', `Raw git diff: ${diffStringArray.length} files, ${estimatedTokens.toLocaleString()} estimated tokens, ${excludePatterns.length} exclusions.`);
194
+
195
+ const maxDiffTokens = calculateMaxDiffTokens();
196
+ const diffProcessor = new DiffProcessor({
197
+ maxDiffTokens,
198
+ smallFileLines: 300,
199
+ mediumFileLines: 800,
200
+ sampleHeadLines: 75,
201
+ sampleTailLines: 75,
202
+ });
203
+
204
+ const processedDiff = diffProcessor.processDiffs(diffStringArray);
205
+ const processedDiffString = diffProcessor.formatForContext(processedDiff);
206
+
207
+ logger.log('info', `Processed diff: ${processedDiff.fullDiffs.length} full, ${processedDiff.summarizedDiffs.length} summarized, ${processedDiff.metadataOnly.length} metadata-only, ${processedDiff.totalTokens.toLocaleString()} tokens.`);
208
+
209
+ const totalEstimatedTokens = processedDiff.totalTokens
210
+ + TOKEN_BUDGET.SMARTAGENT_OVERHEAD
211
+ + TOKEN_BUDGET.TASK_PROMPT_OVERHEAD;
212
+
213
+ if (totalEstimatedTokens > TOKEN_BUDGET.OPENAI_CONTEXT_LIMIT - TOKEN_BUDGET.SAFETY_MARGIN) {
214
+ logger.log('warn', `Estimated tokens (${totalEstimatedTokens.toLocaleString()}) approach the model context limit. Consider splitting into smaller commits.`);
149
215
  }
150
216
 
151
- // Use runAgent for commit message generation with filesystem tool
152
- const fsTools = plugins.smartagentTools.filesystemTool({ rootDir: this.projectDir });
217
+ const fsTools = await createReadOnlyFileSystemTools(this.projectDir);
153
218
 
154
219
  const commitSystemPrompt = `
155
220
  You create commit messages for git commits following semantic versioning conventions.
@@ -188,7 +253,7 @@ Here is the structure of the JSON you must return:
188
253
  "recommendedNextVersionScope": "string",
189
254
  "recommendedNextVersionMessage": "string (ONLY the description body WITHOUT the type(scope): prefix - e.g. 'bump dependency to ^1.2.6' NOT 'fix(deps): bump dependency to ^1.2.6')",
190
255
  "recommendedNextVersionDetails": ["string"],
191
- "recommendedNextVersion": "x.x.x"
256
+ "recommendedNextVersion": "x.x.x (will be verified deterministically)"
192
257
  }
193
258
 
194
259
  For recommendedNextVersionDetails, only add entries that have obvious value to the reader.
@@ -202,97 +267,32 @@ Analyze these changes and output the JSON commit message object.
202
267
 
203
268
  logger.log('info', 'Starting commit message generation with agent...');
204
269
 
205
- const commitResult = await plugins.smartagent.runAgent({
206
- model: this.aiDocsRef.model,
270
+ const commitResult = await this.aiDocsRef.runAgent({
271
+ taskName: 'commit',
272
+ projectDir: this.projectDir,
207
273
  prompt: commitTaskPrompt,
208
274
  system: commitSystemPrompt,
209
275
  tools: fsTools,
210
276
  maxSteps: 10,
277
+ maxValidationRetries: 1,
278
+ validateCompletion: (result) => {
279
+ try {
280
+ parseCommitResult(result.text, currentVersion);
281
+ } catch (error) {
282
+ return `Return only valid JSON matching the requested commit object schema. Error: ${error instanceof Error ? error.message : String(error)}`;
283
+ }
284
+ },
211
285
  onToolCall: (toolName) => logger.log('info', `[Commit] Tool call: ${toolName}`),
212
286
  });
213
287
 
214
- // Extract JSON from result - handle cases where AI adds text around it
215
- let jsonString = commitResult.text
216
- .replace(/```json\n?/gi, '')
217
- .replace(/```\n?/gi, '');
218
-
219
- // Try to find JSON object in the result
220
- const jsonMatch = jsonString.match(/\{[\s\S]*\}/);
221
- if (!jsonMatch) {
222
- throw new Error(`Could not find JSON object in result: ${jsonString.substring(0, 100)}...`);
223
- }
224
- jsonString = jsonMatch[0];
225
-
226
- const resultObject: INextCommitObject = JSON.parse(jsonString);
288
+ const resultObject = parseCommitResult(commitResult.text, currentVersion);
227
289
 
228
290
  const previousChangelogPath = plugins.path.join(this.projectDir, 'changelog.md');
229
- let previousChangelog: plugins.smartfile.SmartFile | undefined;
291
+ let oldChangelog = '';
230
292
  if (await plugins.fsInstance.file(previousChangelogPath).exists()) {
231
- previousChangelog = await plugins.smartfileFactory.fromFilePath(previousChangelogPath);
293
+ oldChangelog = String(await plugins.fsInstance.file(previousChangelogPath).encoding('utf8').read());
232
294
  }
233
-
234
- if (!previousChangelog) {
235
- // lets build the changelog based on that
236
- const commitMessages = await gitRepo.getAllCommitMessages();
237
- console.log(JSON.stringify(commitMessages, null, 2));
238
-
239
- const changelogSystemPrompt = `
240
- You generate changelog.md files for software projects.
241
-
242
- RULES:
243
- - Changelog must follow proper markdown format with ## headers for each version
244
- - Entries must be chronologically ordered (newest first)
245
- - Version ranges for trivial commits should be properly summarized
246
- - No duplicate or empty entries
247
- - Format: ## yyyy-mm-dd - x.x.x - scope
248
- `;
249
-
250
- const changelogTaskPrompt = `
251
- You are building a changelog.md file for the project.
252
- Omit commits and versions that lack relevant changes, but make sure to mention them as a range with a summarizing message instead.
253
-
254
- A changelog entry should look like this:
255
-
256
- ## yyyy-mm-dd - x.x.x - scope here
257
- main description here
258
-
259
- - detailed bullet points follow
260
-
261
- You are given:
262
- * the commit messages of the project
263
-
264
- Only return the changelog file content, so it can be written directly to changelog.md.
265
-
266
- Here are the commit messages:
267
-
268
- ${JSON.stringify(commitMessages, null, 2)}
269
- `;
270
-
271
- const changelogResult = await plugins.smartagent.runAgent({
272
- model: this.aiDocsRef.model,
273
- prompt: changelogTaskPrompt,
274
- system: changelogSystemPrompt,
275
- maxSteps: 1,
276
- onToolCall: (toolName) => logger.log('info', `[Changelog] Tool call: ${toolName}`),
277
- });
278
-
279
- previousChangelog = plugins.smartfileFactory.fromString(
280
- previousChangelogPath,
281
- changelogResult.text.replaceAll('```markdown', '').replaceAll('```', ''),
282
- 'utf8'
283
- );
284
- }
285
-
286
- let oldChangelog = previousChangelog.contents.toString().replace('# Changelog\n\n', '');
287
- if (oldChangelog.startsWith('\n')) {
288
- oldChangelog = oldChangelog.replace('\n', '');
289
- }
290
- let newDateString = new plugins.smarttime.ExtendedDate().exportToHyphedSortableDate();
291
- let newChangelog = `# Changelog\n\n${`## ${newDateString} - {{nextVersion}} - {{nextVersionScope}}
292
- {{nextVersionMessage}}
293
-
294
- {{nextVersionDetails}}`}\n\n${oldChangelog}`;
295
- resultObject.changelog = newChangelog;
295
+ resultObject.changelog = buildChangelog(resultObject, oldChangelog);
296
296
 
297
297
  return resultObject;
298
298
  }
@@ -2,12 +2,38 @@ import type { AiDoc } from '../classes.aidoc.js';
2
2
  import * as plugins from '../plugins.js';
3
3
  import { ProjectContext } from './projectcontext.js';
4
4
  import { logger } from '../logging.js';
5
+ import { createReadOnlyFileSystemTools } from '../helpers.agenttools.js';
5
6
 
6
7
  interface IDescriptionInterface {
7
8
  description: string;
8
9
  keywords: string[];
9
10
  }
10
11
 
12
+ const parseDescriptionJson = (text: string): IDescriptionInterface => {
13
+ const jsonString = text
14
+ .replace(/```json\n?/gi, '')
15
+ .replace(/```\n?/gi, '')
16
+ .match(/\{[\s\S]*\}/)?.[0] ?? text;
17
+ const parsed = JSON.parse(jsonString) as IDescriptionInterface;
18
+ if (typeof parsed.description !== 'string' || parsed.description.trim().length === 0) {
19
+ throw new Error('description must be a non-empty string.');
20
+ }
21
+ if (!Array.isArray(parsed.keywords) || parsed.keywords.some(keyword => typeof keyword !== 'string')) {
22
+ throw new Error('keywords must be an array of strings.');
23
+ }
24
+ return {
25
+ description: parsed.description.trim(),
26
+ keywords: parsed.keywords.map(keyword => keyword.trim()).filter(Boolean),
27
+ };
28
+ };
29
+
30
+ const ensureModuleConfig = (config: Record<string, any>): Record<string, any> => {
31
+ if (!config.module || typeof config.module !== 'object') {
32
+ config.module = {};
33
+ }
34
+ return config.module;
35
+ };
36
+
11
37
  export class Description {
12
38
  // INSTANCE
13
39
  private aiDocsRef: AiDoc;
@@ -19,8 +45,7 @@ export class Description {
19
45
  }
20
46
 
21
47
  public async build() {
22
- // Use runAgent with filesystem tool for agent-driven exploration
23
- const fsTools = plugins.smartagentTools.filesystemTool({ rootDir: this.projectDir });
48
+ const fsTools = await createReadOnlyFileSystemTools(this.projectDir);
24
49
 
25
50
  const descriptionSystemPrompt = `
26
51
  You create project descriptions and keywords for npm packages.
@@ -28,7 +53,7 @@ You create project descriptions and keywords for npm packages.
28
53
  You have access to filesystem tools to explore the project.
29
54
 
30
55
  IMPORTANT RULES:
31
- - Only READ files (package.json, .smartconfig.json, source files in ts/)
56
+ - Only READ files (package.json, .smartconfig.json, source files in ts/ and ts_web/)
32
57
  - Do NOT write, delete, or modify any files
33
58
  - Your final response must be valid JSON only
34
59
  - Description must be a clear, concise one-sentence summary
@@ -44,7 +69,7 @@ Use the filesystem tools to explore the project and understand what it does:
44
69
  1. First, use list_directory to see the project structure
45
70
  2. Read package.json to understand the package name and current description
46
71
  3. Read .smartconfig.json if it exists for additional metadata
47
- 4. Read key source files in ts/ directory to understand the implementation
72
+ 4. Read key source files in ts/ and ts_web/ directories to understand the implementation
48
73
 
49
74
  Then generate a description and keywords based on your exploration.
50
75
 
@@ -61,19 +86,26 @@ Don't wrap the JSON in \`\`\`json\`\`\` - just return the raw JSON object.
61
86
 
62
87
  logger.log('info', 'Starting description generation with agent...');
63
88
 
64
- const descriptionResult = await plugins.smartagent.runAgent({
65
- model: this.aiDocsRef.model,
89
+ const descriptionResult = await this.aiDocsRef.runAgent({
90
+ taskName: 'description',
91
+ projectDir: this.projectDir,
66
92
  prompt: descriptionTaskPrompt,
67
93
  system: descriptionSystemPrompt,
68
94
  tools: fsTools,
69
95
  maxSteps: 15,
96
+ useCompaction: true,
97
+ maxValidationRetries: 1,
98
+ validateCompletion: (result) => {
99
+ try {
100
+ parseDescriptionJson(result.text);
101
+ } catch (error) {
102
+ return `Return only valid JSON matching { "description": string, "keywords": string[] }. Error: ${error instanceof Error ? error.message : String(error)}`;
103
+ }
104
+ },
70
105
  onToolCall: (toolName) => logger.log('info', `[Description] Tool call: ${toolName}`),
71
106
  });
72
107
 
73
- console.log(descriptionResult.text);
74
- const resultObject: IDescriptionInterface = JSON.parse(
75
- descriptionResult.text.replace('```json', '').replace('```', ''),
76
- );
108
+ const resultObject = parseDescriptionJson(descriptionResult.text);
77
109
 
78
110
  // Use ProjectContext to get file handles for writing
79
111
  const projectContext = new ProjectContext(this.projectDir);
@@ -81,12 +113,23 @@ Don't wrap the JSON in \`\`\`json\`\`\` - just return the raw JSON object.
81
113
 
82
114
  // Update smartconfig.json
83
115
  const smartconfigJson = files.smartfilesSmartconfigJSON;
84
- const smartconfigJsonContent = JSON.parse(smartconfigJson.contents.toString());
85
-
86
- smartconfigJsonContent['gitzone'].module.description = resultObject.description;
87
- smartconfigJsonContent['gitzone'].module.keywords = resultObject.keywords;
88
-
89
- smartconfigJson.contents = Buffer.from(JSON.stringify(smartconfigJsonContent, null, 2));
116
+ const smartconfigJsonContent = smartconfigJson.contents.length > 0
117
+ ? JSON.parse(smartconfigJson.contents.toString())
118
+ : {};
119
+ if (!smartconfigJsonContent['@git.zone/cli'] || typeof smartconfigJsonContent['@git.zone/cli'] !== 'object') {
120
+ smartconfigJsonContent['@git.zone/cli'] = {};
121
+ }
122
+ const modernModuleConfig = ensureModuleConfig(smartconfigJsonContent['@git.zone/cli']);
123
+ modernModuleConfig.description = resultObject.description;
124
+ modernModuleConfig.keywords = resultObject.keywords;
125
+
126
+ if (smartconfigJsonContent.gitzone && typeof smartconfigJsonContent.gitzone === 'object') {
127
+ const legacyModuleConfig = ensureModuleConfig(smartconfigJsonContent.gitzone);
128
+ legacyModuleConfig.description = resultObject.description;
129
+ legacyModuleConfig.keywords = resultObject.keywords;
130
+ }
131
+
132
+ smartconfigJson.contents = Buffer.from(`${JSON.stringify(smartconfigJsonContent, null, 2)}\n`);
90
133
  await smartconfigJson.write();
91
134
 
92
135
  // Update package.json
@@ -94,7 +137,7 @@ Don't wrap the JSON in \`\`\`json\`\`\` - just return the raw JSON object.
94
137
  const packageJsonContent = JSON.parse(packageJson.contents.toString());
95
138
  packageJsonContent.description = resultObject.description;
96
139
  packageJsonContent.keywords = resultObject.keywords;
97
- packageJson.contents = Buffer.from(JSON.stringify(packageJsonContent, null, 2));
140
+ packageJson.contents = Buffer.from(`${JSON.stringify(packageJsonContent, null, 2)}\n`);
98
141
  await packageJson.write();
99
142
 
100
143
  console.log(`\n======================\n`);
@@ -1,7 +1,11 @@
1
1
  import * as plugins from '../plugins.js';
2
2
 
3
3
  export class ProjectContext {
4
- public static async fromDir(dirArg: string) {}
4
+ public static async fromDir(dirArg: string) {
5
+ const projectContext = new ProjectContext(dirArg);
6
+ await projectContext.update();
7
+ return projectContext;
8
+ }
5
9
 
6
10
  // INSTANCE
7
11
  public projectDir: string;
@@ -12,30 +16,26 @@ export class ProjectContext {
12
16
  this.projectDir = projectDirArg;
13
17
  }
14
18
 
15
- public async gatherFiles() {
16
- const smartfilePackageJSON = await plugins.smartfileFactory.fromFilePath(
17
- plugins.path.join(this.projectDir, 'package.json'),
18
- this.projectDir,
19
- );
20
- const smartfilesReadme = await plugins.smartfileFactory.fromFilePath(
21
- plugins.path.join(this.projectDir, 'readme.md'),
22
- this.projectDir,
23
- );
19
+ private async getSmartFile(fileName: string): Promise<plugins.smartfile.SmartFile> {
20
+ const filePath = plugins.path.join(this.projectDir, fileName);
21
+ if (await plugins.fsInstance.file(filePath).exists()) {
22
+ return await plugins.smartfileFactory.fromFilePath(filePath, this.projectDir);
23
+ }
24
+ return plugins.smartfileFactory.fromString(filePath, '', 'utf8');
25
+ }
24
26
 
25
- const smartfilesReadmeHints = await plugins.smartfileFactory.fromFilePath(
26
- plugins.path.join(this.projectDir, 'readme.hints.md'),
27
- this.projectDir,
28
- );
29
- const smartfilesSmartconfigJSON = await plugins.smartfileFactory.fromFilePath(
30
- plugins.path.join(this.projectDir, '.smartconfig.json'),
31
- this.projectDir,
32
- );
33
- const smartfilesMod = await plugins.smartfileFactory.virtualDirectoryFromPath(
34
- this.projectDir,
35
- ).then(vd => vd.filter(f => f.relative.startsWith('ts') && f.relative.endsWith('.ts')).listFiles());
36
- const smartfilesTest = await plugins.smartfileFactory.virtualDirectoryFromPath(
37
- this.projectDir,
38
- ).then(vd => vd.filter(f => f.relative.startsWith('test/') && f.relative.endsWith('.ts')).listFiles());
27
+ public async gatherFiles() {
28
+ const smartfilePackageJSON = await this.getSmartFile('package.json');
29
+ const smartfilesReadme = await this.getSmartFile('readme.md');
30
+ const smartfilesReadmeHints = await this.getSmartFile('readme.hints.md');
31
+ const smartfilesSmartconfigJSON = await this.getSmartFile('.smartconfig.json');
32
+ const virtualDirectory = await plugins.smartfileFactory.virtualDirectoryFromPath(this.projectDir);
33
+ const smartfilesMod = await virtualDirectory
34
+ .filter(f => (f.relative.startsWith('ts/') || f.relative.startsWith('ts_web/')) && f.relative.endsWith('.ts'))
35
+ .listFiles();
36
+ const smartfilesTest = await virtualDirectory
37
+ .filter(f => f.relative.startsWith('test/') && f.relative.endsWith('.ts'))
38
+ .listFiles();
39
39
  return {
40
40
  smartfilePackageJSON,
41
41
  smartfilesReadme,
@@ -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 = await 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 = await 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