@git.zone/tsdoc 2.0.4 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +11 -12
- 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
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
|
-
###
|
|
160
|
+
### AI Defaults
|
|
159
161
|
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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 `
|
|
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`)
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@git.zone/tsdoc',
|
|
6
|
-
version: '2.0.
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
206
|
-
|
|
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
|
-
|
|
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
|
|
291
|
+
let oldChangelog = '';
|
|
230
292
|
if (await plugins.fsInstance.file(previousChangelogPath).exists()) {
|
|
231
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
65
|
-
|
|
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
|
-
|
|
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 =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
smartconfigJsonContent['
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
this.projectDir
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
);
|
|
29
|
-
const smartfilesSmartconfigJSON = await
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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,
|