kodu 2.1.3 → 3.0.1
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 +24 -3
- package/bin/kodu.js +23 -0
- package/package.json +11 -67
- package/scripts/postinstall.js +69 -0
- package/AGENTS.md +0 -214
- package/__tests__/core/fs/fs.service.test.ts +0 -72
- package/__tests__/shared/cleaner/cleaner.service.test.ts +0 -102
- package/__tests__/shared/git/git.service.test.ts +0 -84
- package/__tests__/shared/tokenizer/tokenizer.service.test.ts +0 -45
- package/biome.json +0 -50
- package/dist/package.json +0 -96
- package/dist/src/app.module.d.ts +0 -2
- package/dist/src/app.module.js +0 -36
- package/dist/src/app.module.js.map +0 -1
- package/dist/src/commands/clean/clean.command.d.ts +0 -37
- package/dist/src/commands/clean/clean.command.js +0 -240
- package/dist/src/commands/clean/clean.command.js.map +0 -1
- package/dist/src/commands/clean/clean.module.d.ts +0 -2
- package/dist/src/commands/clean/clean.module.js +0 -26
- package/dist/src/commands/clean/clean.module.js.map +0 -1
- package/dist/src/commands/init/init.command.d.ts +0 -10
- package/dist/src/commands/init/init.command.js +0 -96
- package/dist/src/commands/init/init.command.js.map +0 -1
- package/dist/src/commands/init/init.module.d.ts +0 -2
- package/dist/src/commands/init/init.module.js +0 -22
- package/dist/src/commands/init/init.module.js.map +0 -1
- package/dist/src/commands/pack/pack.command.d.ts +0 -51
- package/dist/src/commands/pack/pack.command.js +0 -355
- package/dist/src/commands/pack/pack.command.js.map +0 -1
- package/dist/src/commands/pack/pack.module.d.ts +0 -2
- package/dist/src/commands/pack/pack.module.js +0 -27
- package/dist/src/commands/pack/pack.module.js.map +0 -1
- package/dist/src/core/config/config.module.d.ts +0 -2
- package/dist/src/core/config/config.module.js +0 -23
- package/dist/src/core/config/config.module.js.map +0 -1
- package/dist/src/core/config/config.schema.d.ts +0 -19
- package/dist/src/core/config/config.schema.js +0 -56
- package/dist/src/core/config/config.schema.js.map +0 -1
- package/dist/src/core/config/config.service.d.ts +0 -7
- package/dist/src/core/config/config.service.js +0 -49
- package/dist/src/core/config/config.service.js.map +0 -1
- package/dist/src/core/config/prompt.service.d.ts +0 -10
- package/dist/src/core/config/prompt.service.js +0 -80
- package/dist/src/core/config/prompt.service.js.map +0 -1
- package/dist/src/core/file-system/fs.module.d.ts +0 -2
- package/dist/src/core/file-system/fs.module.js +0 -21
- package/dist/src/core/file-system/fs.module.js.map +0 -1
- package/dist/src/core/file-system/fs.service.d.ts +0 -27
- package/dist/src/core/file-system/fs.service.js +0 -203
- package/dist/src/core/file-system/fs.service.js.map +0 -1
- package/dist/src/core/ui/ui.module.d.ts +0 -2
- package/dist/src/core/ui/ui.module.js +0 -22
- package/dist/src/core/ui/ui.module.js.map +0 -1
- package/dist/src/core/ui/ui.service.d.ts +0 -22
- package/dist/src/core/ui/ui.service.js +0 -43
- package/dist/src/core/ui/ui.service.js.map +0 -1
- package/dist/src/main.d.ts +0 -2
- package/dist/src/main.js +0 -16
- package/dist/src/main.js.map +0 -1
- package/dist/src/shared/cleaner/cleaner.service.d.ts +0 -23
- package/dist/src/shared/cleaner/cleaner.service.js +0 -223
- package/dist/src/shared/cleaner/cleaner.service.js.map +0 -1
- package/dist/src/shared/cleaner/cleaner.types.d.ts +0 -21
- package/dist/src/shared/cleaner/cleaner.types.js +0 -3
- package/dist/src/shared/cleaner/cleaner.types.js.map +0 -1
- package/dist/src/shared/constants.d.ts +0 -4
- package/dist/src/shared/constants.js +0 -113
- package/dist/src/shared/constants.js.map +0 -1
- package/dist/src/shared/deps/deps.module.d.ts +0 -2
- package/dist/src/shared/deps/deps.module.js +0 -21
- package/dist/src/shared/deps/deps.module.js.map +0 -1
- package/dist/src/shared/deps/deps.service.d.ts +0 -15
- package/dist/src/shared/deps/deps.service.js +0 -114
- package/dist/src/shared/deps/deps.service.js.map +0 -1
- package/dist/src/shared/git/git.module.d.ts +0 -2
- package/dist/src/shared/git/git.module.js +0 -21
- package/dist/src/shared/git/git.module.js.map +0 -1
- package/dist/src/shared/git/git.service.d.ts +0 -5
- package/dist/src/shared/git/git.service.js +0 -56
- package/dist/src/shared/git/git.service.js.map +0 -1
- package/dist/src/shared/tokenizer/tokenizer.module.d.ts +0 -2
- package/dist/src/shared/tokenizer/tokenizer.module.js +0 -21
- package/dist/src/shared/tokenizer/tokenizer.module.js.map +0 -1
- package/dist/src/shared/tokenizer/tokenizer.service.d.ts +0 -10
- package/dist/src/shared/tokenizer/tokenizer.service.js +0 -36
- package/dist/src/shared/tokenizer/tokenizer.service.js.map +0 -1
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- package/docs/todo.md +0 -7
- package/knip.json +0 -10
- package/kodu.json +0 -63
- package/kodu.schema.json +0 -100
- package/lefthook.yml +0 -11
- package/nest-cli.json +0 -8
- package/scripts/generate-json-schema.ts +0 -18
- package/skills/doc-gen/SKILL.md +0 -490
- package/skills/doc-gen/scripts/doc_gen.py +0 -911
- package/skills/implement-project/SKILL.md +0 -409
- package/skills/liteend-init/SKILL.md +0 -84
- package/skills/litefront-init/SKILL.md +0 -96
- package/skills/litefront-prototype/SKILL.md +0 -484
- package/skills/project-setup-standardizer/SKILL.md +0 -285
- package/skills/start/SKILL.md +0 -319
- package/skills/tech-blueprint/SKILL.md +0 -890
- package/skills/tech-blueprint/scripts/blueprint_validator.py +0 -417
- package/src/app.module.ts +0 -23
- package/src/commands/clean/clean.command.ts +0 -235
- package/src/commands/clean/clean.module.ts +0 -13
- package/src/commands/init/init.command.ts +0 -92
- package/src/commands/init/init.module.ts +0 -9
- package/src/commands/pack/pack.command.ts +0 -347
- package/src/commands/pack/pack.module.ts +0 -14
- package/src/core/config/config.module.ts +0 -10
- package/src/core/config/config.schema.ts +0 -58
- package/src/core/config/config.service.ts +0 -43
- package/src/core/config/prompt.service.ts +0 -80
- package/src/core/file-system/fs.module.ts +0 -8
- package/src/core/file-system/fs.service.ts +0 -248
- package/src/core/ui/ui.module.ts +0 -9
- package/src/core/ui/ui.service.ts +0 -39
- package/src/main.ts +0 -12
- package/src/shared/cleaner/cleaner.service.ts +0 -289
- package/src/shared/cleaner/cleaner.types.ts +0 -23
- package/src/shared/constants.ts +0 -118
- package/src/shared/deps/deps.module.ts +0 -8
- package/src/shared/deps/deps.service.ts +0 -175
- package/src/shared/git/git.module.ts +0 -8
- package/src/shared/git/git.service.ts +0 -47
- package/src/shared/tokenizer/tokenizer.module.ts +0 -8
- package/src/shared/tokenizer/tokenizer.service.ts +0 -30
- package/tsconfig.build.json +0 -7
- package/tsconfig.json +0 -28
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { Command, CommandRunner } from 'nest-commander';
|
|
4
|
-
import { UiService } from '../../core/ui/ui.service';
|
|
5
|
-
|
|
6
|
-
const GITIGNORE_ENTRY = '.kodu/context.txt';
|
|
7
|
-
|
|
8
|
-
const DEFAULT_KODU_JSON = {
|
|
9
|
-
$schema:
|
|
10
|
-
'https://raw.githubusercontent.com/anomalyco/kodu/main/kodu.schema.json',
|
|
11
|
-
cleaner: {
|
|
12
|
-
whitelist: ['//!'],
|
|
13
|
-
keepJSDoc: true,
|
|
14
|
-
useGitignore: true,
|
|
15
|
-
ignore: [],
|
|
16
|
-
},
|
|
17
|
-
packer: {
|
|
18
|
-
ignore: [
|
|
19
|
-
'package-lock.json',
|
|
20
|
-
'yarn.lock',
|
|
21
|
-
'pnpm-lock.yaml',
|
|
22
|
-
'.git',
|
|
23
|
-
'.kodu',
|
|
24
|
-
'node_modules',
|
|
25
|
-
'dist',
|
|
26
|
-
'coverage',
|
|
27
|
-
],
|
|
28
|
-
useGitignore: true,
|
|
29
|
-
contentBasedBinaryDetection: false,
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
@Command({ name: 'init', description: 'Initialize kodu configuration' })
|
|
34
|
-
export class InitCommand extends CommandRunner {
|
|
35
|
-
constructor(private readonly ui: UiService) {
|
|
36
|
-
super();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async run(): Promise<void> {
|
|
40
|
-
await this.ensureKoduJson();
|
|
41
|
-
await this.updateGitignore();
|
|
42
|
-
this.ui.log.success('Done.');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private async ensureKoduJson(): Promise<void> {
|
|
46
|
-
const configPath = path.join(process.cwd(), 'kodu.json');
|
|
47
|
-
|
|
48
|
-
if (await this.exists(configPath)) {
|
|
49
|
-
this.ui.log.info('kodu.json already exists');
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
await fs.writeFile(
|
|
54
|
-
configPath,
|
|
55
|
-
`${JSON.stringify(DEFAULT_KODU_JSON, null, 2)}\n`,
|
|
56
|
-
'utf8',
|
|
57
|
-
);
|
|
58
|
-
this.ui.log.success('Created kodu.json');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private async updateGitignore(): Promise<void> {
|
|
62
|
-
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
63
|
-
|
|
64
|
-
if (!(await this.exists(gitignorePath))) {
|
|
65
|
-
this.ui.log.warn('.gitignore not found, skipping.');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const content = await fs.readFile(gitignorePath, 'utf8');
|
|
70
|
-
const lines = content.split(/\r?\n/);
|
|
71
|
-
|
|
72
|
-
if (lines.some((line) => line.trim() === GITIGNORE_ENTRY)) {
|
|
73
|
-
this.ui.log.info(`${GITIGNORE_ENTRY} already in .gitignore`);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const trimmed = content.trimEnd();
|
|
78
|
-
const next =
|
|
79
|
-
trimmed.length > 0 ? `${trimmed}\n${GITIGNORE_ENTRY}` : GITIGNORE_ENTRY;
|
|
80
|
-
await fs.writeFile(gitignorePath, `${next}\n`, 'utf8');
|
|
81
|
-
this.ui.log.success(`Added ${GITIGNORE_ENTRY} to .gitignore`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private async exists(target: string): Promise<boolean> {
|
|
85
|
-
try {
|
|
86
|
-
await fs.access(target);
|
|
87
|
-
return true;
|
|
88
|
-
} catch {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import clipboard from 'clipboardy';
|
|
4
|
-
import { Command, CommandRunner, Option } from 'nest-commander';
|
|
5
|
-
import { ConfigService } from '../../core/config/config.service';
|
|
6
|
-
import { PromptService } from '../../core/config/prompt.service';
|
|
7
|
-
import { FsService } from '../../core/file-system/fs.service';
|
|
8
|
-
import { UiService } from '../../core/ui/ui.service';
|
|
9
|
-
import { CleanerService } from '../../shared/cleaner/cleaner.service';
|
|
10
|
-
import { DepsService } from '../../shared/deps/deps.service';
|
|
11
|
-
import { TokenizerService } from '../../shared/tokenizer/tokenizer.service';
|
|
12
|
-
|
|
13
|
-
type OutputFormat = 'xml' | 'text';
|
|
14
|
-
|
|
15
|
-
type PackOptions = {
|
|
16
|
-
copy?: boolean;
|
|
17
|
-
template?: string;
|
|
18
|
-
out?: string;
|
|
19
|
-
path?: string[];
|
|
20
|
-
exclude?: string[];
|
|
21
|
-
list?: boolean;
|
|
22
|
-
format?: OutputFormat;
|
|
23
|
-
clean?: boolean;
|
|
24
|
-
deps?: boolean;
|
|
25
|
-
depsDepth?: number;
|
|
26
|
-
explain?: boolean;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type TemplateContext = {
|
|
30
|
-
context: string;
|
|
31
|
-
fileList: string;
|
|
32
|
-
tokenCount: number;
|
|
33
|
-
usdEstimate: number;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
@Command({
|
|
37
|
-
name: 'pack',
|
|
38
|
-
description: 'Collect project context into a single file',
|
|
39
|
-
})
|
|
40
|
-
export class PackCommand extends CommandRunner {
|
|
41
|
-
constructor(
|
|
42
|
-
private readonly ui: UiService,
|
|
43
|
-
private readonly configService: ConfigService,
|
|
44
|
-
private readonly promptService: PromptService,
|
|
45
|
-
private readonly fsService: FsService,
|
|
46
|
-
private readonly tokenizer: TokenizerService,
|
|
47
|
-
private readonly cleaner: CleanerService,
|
|
48
|
-
private readonly depsService: DepsService,
|
|
49
|
-
) {
|
|
50
|
-
super();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
@Option({ flags: '-c, --copy', description: 'Copy result to clipboard' })
|
|
54
|
-
parseCopy(): boolean {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@Option({
|
|
59
|
-
flags: '-t, --template <name>',
|
|
60
|
-
description: 'Template name from .kodu/prompts',
|
|
61
|
-
})
|
|
62
|
-
parseTemplate(value: string): string {
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
@Option({
|
|
67
|
-
flags: '-o, --out <path>',
|
|
68
|
-
description: 'Path to save result',
|
|
69
|
-
})
|
|
70
|
-
parseOut(value: string): string {
|
|
71
|
-
return value;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
@Option({
|
|
75
|
-
flags: '-p, --path <path>',
|
|
76
|
-
description: 'Directory or glob to include (repeatable)',
|
|
77
|
-
})
|
|
78
|
-
parsePath(value: string, previous: string[] = []): string[] {
|
|
79
|
-
return [...previous, value];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
@Option({
|
|
83
|
-
flags: '-e, --exclude <pattern>',
|
|
84
|
-
description: 'Additional exclude pattern (repeatable)',
|
|
85
|
-
})
|
|
86
|
-
parseExclude(value: string, previous: string[] = []): string[] {
|
|
87
|
-
return [...previous, value];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
@Option({
|
|
91
|
-
flags: '-l, --list',
|
|
92
|
-
description: 'Print file list only, without content',
|
|
93
|
-
})
|
|
94
|
-
parseList(): boolean {
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@Option({
|
|
99
|
-
flags: '--clean',
|
|
100
|
-
description: 'Strip comments in-memory before packing (files not modified)',
|
|
101
|
-
})
|
|
102
|
-
parseClean(): boolean {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
@Option({
|
|
107
|
-
flags: '--deps',
|
|
108
|
-
description:
|
|
109
|
-
'Trace imports from entry point(s) and include their dependencies',
|
|
110
|
-
})
|
|
111
|
-
parseDeps(): boolean {
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
@Option({
|
|
116
|
-
flags: '--deps-depth <n>',
|
|
117
|
-
description: 'Max import depth when using --deps (default: unlimited)',
|
|
118
|
-
})
|
|
119
|
-
parseDepsDepth(value: string): number {
|
|
120
|
-
const n = Number.parseInt(value, 10);
|
|
121
|
-
if (Number.isNaN(n) || n < 1) {
|
|
122
|
-
this.ui.log.warn(`Invalid --deps-depth "${value}", ignoring`);
|
|
123
|
-
return Infinity;
|
|
124
|
-
}
|
|
125
|
-
return n;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
@Option({
|
|
129
|
-
flags: '--explain',
|
|
130
|
-
description: 'Print why each file was included (requires --deps)',
|
|
131
|
-
})
|
|
132
|
-
parseExplain(): boolean {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
@Option({
|
|
137
|
-
flags: '-f, --format <format>',
|
|
138
|
-
description: 'Output format: xml (default) or text',
|
|
139
|
-
})
|
|
140
|
-
parseFormat(value: string): OutputFormat {
|
|
141
|
-
if (value !== 'xml' && value !== 'text') {
|
|
142
|
-
this.ui.log.warn(`Unknown format "${value}", using "xml"`);
|
|
143
|
-
return 'xml';
|
|
144
|
-
}
|
|
145
|
-
return value;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async run(inputs: string[], options: PackOptions): Promise<void> {
|
|
149
|
-
const spinner = this.ui
|
|
150
|
-
.createSpinner({ text: 'Collecting files...' })
|
|
151
|
-
.start();
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const { packer } = this.configService.getConfig();
|
|
155
|
-
const extraExcludes = options.exclude ?? [];
|
|
156
|
-
|
|
157
|
-
let files: string[];
|
|
158
|
-
let explainMap: Map<string, string> | undefined;
|
|
159
|
-
|
|
160
|
-
if (options.deps) {
|
|
161
|
-
if (inputs.length === 0) {
|
|
162
|
-
spinner.error('--deps requires at least one entry file as argument');
|
|
163
|
-
this.ui.log.error('Usage: kodu pack <entry.ts> [more.ts...] --deps');
|
|
164
|
-
process.exitCode = 1;
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
spinner.text = 'Resolving dependency graph...';
|
|
169
|
-
const result = this.depsService.collectDependencies(
|
|
170
|
-
inputs,
|
|
171
|
-
process.cwd(),
|
|
172
|
-
{
|
|
173
|
-
maxDepth: options.depsDepth,
|
|
174
|
-
includeTypes: true,
|
|
175
|
-
includeDynamic: false,
|
|
176
|
-
},
|
|
177
|
-
);
|
|
178
|
-
files = result.files;
|
|
179
|
-
explainMap = result.explain;
|
|
180
|
-
} else {
|
|
181
|
-
files = await this.fsService.findProjectFiles({
|
|
182
|
-
excludeBinary: true,
|
|
183
|
-
useGitignore: packer.useGitignore,
|
|
184
|
-
ignore: [...packer.ignore, ...extraExcludes],
|
|
185
|
-
contentBasedBinaryDetection: packer.contentBasedBinaryDetection,
|
|
186
|
-
rootPaths: inputs.length > 0 ? inputs : options.path,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (files.length === 0) {
|
|
191
|
-
spinner.stop('No files to pack.');
|
|
192
|
-
this.ui.log.warn('No files to pack.');
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (options.list) {
|
|
197
|
-
spinner.success(`Found ${files.length} files`);
|
|
198
|
-
for (const file of files) {
|
|
199
|
-
if (options.explain && explainMap) {
|
|
200
|
-
const absFile = path.resolve(process.cwd(), file);
|
|
201
|
-
const reason =
|
|
202
|
-
explainMap.get(absFile) ?? explainMap.get(file) ?? '';
|
|
203
|
-
this.ui.log.info(`${file} ← ${reason}`);
|
|
204
|
-
} else {
|
|
205
|
-
this.ui.log.info(file);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (options.explain && explainMap) {
|
|
212
|
-
spinner.success(`Found ${files.length} files`);
|
|
213
|
-
this.ui.log.info('Dependency explanation:');
|
|
214
|
-
for (const file of files) {
|
|
215
|
-
const absFile = path.resolve(process.cwd(), file);
|
|
216
|
-
const reason = explainMap.get(absFile) ?? explainMap.get(file) ?? '';
|
|
217
|
-
this.ui.log.info(` ${file} ← ${reason}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const format: OutputFormat = options.format ?? 'xml';
|
|
222
|
-
const context = await this.buildContext(files, format, options.clean);
|
|
223
|
-
const fileList = files.join('\n');
|
|
224
|
-
const { tokens, usdEstimate } = this.tokenizer.count(context);
|
|
225
|
-
|
|
226
|
-
const basePrompt = await this.applyConfiguredPrompt({
|
|
227
|
-
context,
|
|
228
|
-
fileList,
|
|
229
|
-
tokenCount: tokens,
|
|
230
|
-
usdEstimate,
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const templateApplied = options.template
|
|
234
|
-
? await this.applyTemplate(options.template, {
|
|
235
|
-
context,
|
|
236
|
-
fileList,
|
|
237
|
-
tokenCount: tokens,
|
|
238
|
-
usdEstimate,
|
|
239
|
-
})
|
|
240
|
-
: basePrompt;
|
|
241
|
-
|
|
242
|
-
const outputPath = await this.writeOutput(templateApplied, options.out);
|
|
243
|
-
|
|
244
|
-
if (options.copy) {
|
|
245
|
-
await clipboard.write(templateApplied);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
spinner.success('Collection complete');
|
|
249
|
-
this.ui.log.info(`Files: ${files.length}`);
|
|
250
|
-
this.ui.log.info(`Tokens: ${tokens}`);
|
|
251
|
-
this.ui.log.info(`Cost estimate: ~$${usdEstimate.toFixed(4)}`);
|
|
252
|
-
this.ui.log.info(
|
|
253
|
-
`Format: ${format}${options.clean ? ' (comments stripped)' : ''}`,
|
|
254
|
-
);
|
|
255
|
-
this.ui.log.success(`Saved to ${outputPath}`);
|
|
256
|
-
|
|
257
|
-
if (options.copy) {
|
|
258
|
-
this.ui.log.success('Result copied to clipboard');
|
|
259
|
-
}
|
|
260
|
-
} catch (error) {
|
|
261
|
-
spinner.error('Error collecting context');
|
|
262
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
263
|
-
this.ui.log.error(message);
|
|
264
|
-
process.exitCode = 1;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private async buildContext(
|
|
269
|
-
files: string[],
|
|
270
|
-
format: OutputFormat,
|
|
271
|
-
clean = false,
|
|
272
|
-
): Promise<string> {
|
|
273
|
-
const chunks = await Promise.all(
|
|
274
|
-
files.map(async (file) => {
|
|
275
|
-
let content = await this.fsService.readFileRelative(file);
|
|
276
|
-
if (clean) {
|
|
277
|
-
content = this.cleaner.cleanContent(file, content);
|
|
278
|
-
}
|
|
279
|
-
if (format === 'xml') {
|
|
280
|
-
return `<file path="${file}">\n${content}\n</file>`;
|
|
281
|
-
}
|
|
282
|
-
return `// file: ${file}\n${content}`;
|
|
283
|
-
}),
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
if (format === 'xml') {
|
|
287
|
-
return `<files>\n${chunks.join('\n\n')}\n</files>`;
|
|
288
|
-
}
|
|
289
|
-
return chunks.join('\n\n');
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private async applyTemplate(
|
|
293
|
-
name: string,
|
|
294
|
-
ctx: TemplateContext,
|
|
295
|
-
): Promise<string> {
|
|
296
|
-
const template = await this.loadTemplate(name);
|
|
297
|
-
return this.fillTemplate(template, ctx);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private async loadTemplate(name: string): Promise<string> {
|
|
301
|
-
return this.promptService.loadFromPromptsDir(name);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
private async writeOutput(
|
|
305
|
-
content: string,
|
|
306
|
-
outPath?: string,
|
|
307
|
-
): Promise<string> {
|
|
308
|
-
const target = outPath ?? path.join(process.cwd(), '.kodu', 'context.txt');
|
|
309
|
-
const dir = path.dirname(target);
|
|
310
|
-
await fs.mkdir(dir, { recursive: true });
|
|
311
|
-
await fs.writeFile(target, `${content}\n`, 'utf8');
|
|
312
|
-
return target;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private async applyConfiguredPrompt(ctx: TemplateContext): Promise<string> {
|
|
316
|
-
const config = this.configService.getConfig();
|
|
317
|
-
const packPrompt = config.prompts?.pack;
|
|
318
|
-
|
|
319
|
-
if (!packPrompt) {
|
|
320
|
-
return ctx.context;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
const template = await this.promptService.load(packPrompt);
|
|
325
|
-
return this.fillTemplate(template, ctx);
|
|
326
|
-
} catch {
|
|
327
|
-
this.ui.log.warn(
|
|
328
|
-
`Prompt file not found: ${packPrompt}, using raw context`,
|
|
329
|
-
);
|
|
330
|
-
return ctx.context;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private fillTemplate(template: string, ctx: TemplateContext): string {
|
|
335
|
-
const filled = template
|
|
336
|
-
.replace(/\{\{context\}\}/g, ctx.context)
|
|
337
|
-
.replace(/\{\{fileList\}\}/g, ctx.fileList)
|
|
338
|
-
.replace(/\{\{tokenCount\}\}/g, ctx.tokenCount.toString())
|
|
339
|
-
.replace(/\{\{usdEstimate\}\}/g, ctx.usdEstimate.toFixed(4));
|
|
340
|
-
|
|
341
|
-
if (!template.includes('{{context}}')) {
|
|
342
|
-
return `${filled}\n\n${ctx.context}`;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return filled;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common';
|
|
2
|
-
import { ConfigModule } from '../../core/config/config.module';
|
|
3
|
-
import { FsModule } from '../../core/file-system/fs.module';
|
|
4
|
-
import { UiModule } from '../../core/ui/ui.module';
|
|
5
|
-
import { CleanerService } from '../../shared/cleaner/cleaner.service';
|
|
6
|
-
import { DepsModule } from '../../shared/deps/deps.module';
|
|
7
|
-
import { TokenizerModule } from '../../shared/tokenizer/tokenizer.module';
|
|
8
|
-
import { PackCommand } from './pack.command';
|
|
9
|
-
|
|
10
|
-
@Module({
|
|
11
|
-
imports: [ConfigModule, UiModule, FsModule, TokenizerModule, DepsModule],
|
|
12
|
-
providers: [PackCommand, CleanerService],
|
|
13
|
-
})
|
|
14
|
-
export class PackModule {}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { Global, Module } from '@nestjs/common';
|
|
2
|
-
import { ConfigService } from './config.service';
|
|
3
|
-
import { PromptService } from './prompt.service';
|
|
4
|
-
|
|
5
|
-
@Global()
|
|
6
|
-
@Module({
|
|
7
|
-
providers: [ConfigService, PromptService],
|
|
8
|
-
exports: [ConfigService, PromptService],
|
|
9
|
-
})
|
|
10
|
-
export class ConfigModule {}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
const cleanerSchema = z.object({
|
|
4
|
-
whitelist: z.array(z.string()).default(['//!']),
|
|
5
|
-
keepJSDoc: z.boolean().default(true),
|
|
6
|
-
useGitignore: z.boolean().default(true),
|
|
7
|
-
ignore: z.array(z.string()).default([]),
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const packerSchema = z.object({
|
|
11
|
-
ignore: z
|
|
12
|
-
.array(z.string())
|
|
13
|
-
.default([
|
|
14
|
-
'package-lock.json',
|
|
15
|
-
'yarn.lock',
|
|
16
|
-
'pnpm-lock.yaml',
|
|
17
|
-
'.git',
|
|
18
|
-
'.kodu',
|
|
19
|
-
'node_modules',
|
|
20
|
-
'dist',
|
|
21
|
-
'coverage',
|
|
22
|
-
]),
|
|
23
|
-
useGitignore: z.boolean().default(true),
|
|
24
|
-
contentBasedBinaryDetection: z.boolean().default(false),
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const promptsSchema = z
|
|
28
|
-
.object({
|
|
29
|
-
pack: z.string().optional(),
|
|
30
|
-
})
|
|
31
|
-
.optional();
|
|
32
|
-
|
|
33
|
-
export const configSchema = z.object({
|
|
34
|
-
$schema: z.string().optional(),
|
|
35
|
-
cleaner: cleanerSchema.default({
|
|
36
|
-
whitelist: ['//!'],
|
|
37
|
-
keepJSDoc: true,
|
|
38
|
-
useGitignore: true,
|
|
39
|
-
ignore: [],
|
|
40
|
-
}),
|
|
41
|
-
packer: packerSchema.default({
|
|
42
|
-
ignore: [
|
|
43
|
-
'package-lock.json',
|
|
44
|
-
'yarn.lock',
|
|
45
|
-
'pnpm-lock.yaml',
|
|
46
|
-
'.git',
|
|
47
|
-
'.kodu',
|
|
48
|
-
'node_modules',
|
|
49
|
-
'dist',
|
|
50
|
-
'coverage',
|
|
51
|
-
],
|
|
52
|
-
useGitignore: true,
|
|
53
|
-
contentBasedBinaryDetection: false,
|
|
54
|
-
}),
|
|
55
|
-
prompts: promptsSchema,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
export type KoduConfig = z.infer<typeof configSchema>;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
2
|
-
import { lilconfigSync } from 'lilconfig';
|
|
3
|
-
import pc from 'picocolors';
|
|
4
|
-
import { configSchema, type KoduConfig } from './config.schema';
|
|
5
|
-
|
|
6
|
-
@Injectable()
|
|
7
|
-
export class ConfigService {
|
|
8
|
-
private config?: KoduConfig;
|
|
9
|
-
|
|
10
|
-
getConfig(): KoduConfig {
|
|
11
|
-
if (!this.config) {
|
|
12
|
-
this.config = this.loadConfig();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return this.config;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
private loadConfig(): KoduConfig {
|
|
19
|
-
const explorer = lilconfigSync('kodu', { searchPlaces: ['kodu.json'] });
|
|
20
|
-
const result = explorer.search(process.cwd());
|
|
21
|
-
|
|
22
|
-
const rawConfig =
|
|
23
|
-
result && !result.isEmpty && result.config ? result.config : {};
|
|
24
|
-
|
|
25
|
-
const parsed = configSchema.safeParse(rawConfig);
|
|
26
|
-
|
|
27
|
-
if (!parsed.success) {
|
|
28
|
-
console.error(pc.red('kodu.json is invalid:'));
|
|
29
|
-
parsed.error.issues.forEach((issue) => {
|
|
30
|
-
const path = issue.path.join('.') || '(root)';
|
|
31
|
-
console.error(pc.red(`- ${path}: ${issue.message}`));
|
|
32
|
-
});
|
|
33
|
-
this.terminate('Fix the config and run the command again.');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return parsed.data;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private terminate(message: string): never {
|
|
40
|
-
console.error(pc.red(message));
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { Injectable } from '@nestjs/common';
|
|
4
|
-
|
|
5
|
-
@Injectable()
|
|
6
|
-
export class PromptService {
|
|
7
|
-
private readonly cache = new Map<string, string>();
|
|
8
|
-
|
|
9
|
-
async load(source: string): Promise<string> {
|
|
10
|
-
return this.readSource(source);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async loadFromPromptsDir(name: string): Promise<string> {
|
|
14
|
-
const candidates = this.buildCandidates(name);
|
|
15
|
-
|
|
16
|
-
for (const candidate of candidates) {
|
|
17
|
-
if (await this.exists(candidate)) {
|
|
18
|
-
return this.readAndCache(candidate);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
throw new Error(
|
|
23
|
-
`Template ${name} not found. Expected files: ${candidates
|
|
24
|
-
.map((c) => path.relative(process.cwd(), c))
|
|
25
|
-
.join(', ')}`,
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private async readSource(source: string): Promise<string> {
|
|
30
|
-
const resolved = path.isAbsolute(source)
|
|
31
|
-
? source
|
|
32
|
-
: path.resolve(process.cwd(), source);
|
|
33
|
-
|
|
34
|
-
if (await this.exists(resolved)) {
|
|
35
|
-
return this.readAndCache(resolved);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (this.looksLikeInline(source)) {
|
|
39
|
-
return source;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
throw new Error(
|
|
43
|
-
`Prompt file not found: ${path.relative(process.cwd(), resolved)}`,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private async readAndCache(target: string): Promise<string> {
|
|
48
|
-
const cached = this.cache.get(target);
|
|
49
|
-
if (cached) {
|
|
50
|
-
return cached;
|
|
51
|
-
}
|
|
52
|
-
const content = await fs.readFile(target, 'utf8');
|
|
53
|
-
this.cache.set(target, content);
|
|
54
|
-
return content;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private looksLikeInline(value: string): boolean {
|
|
58
|
-
if (value.includes('\n')) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
const hasPathSegments = value.includes('/') || value.includes('\\');
|
|
62
|
-
const hasExtension = path.extname(value) !== '';
|
|
63
|
-
return value.trim().length > 0 && !hasPathSegments && !hasExtension;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private buildCandidates(name: string): string[] {
|
|
67
|
-
const names = path.extname(name) ? [name] : [`${name}.md`, `${name}.txt`];
|
|
68
|
-
const root = path.join(process.cwd(), '.kodu', 'prompts');
|
|
69
|
-
return names.map((variant) => path.join(root, variant));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private async exists(target: string): Promise<boolean> {
|
|
73
|
-
try {
|
|
74
|
-
await fs.access(target);
|
|
75
|
-
return true;
|
|
76
|
-
} catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|