kodu 1.2.0 → 2.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.
Files changed (138) hide show
  1. package/AGENTS.md +36 -68
  2. package/README.md +128 -96
  3. package/dist/package.json +1 -2
  4. package/dist/src/app.module.js +0 -8
  5. package/dist/src/app.module.js.map +1 -1
  6. package/dist/src/commands/clean/clean.command.d.ts +14 -1
  7. package/dist/src/commands/clean/clean.command.js +142 -32
  8. package/dist/src/commands/clean/clean.command.js.map +1 -1
  9. package/dist/src/commands/init/init.command.d.ts +2 -9
  10. package/dist/src/commands/init/init.command.js +15 -241
  11. package/dist/src/commands/init/init.command.js.map +1 -1
  12. package/dist/src/commands/pack/pack.command.d.ts +12 -1
  13. package/dist/src/commands/pack/pack.command.js +81 -6
  14. package/dist/src/commands/pack/pack.command.js.map +1 -1
  15. package/dist/src/commands/pack/pack.module.js +2 -1
  16. package/dist/src/commands/pack/pack.module.js.map +1 -1
  17. package/dist/src/core/config/config.schema.d.ts +0 -46
  18. package/dist/src/core/config/config.schema.js +1 -51
  19. package/dist/src/core/config/config.schema.js.map +1 -1
  20. package/dist/src/core/config/config.service.js +2 -2
  21. package/dist/src/core/config/config.service.js.map +1 -1
  22. package/dist/src/core/config/prompt.service.d.ts +1 -4
  23. package/dist/src/core/config/prompt.service.js +4 -17
  24. package/dist/src/core/config/prompt.service.js.map +1 -1
  25. package/dist/src/shared/cleaner/cleaner.service.d.ts +2 -0
  26. package/dist/src/shared/cleaner/cleaner.service.js +32 -7
  27. package/dist/src/shared/cleaner/cleaner.service.js.map +1 -1
  28. package/dist/src/shared/cleaner/cleaner.types.d.ts +7 -0
  29. package/dist/src/shared/constants.d.ts +0 -4
  30. package/dist/src/shared/constants.js +1 -5
  31. package/dist/src/shared/constants.js.map +1 -1
  32. package/dist/src/shared/git/git.module.js +0 -2
  33. package/dist/src/shared/git/git.module.js.map +1 -1
  34. package/dist/src/shared/git/git.service.d.ts +1 -8
  35. package/dist/src/shared/git/git.service.js +10 -33
  36. package/dist/src/shared/git/git.service.js.map +1 -1
  37. package/dist/src/shared/tokenizer/tokenizer.module.js +0 -2
  38. package/dist/src/shared/tokenizer/tokenizer.module.js.map +1 -1
  39. package/dist/src/shared/tokenizer/tokenizer.service.d.ts +0 -6
  40. package/dist/src/shared/tokenizer/tokenizer.service.js +8 -38
  41. package/dist/src/shared/tokenizer/tokenizer.service.js.map +1 -1
  42. package/dist/tsconfig.build.tsbuildinfo +1 -1
  43. package/kodu.schema.json +0 -139
  44. package/package.json +1 -2
  45. package/src/app.module.ts +0 -8
  46. package/src/commands/clean/clean.command.ts +143 -33
  47. package/src/commands/init/init.command.ts +15 -310
  48. package/src/commands/pack/pack.command.ts +74 -4
  49. package/src/commands/pack/pack.module.ts +2 -1
  50. package/src/core/config/config.schema.ts +1 -68
  51. package/src/core/config/config.service.ts +2 -2
  52. package/src/core/config/prompt.service.ts +4 -26
  53. package/src/shared/cleaner/cleaner.service.ts +43 -12
  54. package/src/shared/cleaner/cleaner.types.ts +7 -0
  55. package/src/shared/constants.ts +0 -4
  56. package/src/shared/git/git.module.ts +0 -2
  57. package/src/shared/git/git.service.ts +9 -31
  58. package/src/shared/tokenizer/tokenizer.module.ts +0 -2
  59. package/src/shared/tokenizer/tokenizer.service.ts +9 -39
  60. package/.kodu/prompts/.keep +0 -0
  61. package/.kodu/prompts/commit.md +0 -9
  62. package/.kodu/prompts/pack.md +0 -7
  63. package/.kodu/prompts/review-bug.md +0 -6
  64. package/.kodu/prompts/review-security.md +0 -6
  65. package/.kodu/prompts/review-style.md +0 -6
  66. package/.opencode/command/openspec-apply.md +0 -24
  67. package/.opencode/command/openspec-archive.md +0 -27
  68. package/.opencode/command/openspec-proposal.md +0 -29
  69. package/.opencode/skills/kodu-ops/SKILL.md +0 -184
  70. package/dist/src/commands/commit/commit.command.d.ts +0 -18
  71. package/dist/src/commands/commit/commit.command.js +0 -149
  72. package/dist/src/commands/commit/commit.command.js.map +0 -1
  73. package/dist/src/commands/commit/commit.module.d.ts +0 -2
  74. package/dist/src/commands/commit/commit.module.js +0 -25
  75. package/dist/src/commands/commit/commit.module.js.map +0 -1
  76. package/dist/src/commands/ops/ops.command.d.ts +0 -4
  77. package/dist/src/commands/ops/ops.command.js +0 -39
  78. package/dist/src/commands/ops/ops.command.js.map +0 -1
  79. package/dist/src/commands/ops/ops.module.d.ts +0 -2
  80. package/dist/src/commands/ops/ops.module.js +0 -33
  81. package/dist/src/commands/ops/ops.module.js.map +0 -1
  82. package/dist/src/commands/ops/ops.types.d.ts +0 -13
  83. package/dist/src/commands/ops/ops.types.js +0 -12
  84. package/dist/src/commands/ops/ops.types.js.map +0 -1
  85. package/dist/src/commands/ops/ops.utils.d.ts +0 -13
  86. package/dist/src/commands/ops/ops.utils.js +0 -121
  87. package/dist/src/commands/ops/ops.utils.js.map +0 -1
  88. package/dist/src/commands/ops/subcommands/ops-env.command.d.ts +0 -24
  89. package/dist/src/commands/ops/subcommands/ops-env.command.js +0 -156
  90. package/dist/src/commands/ops/subcommands/ops-env.command.js.map +0 -1
  91. package/dist/src/commands/ops/subcommands/ops-routes.command.d.ts +0 -22
  92. package/dist/src/commands/ops/subcommands/ops-routes.command.js +0 -203
  93. package/dist/src/commands/ops/subcommands/ops-routes.command.js.map +0 -1
  94. package/dist/src/commands/ops/subcommands/ops-service.command.d.ts +0 -22
  95. package/dist/src/commands/ops/subcommands/ops-service.command.js +0 -169
  96. package/dist/src/commands/ops/subcommands/ops-service.command.js.map +0 -1
  97. package/dist/src/commands/ops/subcommands/ops-sysinfo.command.d.ts +0 -14
  98. package/dist/src/commands/ops/subcommands/ops-sysinfo.command.js +0 -75
  99. package/dist/src/commands/ops/subcommands/ops-sysinfo.command.js.map +0 -1
  100. package/dist/src/commands/review/review.command.d.ts +0 -26
  101. package/dist/src/commands/review/review.command.js +0 -205
  102. package/dist/src/commands/review/review.command.js.map +0 -1
  103. package/dist/src/commands/review/review.module.d.ts +0 -2
  104. package/dist/src/commands/review/review.module.js +0 -26
  105. package/dist/src/commands/review/review.module.js.map +0 -1
  106. package/dist/src/core/config/default-prompts.d.ts +0 -9
  107. package/dist/src/core/config/default-prompts.js +0 -49
  108. package/dist/src/core/config/default-prompts.js.map +0 -1
  109. package/dist/src/shared/ai/ai.module.d.ts +0 -2
  110. package/dist/src/shared/ai/ai.module.js +0 -23
  111. package/dist/src/shared/ai/ai.module.js.map +0 -1
  112. package/dist/src/shared/ai/ai.service.d.ts +0 -22
  113. package/dist/src/shared/ai/ai.service.js +0 -164
  114. package/dist/src/shared/ai/ai.service.js.map +0 -1
  115. package/dist/src/shared/ssh/ssh.module.d.ts +0 -2
  116. package/dist/src/shared/ssh/ssh.module.js +0 -21
  117. package/dist/src/shared/ssh/ssh.module.js.map +0 -1
  118. package/dist/src/shared/ssh/ssh.service.d.ts +0 -11
  119. package/dist/src/shared/ssh/ssh.service.js +0 -53
  120. package/dist/src/shared/ssh/ssh.service.js.map +0 -1
  121. package/dist/tsconfig.tsbuildinfo +0 -1
  122. package/src/commands/commit/commit.command.ts +0 -139
  123. package/src/commands/commit/commit.module.ts +0 -12
  124. package/src/commands/ops/ops.command.ts +0 -30
  125. package/src/commands/ops/ops.module.ts +0 -20
  126. package/src/commands/ops/ops.types.ts +0 -24
  127. package/src/commands/ops/ops.utils.ts +0 -160
  128. package/src/commands/ops/subcommands/ops-env.command.ts +0 -165
  129. package/src/commands/ops/subcommands/ops-routes.command.ts +0 -221
  130. package/src/commands/ops/subcommands/ops-service.command.ts +0 -190
  131. package/src/commands/ops/subcommands/ops-sysinfo.command.ts +0 -77
  132. package/src/commands/review/review.command.ts +0 -199
  133. package/src/commands/review/review.module.ts +0 -13
  134. package/src/core/config/default-prompts.ts +0 -53
  135. package/src/shared/ai/ai.module.ts +0 -10
  136. package/src/shared/ai/ai.service.ts +0 -216
  137. package/src/shared/ssh/ssh.module.ts +0 -8
  138. package/src/shared/ssh/ssh.service.ts +0 -61
@@ -1,342 +1,47 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Command, CommandRunner } from 'nest-commander';
4
- import { type KoduConfig } from '../../core/config/config.schema';
5
- import {
6
- DEFAULT_COMMIT_PROMPT,
7
- DEFAULT_PACK_PROMPT,
8
- DEFAULT_REVIEW_PROMPTS,
9
- } from '../../core/config/default-prompts';
10
4
  import { UiService } from '../../core/ui/ui.service';
11
- import {
12
- DEFAULT_COMMIT_TOKENS,
13
- DEFAULT_LLM_MODEL,
14
- DEFAULT_REVIEW_TOKENS,
15
- } from '../../shared/constants';
16
5
 
17
- const buildDefaultCommandSettings = () => ({
18
- commit: { modelSettings: { maxOutputTokens: DEFAULT_COMMIT_TOKENS } },
19
- review: { modelSettings: { maxOutputTokens: DEFAULT_REVIEW_TOKENS } },
20
- });
6
+ const GITIGNORE_ENTRY = '.kodu/context.txt';
21
7
 
22
- @Command({ name: 'init', description: 'Initialize Kodu configuration' })
8
+ @Command({ name: 'init', description: 'Add kodu output to .gitignore' })
23
9
  export class InitCommand extends CommandRunner {
24
10
  constructor(private readonly ui: UiService) {
25
11
  super();
26
12
  }
27
13
 
28
14
  async run(): Promise<void> {
29
- const configPath = path.join(process.cwd(), 'kodu.json');
30
-
31
- const defaultLlmConfig = {
32
- model: `openai/${DEFAULT_LLM_MODEL}`,
33
- apiKeyEnv: 'OPENAI_API_KEY',
34
- };
35
-
36
- const defaultConfig: KoduConfig = {
37
- $schema:
38
- 'https://raw.githubusercontent.com/uxname/kodu/refs/heads/master/kodu.schema.json',
39
- llm: defaultLlmConfig,
40
- ops: {
41
- servers: {
42
- dev: {
43
- host: 'example.com',
44
- port: 22,
45
- user: 'ubuntu',
46
- sshKeyPath: '~/.ssh/id_rsa',
47
- description: 'Example AgentOps server',
48
- paths: {
49
- apps: '/var/agent-apps',
50
- caddy: '/var/agent-apps/caddy',
51
- },
52
- },
53
- },
54
- },
55
- cleaner: {
56
- whitelist: ['//!'],
57
- keepJSDoc: true,
58
- useGitignore: true,
59
- ignore: [],
60
- },
61
- packer: {
62
- ignore: [
63
- 'package-lock.json',
64
- 'yarn.lock',
65
- 'pnpm-lock.yaml',
66
- '.git',
67
- '.kodu',
68
- 'node_modules',
69
- 'dist',
70
- 'coverage',
71
- ],
72
- useGitignore: true,
73
- contentBasedBinaryDetection: false,
74
- },
75
- };
76
-
77
- const useAi = await this.ui.promptConfirm({
78
- message: 'Will you use AI functions?',
79
- default: true,
80
- });
81
-
82
- let llmConfig: KoduConfig['llm'] | undefined;
83
- if (useAi) {
84
- const useCustomModel = await this.ui.promptConfirm({
85
- message: 'Use your own model?',
86
- default: false,
87
- });
88
-
89
- let model: string;
90
- if (useCustomModel) {
91
- model = await this.ui.promptInput({
92
- message:
93
- 'Enter model in format provider/model-name (e.g., openai/gpt-4o):',
94
- default: defaultLlmConfig.model,
95
- validate: (input) => {
96
- if (!input.includes('/')) {
97
- return 'Model must be in format provider/model-name';
98
- }
99
- return true;
100
- },
101
- });
102
- } else {
103
- model = await this.ui.promptSelect<string>(
104
- this.buildModelQuestion(defaultLlmConfig.model),
105
- );
106
- }
107
-
108
- llmConfig = {
109
- model,
110
- apiKeyEnv: defaultLlmConfig.apiKeyEnv,
111
- commands: buildDefaultCommandSettings(),
112
- };
113
- }
114
-
115
- const extendIgnore = await this.ui.promptConfirm({
116
- message: 'Modify standard ignore list?',
117
- default: false,
118
- });
119
-
120
- const ignoreList = extendIgnore
121
- ? await this.askIgnoreList(defaultConfig.packer.ignore)
122
- : defaultConfig.packer.ignore;
123
-
124
- const additionalWhitelist = await this.ui.promptInput({
125
- message:
126
- 'Additional whitelist prefixes (comma-separated, empty - keep default):',
127
- default: '',
128
- });
129
-
130
- const whitelist = this.mergeWhitelist(
131
- defaultConfig.cleaner.whitelist,
132
- additionalWhitelist,
133
- );
134
-
135
- const promptPaths = this.buildPromptPaths();
136
-
137
- const configToSave: KoduConfig = {
138
- $schema: defaultConfig.$schema,
139
- ...(llmConfig && { llm: llmConfig }),
140
- cleaner: {
141
- whitelist,
142
- keepJSDoc: defaultConfig.cleaner.keepJSDoc,
143
- useGitignore: defaultConfig.cleaner.useGitignore,
144
- ignore: defaultConfig.cleaner.ignore,
145
- },
146
- packer: {
147
- ignore: ignoreList,
148
- useGitignore: defaultConfig.packer.useGitignore,
149
- contentBasedBinaryDetection:
150
- defaultConfig.packer.contentBasedBinaryDetection,
151
- },
152
- ops: defaultConfig.ops,
153
- prompts: {
154
- review: {
155
- bug: promptPaths.review.bug,
156
- style: promptPaths.review.style,
157
- security: promptPaths.review.security,
158
- },
159
- commit: promptPaths.commit,
160
- pack: promptPaths.pack,
161
- },
162
- };
163
-
164
- await this.writeConfig(configPath, configToSave);
165
- await this.ensurePromptFiles(promptPaths);
166
- await this.ensureGitignore();
167
-
168
- this.ui.log.success('Kodu configuration created.');
169
- if (useAi) {
170
- this.ui.log.info('🎉 Kodu initialized! Run `kodu pack` to continue.');
171
- } else {
172
- this.ui.log.info('🎉 Kodu initialized! Available commands: pack, clean.');
173
- this.ui.log.info(
174
- 'To use AI functions (review, commit) add llm section to kodu.json.',
175
- );
176
- }
177
- }
178
-
179
- private buildModelQuestion(defaultModel: string) {
180
- return {
181
- message: 'Select AI model',
182
- choices: [
183
- {
184
- name: 'OpenAI GPT-4o (recommended)',
185
- value: `openai/${DEFAULT_LLM_MODEL}`,
186
- },
187
- { name: 'OpenAI GPT-4o Mini', value: 'openai/gpt-4o-mini' },
188
- { name: 'OpenAI GPT-4o', value: 'openai/gpt-4o' },
189
- {
190
- name: 'Anthropic Claude 3.5 Sonnet',
191
- value: 'anthropic/claude-3-5-sonnet-20241022',
192
- },
193
- { name: 'Google Gemini 2.5 Flash', value: 'google/gemini-2.5-flash' },
194
- ],
195
- default: defaultModel,
196
- };
197
- }
198
-
199
- private async askIgnoreList(defaultIgnore: string[]): Promise<string[]> {
200
- const answer = await this.ui.promptInput({
201
- message: 'Specify ignore patterns (comma-separated)',
202
- default: defaultIgnore.join(', '),
203
- });
204
-
205
- return answer
206
- .split(',')
207
- .map((item) => item.trim())
208
- .filter((item) => item.length > 0);
209
- }
210
-
211
- private mergeWhitelist(defaultWhitelist: string[], extra: string): string[] {
212
- if (!extra.trim()) {
213
- return defaultWhitelist;
214
- }
215
-
216
- const additions = extra
217
- .split(',')
218
- .map((item) => item.trim())
219
- .filter((item) => item.length > 0);
220
-
221
- return Array.from(new Set([...defaultWhitelist, ...additions]));
222
- }
223
-
224
- private async writeConfig(
225
- configPath: string,
226
- config: KoduConfig,
227
- ): Promise<void> {
228
- if (await this.fileExists(configPath)) {
229
- const overwrite = await this.ui.promptConfirm({
230
- message: 'kodu.json already exists. Overwrite?',
231
- default: false,
232
- });
233
-
234
- if (!overwrite) {
235
- this.ui.log.warn(
236
- 'Initialization cancelled: kodu.json file already exists.',
237
- );
238
- return;
239
- }
240
- }
241
-
242
- await fs.writeFile(
243
- configPath,
244
- `${JSON.stringify(config, null, 2)}\n`,
245
- 'utf8',
246
- );
247
- this.ui.log.success(`Saved ${configPath}`);
15
+ await this.updateGitignore();
16
+ this.ui.log.success('Done.');
248
17
  }
249
18
 
250
- private async ensurePromptFiles(
251
- paths: ReturnType<InitCommand['buildPromptPaths']>,
252
- ): Promise<void> {
253
- const promptDir = path.join(process.cwd(), '.kodu', 'prompts');
254
- await fs.mkdir(promptDir, { recursive: true });
255
-
256
- const keepFile = path.join(promptDir, '.keep');
257
- if (!(await this.fileExists(keepFile))) {
258
- await fs.writeFile(keepFile, '');
259
- }
260
-
261
- await Promise.all([
262
- this.writePromptIfMissing(paths.review.bug, DEFAULT_REVIEW_PROMPTS.bug),
263
- this.writePromptIfMissing(
264
- paths.review.style,
265
- DEFAULT_REVIEW_PROMPTS.style,
266
- ),
267
- this.writePromptIfMissing(
268
- paths.review.security,
269
- DEFAULT_REVIEW_PROMPTS.security,
270
- ),
271
- this.writePromptIfMissing(paths.commit, DEFAULT_COMMIT_PROMPT),
272
- this.writePromptIfMissing(paths.pack, DEFAULT_PACK_PROMPT),
273
- ]);
274
- }
275
-
276
- private buildPromptPaths() {
277
- return {
278
- review: {
279
- bug: path.posix.join('.kodu', 'prompts', 'review-bug.md'),
280
- style: path.posix.join('.kodu', 'prompts', 'review-style.md'),
281
- security: path.posix.join('.kodu', 'prompts', 'review-security.md'),
282
- },
283
- commit: path.posix.join('.kodu', 'prompts', 'commit.md'),
284
- pack: path.posix.join('.kodu', 'prompts', 'pack.md'),
285
- } as const;
286
- }
287
-
288
- private async writePromptIfMissing(
289
- target: string,
290
- content: string,
291
- ): Promise<void> {
292
- const absolute = path.isAbsolute(target)
293
- ? target
294
- : path.join(process.cwd(), target);
19
+ private async updateGitignore(): Promise<void> {
20
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
295
21
 
296
- if (await this.fileExists(absolute)) {
22
+ if (!(await this.exists(gitignorePath))) {
23
+ this.ui.log.warn('.gitignore not found, skipping.');
297
24
  return;
298
25
  }
299
26
 
300
- await fs.mkdir(path.dirname(absolute), { recursive: true });
301
- await fs.writeFile(absolute, `${content}\n`, 'utf8');
302
- }
303
-
304
- private async ensureGitignore(): Promise<void> {
305
- const gitignorePath = path.join(process.cwd(), '.gitignore');
306
- const content = (await this.fileExists(gitignorePath))
307
- ? await fs.readFile(gitignorePath, 'utf8')
308
- : '';
309
-
27
+ const content = await fs.readFile(gitignorePath, 'utf8');
310
28
  const lines = content.split(/\r?\n/);
311
- const additions: string[] = [];
312
-
313
- if (!lines.some((line) => line.trim() === '.env')) {
314
- const addEnv = await this.ui.promptConfirm({
315
- message: '.env not in .gitignore. Add it?',
316
- default: true,
317
- });
318
-
319
- if (addEnv) {
320
- additions.push('.env');
321
- }
322
- }
323
29
 
324
- if (additions.length === 0) {
30
+ if (lines.some((line) => line.trim() === GITIGNORE_ENTRY)) {
31
+ this.ui.log.info(`${GITIGNORE_ENTRY} already in .gitignore`);
325
32
  return;
326
33
  }
327
34
 
328
35
  const trimmed = content.trimEnd();
329
36
  const next =
330
- trimmed.length > 0
331
- ? `${trimmed}\n${additions.join('\n')}`
332
- : additions.join('\n');
37
+ trimmed.length > 0 ? `${trimmed}\n${GITIGNORE_ENTRY}` : GITIGNORE_ENTRY;
333
38
  await fs.writeFile(gitignorePath, `${next}\n`, 'utf8');
334
- this.ui.log.success('Updated .gitignore');
39
+ this.ui.log.success(`Added ${GITIGNORE_ENTRY} to .gitignore`);
335
40
  }
336
41
 
337
- private async fileExists(targetPath: string): Promise<boolean> {
42
+ private async exists(target: string): Promise<boolean> {
338
43
  try {
339
- await fs.access(targetPath);
44
+ await fs.access(target);
340
45
  return true;
341
46
  } catch {
342
47
  return false;
@@ -6,13 +6,20 @@ import { ConfigService } from '../../core/config/config.service';
6
6
  import { PromptService } from '../../core/config/prompt.service';
7
7
  import { FsService } from '../../core/file-system/fs.service';
8
8
  import { UiService } from '../../core/ui/ui.service';
9
+ import { CleanerService } from '../../shared/cleaner/cleaner.service';
9
10
  import { TokenizerService } from '../../shared/tokenizer/tokenizer.service';
10
11
 
12
+ type OutputFormat = 'xml' | 'text';
13
+
11
14
  type PackOptions = {
12
15
  copy?: boolean;
13
16
  template?: string;
14
17
  out?: string;
15
18
  path?: string[];
19
+ exclude?: string[];
20
+ list?: boolean;
21
+ format?: OutputFormat;
22
+ clean?: boolean;
16
23
  };
17
24
 
18
25
  type TemplateContext = {
@@ -33,6 +40,7 @@ export class PackCommand extends CommandRunner {
33
40
  private readonly promptService: PromptService,
34
41
  private readonly fsService: FsService,
35
42
  private readonly tokenizer: TokenizerService,
43
+ private readonly cleaner: CleanerService,
36
44
  ) {
37
45
  super();
38
46
  }
@@ -66,6 +74,42 @@ export class PackCommand extends CommandRunner {
66
74
  return [...previous, value];
67
75
  }
68
76
 
77
+ @Option({
78
+ flags: '-e, --exclude <pattern>',
79
+ description: 'Additional exclude pattern (repeatable)',
80
+ })
81
+ parseExclude(value: string, previous: string[] = []): string[] {
82
+ return [...previous, value];
83
+ }
84
+
85
+ @Option({
86
+ flags: '-l, --list',
87
+ description: 'Print file list only, without content',
88
+ })
89
+ parseList(): boolean {
90
+ return true;
91
+ }
92
+
93
+ @Option({
94
+ flags: '--clean',
95
+ description: 'Strip comments in-memory before packing (files not modified)',
96
+ })
97
+ parseClean(): boolean {
98
+ return true;
99
+ }
100
+
101
+ @Option({
102
+ flags: '-f, --format <format>',
103
+ description: 'Output format: xml (default) or text',
104
+ })
105
+ parseFormat(value: string): OutputFormat {
106
+ if (value !== 'xml' && value !== 'text') {
107
+ this.ui.log.warn(`Unknown format "${value}", using "xml"`);
108
+ return 'xml';
109
+ }
110
+ return value;
111
+ }
112
+
69
113
  async run(_inputs: string[], options: PackOptions): Promise<void> {
70
114
  const spinner = this.ui
71
115
  .createSpinner({ text: 'Collecting files...' })
@@ -73,10 +117,11 @@ export class PackCommand extends CommandRunner {
73
117
 
74
118
  try {
75
119
  const { packer } = this.configService.getConfig();
120
+ const extraExcludes = options.exclude ?? [];
76
121
  const files = await this.fsService.findProjectFiles({
77
122
  excludeBinary: true,
78
123
  useGitignore: packer.useGitignore,
79
- ignore: packer.ignore,
124
+ ignore: [...packer.ignore, ...extraExcludes],
80
125
  contentBasedBinaryDetection: packer.contentBasedBinaryDetection,
81
126
  rootPaths: options.path,
82
127
  });
@@ -87,7 +132,16 @@ export class PackCommand extends CommandRunner {
87
132
  return;
88
133
  }
89
134
 
90
- const context = await this.buildContext(files);
135
+ if (options.list) {
136
+ spinner.success(`Found ${files.length} files`);
137
+ for (const file of files) {
138
+ this.ui.log.info(file);
139
+ }
140
+ return;
141
+ }
142
+
143
+ const format: OutputFormat = options.format ?? 'xml';
144
+ const context = await this.buildContext(files, format, options.clean);
91
145
  const fileList = files.join('\n');
92
146
  const { tokens, usdEstimate } = this.tokenizer.count(context);
93
147
 
@@ -117,6 +171,9 @@ export class PackCommand extends CommandRunner {
117
171
  this.ui.log.info(`Files: ${files.length}`);
118
172
  this.ui.log.info(`Tokens: ${tokens}`);
119
173
  this.ui.log.info(`Cost estimate: ~$${usdEstimate.toFixed(4)}`);
174
+ this.ui.log.info(
175
+ `Format: ${format}${options.clean ? ' (comments stripped)' : ''}`,
176
+ );
120
177
  this.ui.log.success(`Saved to ${outputPath}`);
121
178
 
122
179
  if (options.copy) {
@@ -130,14 +187,27 @@ export class PackCommand extends CommandRunner {
130
187
  }
131
188
  }
132
189
 
133
- private async buildContext(files: string[]): Promise<string> {
190
+ private async buildContext(
191
+ files: string[],
192
+ format: OutputFormat,
193
+ clean = false,
194
+ ): Promise<string> {
134
195
  const chunks = await Promise.all(
135
196
  files.map(async (file) => {
136
- const content = await this.fsService.readFileRelative(file);
197
+ let content = await this.fsService.readFileRelative(file);
198
+ if (clean) {
199
+ content = this.cleaner.cleanContent(file, content);
200
+ }
201
+ if (format === 'xml') {
202
+ return `<file path="${file}">\n${content}\n</file>`;
203
+ }
137
204
  return `// file: ${file}\n${content}`;
138
205
  }),
139
206
  );
140
207
 
208
+ if (format === 'xml') {
209
+ return `<files>\n${chunks.join('\n\n')}\n</files>`;
210
+ }
141
211
  return chunks.join('\n\n');
142
212
  }
143
213
 
@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
2
2
  import { ConfigModule } from '../../core/config/config.module';
3
3
  import { FsModule } from '../../core/file-system/fs.module';
4
4
  import { UiModule } from '../../core/ui/ui.module';
5
+ import { CleanerService } from '../../shared/cleaner/cleaner.service';
5
6
  import { TokenizerModule } from '../../shared/tokenizer/tokenizer.module';
6
7
  import { PackCommand } from './pack.command';
7
8
 
8
9
  @Module({
9
10
  imports: [ConfigModule, UiModule, FsModule, TokenizerModule],
10
- providers: [PackCommand],
11
+ providers: [PackCommand, CleanerService],
11
12
  })
12
13
  export class PackModule {}
@@ -1,43 +1,4 @@
1
1
  import { z } from 'zod';
2
- import {
3
- DEFAULT_COMMIT_TOKENS,
4
- DEFAULT_LLM_MODEL,
5
- DEFAULT_REVIEW_TOKENS,
6
- } from '../../shared/constants';
7
-
8
- // Model ID format: provider/model-name (e.g., "openai/gpt-4o", "anthropic/claude-4-5-sonnet")
9
- const modelIdSchema = z.string().regex(/^[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_.]+$/, {
10
- message:
11
- "Model must be in format 'provider/model-name' (e.g., 'openai/gpt-4o')",
12
- });
13
-
14
- const llmCommandSettingsSchema = z
15
- .object({
16
- maxOutputTokens: z.number().int().positive().optional(),
17
- })
18
- .passthrough();
19
-
20
- const llmCommandSchema = z.object({
21
- modelSettings: llmCommandSettingsSchema.optional(),
22
- });
23
-
24
- const createDefaultCommandSettings = () => ({
25
- commit: { modelSettings: { maxOutputTokens: DEFAULT_COMMIT_TOKENS } },
26
- review: { modelSettings: { maxOutputTokens: DEFAULT_REVIEW_TOKENS } },
27
- });
28
-
29
- const llmCommandsSchema = z
30
- .object({
31
- commit: llmCommandSchema.optional(),
32
- review: llmCommandSchema.optional(),
33
- })
34
- .default(() => createDefaultCommandSettings());
35
-
36
- const llmSchema = z.object({
37
- model: modelIdSchema.default(`openai/${DEFAULT_LLM_MODEL}`),
38
- apiKeyEnv: z.string().default('OPENAI_API_KEY'),
39
- commands: llmCommandsSchema.optional(),
40
- });
41
2
 
42
3
  const cleanerSchema = z.object({
43
4
  whitelist: z.array(z.string()).default(['//!']),
@@ -63,40 +24,14 @@ const packerSchema = z.object({
63
24
  contentBasedBinaryDetection: z.boolean().default(false),
64
25
  });
65
26
 
66
- const promptSourceSchema = z.string();
67
-
68
27
  const promptsSchema = z
69
28
  .object({
70
- review: z.record(z.string(), promptSourceSchema).optional(),
71
- commit: promptSourceSchema.optional(),
72
- pack: promptSourceSchema.optional(),
29
+ pack: z.string().optional(),
73
30
  })
74
31
  .optional();
75
32
 
76
- const serverPathsSchema = z
77
- .object({
78
- apps: z.string().default('/var/agent-apps'),
79
- caddy: z.string().optional(),
80
- })
81
- .optional();
82
-
83
- const serverConfigSchema = z.object({
84
- host: z.string(),
85
- port: z.number().default(22),
86
- user: z.string(),
87
- sshKeyPath: z.string(),
88
- description: z.string().optional(),
89
- paths: serverPathsSchema,
90
- env: z.record(z.string(), z.string()).optional(),
91
- });
92
-
93
- const opsSchema = z.object({
94
- servers: z.record(z.string(), serverConfigSchema),
95
- });
96
-
97
33
  export const configSchema = z.object({
98
34
  $schema: z.string().optional(),
99
- llm: llmSchema.optional(),
100
35
  cleaner: cleanerSchema.default({
101
36
  whitelist: ['//!'],
102
37
  keepJSDoc: true,
@@ -118,8 +53,6 @@ export const configSchema = z.object({
118
53
  contentBasedBinaryDetection: false,
119
54
  }),
120
55
  prompts: promptsSchema,
121
- ops: opsSchema.optional(),
122
56
  });
123
57
 
124
58
  export type KoduConfig = z.infer<typeof configSchema>;
125
- export type ServerConfig = z.infer<typeof serverConfigSchema>;
@@ -21,14 +21,14 @@ export class ConfigService {
21
21
 
22
22
  if (!result || result.isEmpty || !result.config) {
23
23
  this.terminate(
24
- 'kodu.json config not found. Run `kodu init` to create the file.',
24
+ 'kodu.json not found. Create it in the project root to configure kodu.',
25
25
  );
26
26
  }
27
27
 
28
28
  const parsed = configSchema.safeParse(result.config);
29
29
 
30
30
  if (!parsed.success) {
31
- console.error(pc.red('kodu.json config is invalid:'));
31
+ console.error(pc.red('kodu.json is invalid:'));
32
32
  parsed.error.issues.forEach((issue) => {
33
33
  const path = issue.path.join('.') || '(root)';
34
34
  console.error(pc.red(`- ${path}: ${issue.message}`));
@@ -1,19 +1,13 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Injectable } from '@nestjs/common';
4
- import { replacePromptVariables } from './default-prompts';
5
-
6
- type Variables = Record<string, string | number>;
7
4
 
8
5
  @Injectable()
9
6
  export class PromptService {
10
7
  private readonly cache = new Map<string, string>();
11
8
 
12
- async load(source: string, variables?: Variables): Promise<string> {
13
- const raw = await this.readSource(source);
14
- return variables
15
- ? replacePromptVariables(raw, this.normalize(variables))
16
- : raw;
9
+ async load(source: string): Promise<string> {
10
+ return this.readSource(source);
17
11
  }
18
12
 
19
13
  async loadFromPromptsDir(name: string): Promise<string> {
@@ -66,29 +60,13 @@ export class PromptService {
66
60
  }
67
61
  const hasPathSegments = value.includes('/') || value.includes('\\');
68
62
  const hasExtension = path.extname(value) !== '';
69
-
70
63
  return value.trim().length > 0 && !hasPathSegments && !hasExtension;
71
64
  }
72
65
 
73
66
  private buildCandidates(name: string): string[] {
74
67
  const names = path.extname(name) ? [name] : [`${name}.md`, `${name}.txt`];
75
-
76
- const roots = [path.join(process.cwd(), '.kodu', 'prompts')];
77
-
78
- const candidates: string[] = [];
79
- for (const root of roots) {
80
- for (const variant of names) {
81
- candidates.push(path.join(root, variant));
82
- }
83
- }
84
-
85
- return candidates;
86
- }
87
-
88
- private normalize(variables: Variables): Record<string, string> {
89
- return Object.fromEntries(
90
- Object.entries(variables).map(([key, value]) => [key, value.toString()]),
91
- );
68
+ const root = path.join(process.cwd(), '.kodu', 'prompts');
69
+ return names.map((variant) => path.join(root, variant));
92
70
  }
93
71
 
94
72
  private async exists(target: string): Promise<boolean> {