@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/readme.md CHANGED
@@ -33,6 +33,8 @@ pnpm add @git.zone/tsdoc
33
33
  | `tsdoc description` | 🏷️ Generates AI-powered description and keywords only |
34
34
  | `tsdoc commit` | 💬 Generates a semantic commit message from uncommitted changes |
35
35
  | `tsdoc typedoc` | 📚 Generates traditional TypeDoc API documentation |
36
+ | `tsdoc auth status` | Shows available ChatGPT subscription auth sources without printing secrets |
37
+ | `tsdoc auth login` | Starts a ChatGPT device-code login and stores SmartAI auth |
36
38
 
37
39
  ### 🤖 AI-Powered Documentation (`aidoc`)
38
40
 
@@ -155,26 +157,45 @@ await aidoc.start();
155
157
 
156
158
  ## Configuration
157
159
 
158
- ### OpenAI Token
160
+ ### AI Defaults
159
161
 
160
- An OpenAI API key is required for all AI features. It can be provided in three ways (checked in order):
162
+ AI features default to OpenAI `gpt-5.5` with `providerOptions.openai.reasoningEffort` set to `xhigh`. `tsdoc` uses `@push.rocks/smartai` for model setup and `@push.rocks/smartagent` for cached agent runs.
161
163
 
162
- 1. **Environment variable**: Set `OPENAI_TOKEN` in your environment or `.env` file
163
- 2. **Constructor argument**: Pass `{ OPENAI_TOKEN: 'sk-...' }` to `new AiDoc()`
164
- 3. **Interactive prompt**: On first run, tsdoc will prompt for the token and persist it
164
+ ### Authentication
165
165
 
166
- The token is persisted at `~/.smartconfig/kv/@git.zone/tsdoc.json` for subsequent runs.
166
+ Default auth mode is `auto`. For OpenAI, `tsdoc` first tries ChatGPT subscription auth and falls back to API-key auth only when no usable subscription auth exists.
167
+
168
+ Subscription auth source preference:
169
+
170
+ 1. `opencode` (`~/.local/share/opencode/auth.json`)
171
+ 2. `codex` (`~/.codex/auth.json`)
172
+ 3. `smartai` (`~/.git.zone/ide/openai-chatgpt-auth.json`)
173
+
174
+ Use `tsdoc auth status` to inspect available sources without printing tokens. Use `tsdoc auth login` to create SmartAI-managed ChatGPT auth through the device-code flow.
175
+
176
+ API-key fallback still supports `OPENAI_TOKEN`, `OPENAI_API_KEY`, constructor-provided `apiKey`, and the persisted `~/.smartconfig/kv/@git.zone/tsdoc.json` token.
167
177
 
168
178
  ### .smartconfig.json
169
179
 
170
- tsdoc uses `.smartconfig.json` for project metadata. The `tsdoc` key holds legal information that gets appended to generated READMEs:
180
+ tsdoc uses `.smartconfig.json` for project metadata. The `@git.zone/tsdoc` key holds legal and AI configuration. Legacy `tsdoc` is still read as a fallback.
171
181
 
172
182
  ```json
173
183
  {
174
- "tsdoc": {
175
- "legal": "\n## License and Legal Information\n\n..."
184
+ "@git.zone/tsdoc": {
185
+ "legal": "\n## License and Legal Information\n\n...",
186
+ "ai": {
187
+ "provider": "openai",
188
+ "model": "gpt-5.5",
189
+ "authMode": "auto",
190
+ "chatGptAuthSources": ["opencode", "codex", "smartai"],
191
+ "providerOptions": {
192
+ "openai": {
193
+ "reasoningEffort": "xhigh"
194
+ }
195
+ }
196
+ }
176
197
  },
177
- "gitzone": {
198
+ "@git.zone/cli": {
178
199
  "module": {
179
200
  "githost": "gitlab.com",
180
201
  "gitscope": "gitzone",
@@ -187,7 +208,7 @@ tsdoc uses `.smartconfig.json` for project metadata. The `tsdoc` key holds legal
187
208
  }
188
209
  ```
189
210
 
190
- The `description` command writes updated description/keywords to both `gitzone.module` in `.smartconfig.json` and to `package.json`.
211
+ The `description` command writes updated description/keywords to `@git.zone/cli.module` in `.smartconfig.json`, updates legacy `gitzone.module` when present, and updates `package.json`.
191
212
 
192
213
  ## Architecture
193
214
 
@@ -210,11 +231,11 @@ The `description` command writes updated description/keywords to both `gitzone.m
210
231
  Each documentation task (readme, commit, description) runs an autonomous AI agent via `@push.rocks/smartagent`'s `runAgent()`:
211
232
 
212
233
  1. **System prompt** defines the agent's role, constraints, and output format
213
- 2. **Filesystem tools** give the agent scoped, read-only access to the project directory
234
+ 2. **Filesystem tools** give the agent scoped, enforced read-only access to the project directory
214
235
  3. **Autonomous exploration** — the agent decides which files to read, in what order
215
236
  4. **Structured output** — README markdown, commit JSON, or description JSON
216
237
 
217
- The agents use `@push.rocks/smartai`'s `getModel()` to create a language model instance backed by OpenAI.
238
+ The agents use `@push.rocks/smartai`'s `getModelSetup()` so model setup, ChatGPT subscription auth, provider options, and cache options are preserved across agent calls.
218
239
 
219
240
  ### ⚡ Diff Processing Pipeline
220
241
 
@@ -227,7 +248,7 @@ The `DiffProcessor` handles large git diffs without blowing up token budgets:
227
248
  | **Large** | ≥ 800 lines changed | Metadata only (filepath + stats) |
228
249
 
229
250
  Files are scored by importance:
230
- - **100** — Source files (`src/`, `lib/`, `app/`, `components/`, `pages/`, `api/`)
251
+ - **100** — Source files (`ts/`, `ts_web/`, `src/`, `lib/`, `app/`, `components/`, `pages/`, `api/`)
231
252
  - **80** — Test files (`test/`, `*.test.ts`, `*.spec.ts`)
232
253
  - **70** — Interface/type files, entry points (`index.ts`, `mod.ts`)
233
254
  - **60** — Configuration files (`.json`, `.yaml`, `.config.ts`)
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@git.zone/tsdoc',
6
- version: '2.0.5',
6
+ version: '2.0.6',
7
7
  description: 'A comprehensive TypeScript documentation tool that leverages AI to generate and enhance project documentation, including dynamic README creation, API docs via TypeDoc, and smart commit message generation.'
8
8
  }
@@ -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 = 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 = 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,