kodu 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/AGENTS.md +36 -68
  2. package/README.md +97 -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/init/init.command.d.ts +2 -9
  7. package/dist/src/commands/init/init.command.js +15 -241
  8. package/dist/src/commands/init/init.command.js.map +1 -1
  9. package/dist/src/commands/pack/pack.command.d.ts +7 -0
  10. package/dist/src/commands/pack/pack.command.js +59 -3
  11. package/dist/src/commands/pack/pack.command.js.map +1 -1
  12. package/dist/src/core/config/config.schema.d.ts +0 -46
  13. package/dist/src/core/config/config.schema.js +1 -51
  14. package/dist/src/core/config/config.schema.js.map +1 -1
  15. package/dist/src/core/config/config.service.js +2 -2
  16. package/dist/src/core/config/config.service.js.map +1 -1
  17. package/dist/src/core/config/prompt.service.d.ts +1 -4
  18. package/dist/src/core/config/prompt.service.js +4 -17
  19. package/dist/src/core/config/prompt.service.js.map +1 -1
  20. package/dist/src/shared/constants.d.ts +0 -4
  21. package/dist/src/shared/constants.js +1 -5
  22. package/dist/src/shared/constants.js.map +1 -1
  23. package/dist/src/shared/git/git.module.js +0 -2
  24. package/dist/src/shared/git/git.module.js.map +1 -1
  25. package/dist/src/shared/git/git.service.d.ts +0 -8
  26. package/dist/src/shared/git/git.service.js +2 -34
  27. package/dist/src/shared/git/git.service.js.map +1 -1
  28. package/dist/src/shared/tokenizer/tokenizer.module.js +0 -2
  29. package/dist/src/shared/tokenizer/tokenizer.module.js.map +1 -1
  30. package/dist/src/shared/tokenizer/tokenizer.service.d.ts +0 -6
  31. package/dist/src/shared/tokenizer/tokenizer.service.js +8 -38
  32. package/dist/src/shared/tokenizer/tokenizer.service.js.map +1 -1
  33. package/dist/tsconfig.build.tsbuildinfo +1 -1
  34. package/kodu.schema.json +0 -139
  35. package/package.json +1 -2
  36. package/src/app.module.ts +0 -8
  37. package/src/commands/init/init.command.ts +15 -310
  38. package/src/commands/pack/pack.command.ts +56 -3
  39. package/src/core/config/config.schema.ts +1 -68
  40. package/src/core/config/config.service.ts +2 -2
  41. package/src/core/config/prompt.service.ts +4 -26
  42. package/src/shared/constants.ts +0 -4
  43. package/src/shared/git/git.module.ts +0 -2
  44. package/src/shared/git/git.service.ts +1 -33
  45. package/src/shared/tokenizer/tokenizer.module.ts +0 -2
  46. package/src/shared/tokenizer/tokenizer.service.ts +9 -39
  47. package/.kodu/prompts/.keep +0 -0
  48. package/.kodu/prompts/commit.md +0 -9
  49. package/.kodu/prompts/pack.md +0 -7
  50. package/.kodu/prompts/review-bug.md +0 -6
  51. package/.kodu/prompts/review-security.md +0 -6
  52. package/.kodu/prompts/review-style.md +0 -6
  53. package/.opencode/command/openspec-apply.md +0 -24
  54. package/.opencode/command/openspec-archive.md +0 -27
  55. package/.opencode/command/openspec-proposal.md +0 -29
  56. package/.opencode/skills/kodu-ops/SKILL.md +0 -184
  57. package/dist/src/commands/commit/commit.command.d.ts +0 -18
  58. package/dist/src/commands/commit/commit.command.js +0 -149
  59. package/dist/src/commands/commit/commit.command.js.map +0 -1
  60. package/dist/src/commands/commit/commit.module.d.ts +0 -2
  61. package/dist/src/commands/commit/commit.module.js +0 -25
  62. package/dist/src/commands/commit/commit.module.js.map +0 -1
  63. package/dist/src/commands/ops/ops.command.d.ts +0 -4
  64. package/dist/src/commands/ops/ops.command.js +0 -39
  65. package/dist/src/commands/ops/ops.command.js.map +0 -1
  66. package/dist/src/commands/ops/ops.module.d.ts +0 -2
  67. package/dist/src/commands/ops/ops.module.js +0 -33
  68. package/dist/src/commands/ops/ops.module.js.map +0 -1
  69. package/dist/src/commands/ops/ops.types.d.ts +0 -13
  70. package/dist/src/commands/ops/ops.types.js +0 -12
  71. package/dist/src/commands/ops/ops.types.js.map +0 -1
  72. package/dist/src/commands/ops/ops.utils.d.ts +0 -13
  73. package/dist/src/commands/ops/ops.utils.js +0 -121
  74. package/dist/src/commands/ops/ops.utils.js.map +0 -1
  75. package/dist/src/commands/ops/subcommands/ops-env.command.d.ts +0 -24
  76. package/dist/src/commands/ops/subcommands/ops-env.command.js +0 -156
  77. package/dist/src/commands/ops/subcommands/ops-env.command.js.map +0 -1
  78. package/dist/src/commands/ops/subcommands/ops-routes.command.d.ts +0 -22
  79. package/dist/src/commands/ops/subcommands/ops-routes.command.js +0 -203
  80. package/dist/src/commands/ops/subcommands/ops-routes.command.js.map +0 -1
  81. package/dist/src/commands/ops/subcommands/ops-service.command.d.ts +0 -22
  82. package/dist/src/commands/ops/subcommands/ops-service.command.js +0 -169
  83. package/dist/src/commands/ops/subcommands/ops-service.command.js.map +0 -1
  84. package/dist/src/commands/ops/subcommands/ops-sysinfo.command.d.ts +0 -14
  85. package/dist/src/commands/ops/subcommands/ops-sysinfo.command.js +0 -75
  86. package/dist/src/commands/ops/subcommands/ops-sysinfo.command.js.map +0 -1
  87. package/dist/src/commands/review/review.command.d.ts +0 -26
  88. package/dist/src/commands/review/review.command.js +0 -205
  89. package/dist/src/commands/review/review.command.js.map +0 -1
  90. package/dist/src/commands/review/review.module.d.ts +0 -2
  91. package/dist/src/commands/review/review.module.js +0 -26
  92. package/dist/src/commands/review/review.module.js.map +0 -1
  93. package/dist/src/core/config/default-prompts.d.ts +0 -9
  94. package/dist/src/core/config/default-prompts.js +0 -49
  95. package/dist/src/core/config/default-prompts.js.map +0 -1
  96. package/dist/src/shared/ai/ai.module.d.ts +0 -2
  97. package/dist/src/shared/ai/ai.module.js +0 -23
  98. package/dist/src/shared/ai/ai.module.js.map +0 -1
  99. package/dist/src/shared/ai/ai.service.d.ts +0 -22
  100. package/dist/src/shared/ai/ai.service.js +0 -164
  101. package/dist/src/shared/ai/ai.service.js.map +0 -1
  102. package/dist/src/shared/ssh/ssh.module.d.ts +0 -2
  103. package/dist/src/shared/ssh/ssh.module.js +0 -21
  104. package/dist/src/shared/ssh/ssh.module.js.map +0 -1
  105. package/dist/src/shared/ssh/ssh.service.d.ts +0 -11
  106. package/dist/src/shared/ssh/ssh.service.js +0 -53
  107. package/dist/src/shared/ssh/ssh.service.js.map +0 -1
  108. package/dist/tsconfig.tsbuildinfo +0 -1
  109. package/src/commands/commit/commit.command.ts +0 -139
  110. package/src/commands/commit/commit.module.ts +0 -12
  111. package/src/commands/ops/ops.command.ts +0 -30
  112. package/src/commands/ops/ops.module.ts +0 -20
  113. package/src/commands/ops/ops.types.ts +0 -24
  114. package/src/commands/ops/ops.utils.ts +0 -160
  115. package/src/commands/ops/subcommands/ops-env.command.ts +0 -165
  116. package/src/commands/ops/subcommands/ops-routes.command.ts +0 -221
  117. package/src/commands/ops/subcommands/ops-service.command.ts +0 -190
  118. package/src/commands/ops/subcommands/ops-sysinfo.command.ts +0 -77
  119. package/src/commands/review/review.command.ts +0 -199
  120. package/src/commands/review/review.module.ts +0 -13
  121. package/src/core/config/default-prompts.ts +0 -53
  122. package/src/shared/ai/ai.module.ts +0 -10
  123. package/src/shared/ai/ai.service.ts +0 -216
  124. package/src/shared/ssh/ssh.module.ts +0 -8
  125. package/src/shared/ssh/ssh.service.ts +0 -61
package/kodu.schema.json CHANGED
@@ -5,74 +5,6 @@
5
5
  "$schema": {
6
6
  "type": "string"
7
7
  },
8
- "llm": {
9
- "type": "object",
10
- "properties": {
11
- "model": {
12
- "default": "openai/gpt-4o",
13
- "type": "string",
14
- "pattern": "^[a-zA-Z0-9-_]+\\/[a-zA-Z0-9-_.]+$"
15
- },
16
- "apiKeyEnv": {
17
- "default": "OPENAI_API_KEY",
18
- "type": "string"
19
- },
20
- "commands": {
21
- "default": {
22
- "commit": {
23
- "modelSettings": {
24
- "maxOutputTokens": 1500
25
- }
26
- },
27
- "review": {
28
- "modelSettings": {
29
- "maxOutputTokens": 5000
30
- }
31
- }
32
- },
33
- "type": "object",
34
- "properties": {
35
- "commit": {
36
- "type": "object",
37
- "properties": {
38
- "modelSettings": {
39
- "type": "object",
40
- "properties": {
41
- "maxOutputTokens": {
42
- "type": "integer",
43
- "exclusiveMinimum": 0,
44
- "maximum": 9007199254740991
45
- }
46
- },
47
- "additionalProperties": {}
48
- }
49
- },
50
- "additionalProperties": false
51
- },
52
- "review": {
53
- "type": "object",
54
- "properties": {
55
- "modelSettings": {
56
- "type": "object",
57
- "properties": {
58
- "maxOutputTokens": {
59
- "type": "integer",
60
- "exclusiveMinimum": 0,
61
- "maximum": 9007199254740991
62
- }
63
- },
64
- "additionalProperties": {}
65
- }
66
- },
67
- "additionalProperties": false
68
- }
69
- },
70
- "additionalProperties": false
71
- }
72
- },
73
- "required": ["model", "apiKeyEnv"],
74
- "additionalProperties": false
75
- },
76
8
  "cleaner": {
77
9
  "default": {
78
10
  "whitelist": ["//!"],
@@ -156,82 +88,11 @@
156
88
  "prompts": {
157
89
  "type": "object",
158
90
  "properties": {
159
- "review": {
160
- "type": "object",
161
- "propertyNames": {
162
- "type": "string"
163
- },
164
- "additionalProperties": {
165
- "type": "string"
166
- }
167
- },
168
- "commit": {
169
- "type": "string"
170
- },
171
91
  "pack": {
172
92
  "type": "string"
173
93
  }
174
94
  },
175
95
  "additionalProperties": false
176
- },
177
- "ops": {
178
- "type": "object",
179
- "properties": {
180
- "servers": {
181
- "type": "object",
182
- "propertyNames": {
183
- "type": "string"
184
- },
185
- "additionalProperties": {
186
- "type": "object",
187
- "properties": {
188
- "host": {
189
- "type": "string"
190
- },
191
- "port": {
192
- "default": 22,
193
- "type": "number"
194
- },
195
- "user": {
196
- "type": "string"
197
- },
198
- "sshKeyPath": {
199
- "type": "string"
200
- },
201
- "description": {
202
- "type": "string"
203
- },
204
- "paths": {
205
- "type": "object",
206
- "properties": {
207
- "apps": {
208
- "default": "/var/agent-apps",
209
- "type": "string"
210
- },
211
- "caddy": {
212
- "type": "string"
213
- }
214
- },
215
- "required": ["apps"],
216
- "additionalProperties": false
217
- },
218
- "env": {
219
- "type": "object",
220
- "propertyNames": {
221
- "type": "string"
222
- },
223
- "additionalProperties": {
224
- "type": "string"
225
- }
226
- }
227
- },
228
- "required": ["host", "port", "user", "sshKeyPath"],
229
- "additionalProperties": false
230
- }
231
- }
232
- },
233
- "required": ["servers"],
234
- "additionalProperties": false
235
96
  }
236
97
  },
237
98
  "required": ["cleaner", "packer"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodu",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "High-performance CLI to prepare codebase for LLMs, automate reviews, and draft commits.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -50,7 +50,6 @@
50
50
  "@inquirer/confirm": "^6.0.4",
51
51
  "@inquirer/input": "^5.0.4",
52
52
  "@inquirer/select": "^5.0.4",
53
- "@mastra/core": "^1.0.4",
54
53
  "@nestjs/common": "^11.0.1",
55
54
  "@nestjs/core": "^11.0.1",
56
55
  "clipboardy": "^5.0.2",
package/src/app.module.ts CHANGED
@@ -1,14 +1,10 @@
1
1
  import { Module } from '@nestjs/common';
2
2
  import { CleanModule } from './commands/clean/clean.module';
3
- import { CommitModule } from './commands/commit/commit.module';
4
3
  import { InitModule } from './commands/init/init.module';
5
- import { OpsModule } from './commands/ops/ops.module';
6
4
  import { PackModule } from './commands/pack/pack.module';
7
- import { ReviewModule } from './commands/review/review.module';
8
5
  import { ConfigModule } from './core/config/config.module';
9
6
  import { FsModule } from './core/file-system/fs.module';
10
7
  import { UiModule } from './core/ui/ui.module';
11
- import { AiModule } from './shared/ai/ai.module';
12
8
  import { GitModule } from './shared/git/git.module';
13
9
  import { TokenizerModule } from './shared/tokenizer/tokenizer.module';
14
10
 
@@ -18,14 +14,10 @@ import { TokenizerModule } from './shared/tokenizer/tokenizer.module';
18
14
  UiModule,
19
15
  FsModule,
20
16
  GitModule,
21
- AiModule,
22
17
  TokenizerModule,
23
18
  InitModule,
24
19
  PackModule,
25
20
  CleanModule,
26
- ReviewModule,
27
- CommitModule,
28
- OpsModule,
29
21
  ],
30
22
  })
31
23
  export class AppModule {}
@@ -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;
@@ -8,11 +8,16 @@ import { FsService } from '../../core/file-system/fs.service';
8
8
  import { UiService } from '../../core/ui/ui.service';
9
9
  import { TokenizerService } from '../../shared/tokenizer/tokenizer.service';
10
10
 
11
+ type OutputFormat = 'xml' | 'text';
12
+
11
13
  type PackOptions = {
12
14
  copy?: boolean;
13
15
  template?: string;
14
16
  out?: string;
15
17
  path?: string[];
18
+ exclude?: string[];
19
+ list?: boolean;
20
+ format?: OutputFormat;
16
21
  };
17
22
 
18
23
  type TemplateContext = {
@@ -66,6 +71,34 @@ export class PackCommand extends CommandRunner {
66
71
  return [...previous, value];
67
72
  }
68
73
 
74
+ @Option({
75
+ flags: '-e, --exclude <pattern>',
76
+ description: 'Additional exclude pattern (repeatable)',
77
+ })
78
+ parseExclude(value: string, previous: string[] = []): string[] {
79
+ return [...previous, value];
80
+ }
81
+
82
+ @Option({
83
+ flags: '-l, --list',
84
+ description: 'Print file list only, without content',
85
+ })
86
+ parseList(): boolean {
87
+ return true;
88
+ }
89
+
90
+ @Option({
91
+ flags: '-f, --format <format>',
92
+ description: 'Output format: xml (default) or text',
93
+ })
94
+ parseFormat(value: string): OutputFormat {
95
+ if (value !== 'xml' && value !== 'text') {
96
+ this.ui.log.warn(`Unknown format "${value}", using "xml"`);
97
+ return 'xml';
98
+ }
99
+ return value;
100
+ }
101
+
69
102
  async run(_inputs: string[], options: PackOptions): Promise<void> {
70
103
  const spinner = this.ui
71
104
  .createSpinner({ text: 'Collecting files...' })
@@ -73,10 +106,11 @@ export class PackCommand extends CommandRunner {
73
106
 
74
107
  try {
75
108
  const { packer } = this.configService.getConfig();
109
+ const extraExcludes = options.exclude ?? [];
76
110
  const files = await this.fsService.findProjectFiles({
77
111
  excludeBinary: true,
78
112
  useGitignore: packer.useGitignore,
79
- ignore: packer.ignore,
113
+ ignore: [...packer.ignore, ...extraExcludes],
80
114
  contentBasedBinaryDetection: packer.contentBasedBinaryDetection,
81
115
  rootPaths: options.path,
82
116
  });
@@ -87,7 +121,16 @@ export class PackCommand extends CommandRunner {
87
121
  return;
88
122
  }
89
123
 
90
- const context = await this.buildContext(files);
124
+ if (options.list) {
125
+ spinner.success(`Found ${files.length} files`);
126
+ for (const file of files) {
127
+ this.ui.log.info(file);
128
+ }
129
+ return;
130
+ }
131
+
132
+ const format: OutputFormat = options.format ?? 'xml';
133
+ const context = await this.buildContext(files, format);
91
134
  const fileList = files.join('\n');
92
135
  const { tokens, usdEstimate } = this.tokenizer.count(context);
93
136
 
@@ -117,6 +160,7 @@ export class PackCommand extends CommandRunner {
117
160
  this.ui.log.info(`Files: ${files.length}`);
118
161
  this.ui.log.info(`Tokens: ${tokens}`);
119
162
  this.ui.log.info(`Cost estimate: ~$${usdEstimate.toFixed(4)}`);
163
+ this.ui.log.info(`Format: ${format}`);
120
164
  this.ui.log.success(`Saved to ${outputPath}`);
121
165
 
122
166
  if (options.copy) {
@@ -130,14 +174,23 @@ export class PackCommand extends CommandRunner {
130
174
  }
131
175
  }
132
176
 
133
- private async buildContext(files: string[]): Promise<string> {
177
+ private async buildContext(
178
+ files: string[],
179
+ format: OutputFormat,
180
+ ): Promise<string> {
134
181
  const chunks = await Promise.all(
135
182
  files.map(async (file) => {
136
183
  const content = await this.fsService.readFileRelative(file);
184
+ if (format === 'xml') {
185
+ return `<file path="${file}">\n${content}\n</file>`;
186
+ }
137
187
  return `// file: ${file}\n${content}`;
138
188
  }),
139
189
  );
140
190
 
191
+ if (format === 'xml') {
192
+ return `<files>\n${chunks.join('\n\n')}\n</files>`;
193
+ }
141
194
  return chunks.join('\n\n');
142
195
  }
143
196