golem-cc 2.1.2 → 3.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.
- package/.claude/commands/golem/build.md +18 -0
- package/.claude/commands/golem/config.md +39 -0
- package/.claude/commands/golem/continue.md +73 -0
- package/.claude/commands/golem/doctor.md +46 -0
- package/.claude/commands/golem/document.md +138 -0
- package/.claude/commands/golem/help.md +58 -0
- package/.claude/commands/golem/pause.md +130 -0
- package/.claude/commands/golem/plan.md +111 -0
- package/.claude/commands/golem/review.md +166 -0
- package/.claude/commands/golem/security.md +186 -0
- package/.claude/commands/golem/simplify.md +76 -0
- package/.claude/commands/golem/spec.md +105 -0
- package/.claude/commands/golem/status.md +33 -0
- package/.golem/agents/code-simplifier.md +54 -0
- package/.golem/agents/review-architecture.md +59 -0
- package/.golem/agents/review-logic.md +50 -0
- package/.golem/agents/review-security.md +50 -0
- package/.golem/agents/review-style.md +48 -0
- package/.golem/agents/review-tests.md +48 -0
- package/.golem/agents/spec-builder.md +60 -0
- package/.golem/bin/golem.mjs +270 -0
- package/.golem/lib/build.mjs +557 -0
- package/.golem/lib/claude.mjs +95 -0
- package/.golem/lib/config.mjs +421 -0
- package/.golem/lib/display.mjs +191 -0
- package/.golem/lib/doctor.mjs +197 -0
- package/.golem/lib/document.mjs +792 -0
- package/.golem/lib/gates.mjs +78 -0
- package/.golem/lib/init.mjs +166 -0
- package/.golem/lib/output.mjs +40 -0
- package/.golem/lib/ratelimit.mjs +86 -0
- package/.golem/lib/security.mjs +603 -0
- package/.golem/lib/simplify.mjs +101 -0
- package/.golem/lib/tui.mjs +368 -0
- package/.golem/lib/usage.mjs +119 -0
- package/.golem/lib/worktree.mjs +509 -0
- package/.golem/prompts/build.md +23 -0
- package/.golem/prompts/document-inline.md +66 -0
- package/.golem/prompts/document-markdown.md +80 -0
- package/.golem/prompts/simplify.md +35 -0
- package/README.md +141 -142
- package/bin/golem-shim.mjs +36 -0
- package/bin/install.mjs +193 -0
- package/package.json +27 -32
- package/.env.example +0 -17
- package/bin/golem +0 -1040
- package/commands/golem/build.md +0 -235
- package/commands/golem/config.md +0 -55
- package/commands/golem/doctor.md +0 -137
- package/commands/golem/help.md +0 -212
- package/commands/golem/plan.md +0 -214
- package/commands/golem/review.md +0 -376
- package/commands/golem/security.md +0 -204
- package/commands/golem/simplify.md +0 -94
- package/commands/golem/spec.md +0 -226
- package/commands/golem/status.md +0 -60
- package/dist/api/freshworks.d.ts +0 -61
- package/dist/api/freshworks.d.ts.map +0 -1
- package/dist/api/freshworks.js +0 -119
- package/dist/api/freshworks.js.map +0 -1
- package/dist/api/gitea.d.ts +0 -96
- package/dist/api/gitea.d.ts.map +0 -1
- package/dist/api/gitea.js +0 -154
- package/dist/api/gitea.js.map +0 -1
- package/dist/cli/index.d.ts +0 -9
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -352
- package/dist/cli/index.js.map +0 -1
- package/dist/sync/ticket-sync.d.ts +0 -53
- package/dist/sync/ticket-sync.d.ts.map +0 -1
- package/dist/sync/ticket-sync.js +0 -226
- package/dist/sync/ticket-sync.js.map +0 -1
- package/dist/types.d.ts +0 -125
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/dist/worktree/manager.d.ts +0 -54
- package/dist/worktree/manager.d.ts.map +0 -1
- package/dist/worktree/manager.js +0 -190
- package/dist/worktree/manager.js.map +0 -1
- package/golem/agents/code-simplifier.md +0 -81
- package/golem/agents/spec-builder.md +0 -90
- package/golem/prompts/PROMPT_build.md +0 -71
- package/golem/prompts/PROMPT_plan.md +0 -102
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { readFile, writeFile, access, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
const DEFAULTS = {
|
|
7
|
+
model: 'opus',
|
|
8
|
+
autoCommit: true,
|
|
9
|
+
simplifyOnBuild: true,
|
|
10
|
+
documentOnBuild: false,
|
|
11
|
+
docsPath: 'docs/',
|
|
12
|
+
maxRetries: 3,
|
|
13
|
+
rateLimitWarnThreshold: 80,
|
|
14
|
+
gates: [
|
|
15
|
+
{ name: 'tests', command: 'node --test', enabled: true },
|
|
16
|
+
{ name: 'lint', command: 'npx eslint .', enabled: false },
|
|
17
|
+
{ name: 'typecheck', command: 'npx tsc --noEmit', enabled: false },
|
|
18
|
+
],
|
|
19
|
+
worktree: {
|
|
20
|
+
dir: '~/code/.worktrees',
|
|
21
|
+
copies: ['.golem/', '.claude/', '.env'],
|
|
22
|
+
excludes: ['.golem/IMPLEMENTATION_PLAN.md', '.golem/specs/', '.golem/HANDOFF.md', '.golem/SECURITY_REPORT.md'],
|
|
23
|
+
setup: 'pnpm install',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function golemDir() {
|
|
28
|
+
return join(process.cwd(), '.golem');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function loadConfig() {
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(join(golemDir(), 'config.json'), 'utf-8');
|
|
34
|
+
const userConfig = JSON.parse(raw);
|
|
35
|
+
// Deep merge for nested objects like worktree
|
|
36
|
+
return {
|
|
37
|
+
...DEFAULTS,
|
|
38
|
+
...userConfig,
|
|
39
|
+
worktree: { ...DEFAULTS.worktree, ...(userConfig.worktree || {}) },
|
|
40
|
+
};
|
|
41
|
+
} catch {
|
|
42
|
+
return { ...DEFAULTS };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function saveConfig(config) {
|
|
47
|
+
await writeFile(join(golemDir(), 'config.json'), JSON.stringify(config, null, 2) + '\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function setConfigValue(key, value) {
|
|
51
|
+
const config = await loadConfig();
|
|
52
|
+
|
|
53
|
+
// Handle nested keys like worktree.dir
|
|
54
|
+
if (key.includes('.')) {
|
|
55
|
+
const [parent, child] = key.split('.');
|
|
56
|
+
if (!(parent in DEFAULTS)) {
|
|
57
|
+
throw new Error(`Unknown config key: ${parent}. Valid keys: ${Object.keys(DEFAULTS).join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
if (typeof DEFAULTS[parent] !== 'object' || Array.isArray(DEFAULTS[parent])) {
|
|
60
|
+
throw new Error(`Config key ${parent} is not a nested object`);
|
|
61
|
+
}
|
|
62
|
+
if (!(child in DEFAULTS[parent])) {
|
|
63
|
+
throw new Error(`Unknown nested config key: ${key}. Valid keys for ${parent}: ${Object.keys(DEFAULTS[parent]).join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const defaultVal = DEFAULTS[parent][child];
|
|
67
|
+
if (typeof defaultVal === 'boolean') {
|
|
68
|
+
value = value === 'true';
|
|
69
|
+
} else if (typeof defaultVal === 'number') {
|
|
70
|
+
value = Number(value);
|
|
71
|
+
} else if (Array.isArray(defaultVal)) {
|
|
72
|
+
value = typeof value === 'string' ? JSON.parse(value) : value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
config[parent] = { ...config[parent], [child]: value };
|
|
76
|
+
} else {
|
|
77
|
+
if (!(key in DEFAULTS)) {
|
|
78
|
+
throw new Error(`Unknown config key: ${key}. Valid keys: ${Object.keys(DEFAULTS).join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
const defaultVal = DEFAULTS[key];
|
|
81
|
+
if (typeof defaultVal === 'boolean') {
|
|
82
|
+
value = value === 'true';
|
|
83
|
+
} else if (typeof defaultVal === 'number') {
|
|
84
|
+
value = Number(value);
|
|
85
|
+
} else if (Array.isArray(defaultVal)) {
|
|
86
|
+
value = typeof value === 'string' ? JSON.parse(value) : value;
|
|
87
|
+
}
|
|
88
|
+
config[key] = value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await saveConfig(config);
|
|
92
|
+
return config;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function detectFramework() {
|
|
96
|
+
const checks = [
|
|
97
|
+
{ file: 'nuxt.config.ts', framework: 'nuxt' },
|
|
98
|
+
{ file: 'nuxt.config.js', framework: 'nuxt' },
|
|
99
|
+
{ file: 'next.config.ts', framework: 'next' },
|
|
100
|
+
{ file: 'next.config.js', framework: 'next' },
|
|
101
|
+
{ file: 'next.config.mjs', framework: 'next' },
|
|
102
|
+
];
|
|
103
|
+
for (const { file, framework } of checks) {
|
|
104
|
+
if (await fileExists(join(process.cwd(), file))) return framework;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const _detectionCache = new Map();
|
|
110
|
+
|
|
111
|
+
export function resetDetectionCache() {
|
|
112
|
+
_detectionCache.clear();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function detectProject(root) {
|
|
116
|
+
const dir = root || process.cwd();
|
|
117
|
+
|
|
118
|
+
if (_detectionCache.has(dir)) return _detectionCache.get(dir);
|
|
119
|
+
|
|
120
|
+
const [language, framework, moduleSystem, databaseTooling] = await Promise.all([
|
|
121
|
+
detectLanguage(dir),
|
|
122
|
+
detectFrameworkFull(dir),
|
|
123
|
+
detectModuleSystem(dir),
|
|
124
|
+
detectDatabaseTooling(dir),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const inlineDocStandard = mapDocStandard(language);
|
|
128
|
+
|
|
129
|
+
const result = { language, framework, moduleSystem, databaseTooling, inlineDocStandard };
|
|
130
|
+
_detectionCache.set(dir, result);
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function fileExists(filePath) {
|
|
135
|
+
try {
|
|
136
|
+
await access(filePath);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readJson(filePath) {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(await readFile(filePath, 'utf-8'));
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function hasFilesWithExt(dir, extensions) {
|
|
152
|
+
try {
|
|
153
|
+
const entries = await readdir(dir);
|
|
154
|
+
return entries.some(e => extensions.some(ext => e.endsWith(ext)));
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function detectLanguage(dir) {
|
|
161
|
+
if (await fileExists(join(dir, 'tsconfig.json'))) return 'typescript';
|
|
162
|
+
if (await hasFilesWithExt(dir, ['.ts', '.tsx'])) return 'typescript';
|
|
163
|
+
|
|
164
|
+
for (const f of ['pyproject.toml', 'setup.py', 'requirements.txt']) {
|
|
165
|
+
if (await fileExists(join(dir, f))) return 'python';
|
|
166
|
+
}
|
|
167
|
+
if (await hasFilesWithExt(dir, ['.py'])) return 'python';
|
|
168
|
+
|
|
169
|
+
return 'javascript';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function detectFrameworkFull(dir) {
|
|
173
|
+
const pkg = await readJson(join(dir, 'package.json'));
|
|
174
|
+
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
|
175
|
+
|
|
176
|
+
for (const ext of ['.ts', '.js', '.mjs']) {
|
|
177
|
+
if (await fileExists(join(dir, `nuxt.config${ext}`))) return 'nuxt';
|
|
178
|
+
}
|
|
179
|
+
if (deps.nuxt) return 'nuxt';
|
|
180
|
+
|
|
181
|
+
for (const ext of ['.ts', '.js', '.mjs']) {
|
|
182
|
+
if (await fileExists(join(dir, `next.config${ext}`))) return 'next';
|
|
183
|
+
}
|
|
184
|
+
if (deps.next) return 'next';
|
|
185
|
+
|
|
186
|
+
if (deps.vue) return 'vue';
|
|
187
|
+
if (await hasFilesWithExt(dir, ['.vue'])) return 'vue';
|
|
188
|
+
|
|
189
|
+
if (deps.react) return 'react';
|
|
190
|
+
if (await hasFilesWithExt(dir, ['.jsx', '.tsx'])) return 'react';
|
|
191
|
+
|
|
192
|
+
return 'none';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function detectModuleSystem(dir) {
|
|
196
|
+
const pkg = await readJson(join(dir, 'package.json'));
|
|
197
|
+
const lang = await detectLanguage(dir);
|
|
198
|
+
if (lang === 'python') return 'n/a';
|
|
199
|
+
|
|
200
|
+
if (pkg?.type === 'module') return 'esm';
|
|
201
|
+
if (await hasFilesWithExt(dir, ['.mjs'])) return 'esm';
|
|
202
|
+
if (pkg) return 'commonjs';
|
|
203
|
+
|
|
204
|
+
return 'n/a';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function detectDatabaseTooling(dir) {
|
|
208
|
+
if (await fileExists(join(dir, 'prisma', 'schema.prisma'))) return 'prisma';
|
|
209
|
+
if (await fileExists(join(dir, 'prisma'))) return 'prisma';
|
|
210
|
+
|
|
211
|
+
if (await fileExists(join(dir, 'drizzle.config.ts'))) return 'drizzle';
|
|
212
|
+
if (await fileExists(join(dir, 'drizzle.config.js'))) return 'drizzle';
|
|
213
|
+
|
|
214
|
+
if (await fileExists(join(dir, 'knexfile.js'))) return 'knex';
|
|
215
|
+
if (await fileExists(join(dir, 'knexfile.ts'))) return 'knex';
|
|
216
|
+
|
|
217
|
+
if (await fileExists(join(dir, 'alembic'))) return 'alembic';
|
|
218
|
+
if (await fileExists(join(dir, 'alembic.ini'))) return 'alembic';
|
|
219
|
+
|
|
220
|
+
const pyproject = await readFile(join(dir, 'pyproject.toml'), 'utf-8').catch(() => null);
|
|
221
|
+
if (pyproject && /sqlalchemy/i.test(pyproject)) return 'sqlalchemy';
|
|
222
|
+
|
|
223
|
+
if (await hasFilesWithExt(dir, ['.sql'])) return 'raw-sql';
|
|
224
|
+
if (await fileExists(join(dir, 'migrations'))) return 'raw-sql';
|
|
225
|
+
|
|
226
|
+
return 'none';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function mapDocStandard(language) {
|
|
230
|
+
return { typescript: 'tsdoc', python: 'google-docstring' }[language] || 'jsdoc';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function detectWorkspace(root) {
|
|
234
|
+
const dir = root || process.cwd();
|
|
235
|
+
const subDirs = await findMonorepoPackages(dir);
|
|
236
|
+
|
|
237
|
+
if (subDirs.length === 0) {
|
|
238
|
+
return { '.': await detectProject(dir) };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const entries = await Promise.all(
|
|
242
|
+
subDirs.map(async (sub) => [sub, await detectProject(join(dir, sub))]),
|
|
243
|
+
);
|
|
244
|
+
return Object.fromEntries(entries);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function findMonorepoPackages(dir) {
|
|
248
|
+
const manifests = ['package.json', 'pyproject.toml'];
|
|
249
|
+
|
|
250
|
+
if (await fileExists(join(dir, 'pnpm-workspace.yaml'))) {
|
|
251
|
+
return collectSubPackages(dir, await parsePnpmWorkspace(dir), manifests);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const lerna = await readJson(join(dir, 'lerna.json'));
|
|
255
|
+
if (lerna) {
|
|
256
|
+
return collectSubPackages(dir, lerna.packages || ['packages/*'], manifests);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const pkg = await readJson(join(dir, 'package.json'));
|
|
260
|
+
if (pkg?.workspaces) {
|
|
261
|
+
const patterns = Array.isArray(pkg.workspaces)
|
|
262
|
+
? pkg.workspaces
|
|
263
|
+
: pkg.workspaces.packages || [];
|
|
264
|
+
return collectSubPackages(dir, patterns, manifests);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const candidates = [];
|
|
268
|
+
for (const name of ['packages', 'apps']) {
|
|
269
|
+
const subDir = join(dir, name);
|
|
270
|
+
try {
|
|
271
|
+
const entries = await readdir(subDir, { withFileTypes: true });
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
if (!entry.isDirectory()) continue;
|
|
274
|
+
const childPath = join(subDir, entry.name);
|
|
275
|
+
for (const manifest of manifests) {
|
|
276
|
+
if (await fileExists(join(childPath, manifest))) {
|
|
277
|
+
candidates.push(`${name}/${entry.name}`);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
284
|
+
return candidates;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function parsePnpmWorkspace(dir) {
|
|
288
|
+
try {
|
|
289
|
+
const content = await readFile(join(dir, 'pnpm-workspace.yaml'), 'utf-8');
|
|
290
|
+
const patterns = [];
|
|
291
|
+
let inPackages = false;
|
|
292
|
+
for (const line of content.split('\n')) {
|
|
293
|
+
const trimmed = line.trim();
|
|
294
|
+
if (trimmed === 'packages:') {
|
|
295
|
+
inPackages = true;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (inPackages) {
|
|
299
|
+
if (/^\s*-\s/.test(line)) {
|
|
300
|
+
const val = trimmed.replace(/^-\s*/, '').replace(/^['"]|['"]$/g, '');
|
|
301
|
+
if (val) patterns.push(val);
|
|
302
|
+
} else if (trimmed && !trimmed.startsWith('#')) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return patterns.length > 0 ? patterns : ['packages/*'];
|
|
308
|
+
} catch {
|
|
309
|
+
return ['packages/*'];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function collectSubPackages(dir, patterns, manifests) {
|
|
314
|
+
const results = [];
|
|
315
|
+
for (const pattern of patterns) {
|
|
316
|
+
const base = pattern.replace(/\/\*\*?$/, '');
|
|
317
|
+
const baseDir = join(dir, base);
|
|
318
|
+
try {
|
|
319
|
+
const entries = await readdir(baseDir, { withFileTypes: true });
|
|
320
|
+
for (const entry of entries) {
|
|
321
|
+
if (!entry.isDirectory()) continue;
|
|
322
|
+
const childPath = join(baseDir, entry.name);
|
|
323
|
+
for (const manifest of manifests) {
|
|
324
|
+
if (await fileExists(join(childPath, manifest))) {
|
|
325
|
+
results.push(`${base}/${entry.name}`);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
return results;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function getDefaults() {
|
|
336
|
+
return {
|
|
337
|
+
...DEFAULTS,
|
|
338
|
+
gates: DEFAULTS.gates.map(g => ({ ...g })),
|
|
339
|
+
worktree: {
|
|
340
|
+
...DEFAULTS.worktree,
|
|
341
|
+
copies: [...DEFAULTS.worktree.copies],
|
|
342
|
+
excludes: [...DEFAULTS.worktree.excludes],
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function toolInstalled(name) {
|
|
348
|
+
try {
|
|
349
|
+
execSync(`which ${name}`, { stdio: 'pipe' });
|
|
350
|
+
return true;
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function expandTilde(path) {
|
|
357
|
+
if (path.startsWith('~/')) {
|
|
358
|
+
return join(homedir(), path.slice(2));
|
|
359
|
+
}
|
|
360
|
+
return path;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function getRepoName() {
|
|
364
|
+
try {
|
|
365
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
366
|
+
cwd: process.cwd(),
|
|
367
|
+
stdio: 'pipe',
|
|
368
|
+
encoding: 'utf-8'
|
|
369
|
+
}).trim();
|
|
370
|
+
|
|
371
|
+
// Parse repo name from URL
|
|
372
|
+
// Examples:
|
|
373
|
+
// - https://github.com/user/repo.git -> repo
|
|
374
|
+
// - git@github.com:user/repo.git -> repo
|
|
375
|
+
// - ssh://git@example.com/repo.git -> repo
|
|
376
|
+
const lastSegment = remoteUrl.split('/').pop();
|
|
377
|
+
return lastSegment.replace(/\.git$/, '');
|
|
378
|
+
} catch {
|
|
379
|
+
// No remote or git not available — fallback to directory name
|
|
380
|
+
return basename(process.cwd());
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function gatherShowInfo() {
|
|
385
|
+
const config = await loadConfig();
|
|
386
|
+
const framework = await detectFramework();
|
|
387
|
+
const cwd = process.cwd();
|
|
388
|
+
|
|
389
|
+
const securityTools = ['gitleaks', 'semgrep', 'trivy'].map(t => ({
|
|
390
|
+
name: t,
|
|
391
|
+
installed: toolInstalled(t),
|
|
392
|
+
}));
|
|
393
|
+
|
|
394
|
+
let agentsSummary = null;
|
|
395
|
+
try {
|
|
396
|
+
const content = await readFile(join(golemDir(), 'AGENTS.md'), 'utf-8');
|
|
397
|
+
const lines = content.split('\n').filter(l => l.trim()).slice(0, 5);
|
|
398
|
+
agentsSummary = lines.join('\n');
|
|
399
|
+
} catch {}
|
|
400
|
+
|
|
401
|
+
const commands = ['build', 'plan', 'security', 'simplify', 'doctor', 'config', 'init', 'status', 'help'];
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
config,
|
|
405
|
+
framework,
|
|
406
|
+
paths: {
|
|
407
|
+
project: cwd,
|
|
408
|
+
golem: golemDir(),
|
|
409
|
+
config: join(golemDir(), 'config.json'),
|
|
410
|
+
},
|
|
411
|
+
securityTools,
|
|
412
|
+
agentsSummary,
|
|
413
|
+
commands,
|
|
414
|
+
worktree: {
|
|
415
|
+
dir: config.worktree.dir,
|
|
416
|
+
copies: config.worktree.copies,
|
|
417
|
+
excludes: config.worktree.excludes,
|
|
418
|
+
setup: config.worktree.setup,
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { spinner as createSpinner, info, success, fail } from './output.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Attach display handlers to a claude event emitter.
|
|
6
|
+
* @param {EventEmitter} emitter
|
|
7
|
+
* @param {{ tui?: object }} opts - pass tui for full-screen mode
|
|
8
|
+
* Returns a context object with { spinner, stats }.
|
|
9
|
+
*/
|
|
10
|
+
export function attachDisplay(emitter, opts = {}) {
|
|
11
|
+
const tui = opts.tui || null;
|
|
12
|
+
let inThinkingBlock = false;
|
|
13
|
+
|
|
14
|
+
const ctx = {
|
|
15
|
+
spinner: null,
|
|
16
|
+
stats: {
|
|
17
|
+
inputTokens: 0,
|
|
18
|
+
outputTokens: 0,
|
|
19
|
+
cacheRead: 0,
|
|
20
|
+
cacheCreation: 0,
|
|
21
|
+
cost: 0,
|
|
22
|
+
durationMs: 0,
|
|
23
|
+
filesModified: [],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const closeThinkingBlock = () => {
|
|
28
|
+
if (inThinkingBlock && tui) {
|
|
29
|
+
tui.appendLog(chalk.dim(' ─'));
|
|
30
|
+
inThinkingBlock = false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
emitter.on('init', (event) => {
|
|
35
|
+
const model = event.model || 'unknown';
|
|
36
|
+
if (tui) {
|
|
37
|
+
tui.appendLog(chalk.bold.cyan(`Golem building (${model})`));
|
|
38
|
+
if (tui.setTaskModel) tui.setTaskModel(model);
|
|
39
|
+
} else {
|
|
40
|
+
ctx.spinner = createSpinner(`Claude (${model})`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
emitter.on('assistant', (event) => {
|
|
45
|
+
const msg = event.message;
|
|
46
|
+
if (!msg?.content) return;
|
|
47
|
+
|
|
48
|
+
// Update model from actual response (detects fallback)
|
|
49
|
+
if (tui && tui.setTaskModel && msg.model) {
|
|
50
|
+
tui.setTaskModel(msg.model);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const block of msg.content) {
|
|
54
|
+
if (block.type === 'text' && block.text) {
|
|
55
|
+
if (tui) {
|
|
56
|
+
const lines = block.text.split('\n').filter(l => l);
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (!inThinkingBlock) {
|
|
59
|
+
inThinkingBlock = true;
|
|
60
|
+
tui.appendLog(chalk.dim(' ─'));
|
|
61
|
+
tui.appendLog(chalk.bold.cyan('[Golem]') + ' ' + chalk.white(line));
|
|
62
|
+
} else {
|
|
63
|
+
tui.appendLog(' ' + chalk.white(line));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
const preview = block.text.slice(0, 80).replace(/\n/g, ' ');
|
|
68
|
+
if (ctx.spinner) ctx.spinner.text = preview;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (block.type === 'tool_use') {
|
|
73
|
+
closeThinkingBlock();
|
|
74
|
+
const name = block.name;
|
|
75
|
+
let label;
|
|
76
|
+
|
|
77
|
+
if (name === 'Read') {
|
|
78
|
+
const filePath = block.input?.file_path || block.input?.path || '';
|
|
79
|
+
const short = filePath ? filePath.replace(process.cwd() + '/', '') : '';
|
|
80
|
+
label = chalk.blue(`[Read]`) + ` ${chalk.blue(short)}`;
|
|
81
|
+
} else if (name === 'Edit') {
|
|
82
|
+
const filePath = block.input?.file_path || block.input?.path || '';
|
|
83
|
+
const short = filePath ? filePath.replace(process.cwd() + '/', '') : '';
|
|
84
|
+
if (short && !ctx.stats.filesModified.includes(short)) {
|
|
85
|
+
ctx.stats.filesModified.push(short);
|
|
86
|
+
}
|
|
87
|
+
label = chalk.yellow(`[Edit]`) + ` ${chalk.yellow(short)}`;
|
|
88
|
+
} else if (name === 'Write') {
|
|
89
|
+
const filePath = block.input?.file_path || block.input?.path || '';
|
|
90
|
+
const short = filePath ? filePath.replace(process.cwd() + '/', '') : '';
|
|
91
|
+
if (short && !ctx.stats.filesModified.includes(short)) {
|
|
92
|
+
ctx.stats.filesModified.push(short);
|
|
93
|
+
}
|
|
94
|
+
label = chalk.green(`[Write]`) + ` ${chalk.green(short)}`;
|
|
95
|
+
} else if (name === 'Bash') {
|
|
96
|
+
const cmd = block.input?.command || '';
|
|
97
|
+
const preview = cmd.slice(0, 60).replace(/\n/g, ' ');
|
|
98
|
+
label = chalk.magenta(`[Bash]`) + ` ${chalk.dim(preview)}`;
|
|
99
|
+
} else if (name === 'Glob' || name === 'Grep') {
|
|
100
|
+
const pattern = block.input?.pattern || block.input?.glob || '';
|
|
101
|
+
label = chalk.cyan(`[${name}]`) + ` ${chalk.dim(pattern)}`;
|
|
102
|
+
} else {
|
|
103
|
+
label = chalk.dim(`[${name}]`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (tui) {
|
|
107
|
+
tui.appendLog(label);
|
|
108
|
+
} else {
|
|
109
|
+
if (ctx.spinner) ctx.spinner.text = label;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (block.type === 'tool_result') {
|
|
114
|
+
closeThinkingBlock();
|
|
115
|
+
if (tui && block.content && typeof block.content === 'string') {
|
|
116
|
+
// Show truncated result in TUI
|
|
117
|
+
const lines = block.content.split('\n');
|
|
118
|
+
const maxLines = 8;
|
|
119
|
+
const shown = lines.slice(0, maxLines);
|
|
120
|
+
for (const line of shown) {
|
|
121
|
+
tui.appendLog(chalk.dim(' ' + line.slice(0, 120)));
|
|
122
|
+
}
|
|
123
|
+
if (lines.length > maxLines) {
|
|
124
|
+
tui.appendLog(chalk.dim(` ... (${lines.length - maxLines} more lines)`));
|
|
125
|
+
}
|
|
126
|
+
} else if (!tui) {
|
|
127
|
+
if (block.content && typeof block.content === 'string') {
|
|
128
|
+
if (block.content.includes('FAIL') || block.content.includes('Error')) {
|
|
129
|
+
if (ctx.spinner) ctx.spinner.text = chalk.red('Tests failing...');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Track token usage from message
|
|
137
|
+
if (msg.usage) {
|
|
138
|
+
const input = msg.usage.input_tokens || 0;
|
|
139
|
+
const output = msg.usage.output_tokens || 0;
|
|
140
|
+
ctx.stats.inputTokens += input;
|
|
141
|
+
ctx.stats.outputTokens += output;
|
|
142
|
+
ctx.stats.cacheRead += msg.usage.cache_read_input_tokens || 0;
|
|
143
|
+
ctx.stats.cacheCreation += msg.usage.cache_creation_input_tokens || 0;
|
|
144
|
+
|
|
145
|
+
if (tui) tui.updateTokens(input, output);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
emitter.on('result', (event) => {
|
|
150
|
+
ctx.stats.durationMs = event.duration_ms || 0;
|
|
151
|
+
ctx.stats.cost = event.total_cost_usd || 0;
|
|
152
|
+
|
|
153
|
+
if (event.usage) {
|
|
154
|
+
ctx.stats.inputTokens = event.usage.input_tokens || ctx.stats.inputTokens;
|
|
155
|
+
ctx.stats.outputTokens = event.usage.output_tokens || ctx.stats.outputTokens;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (tui) {
|
|
159
|
+
// TUI mode: just log completion, build.mjs handles summary
|
|
160
|
+
const status = event.is_error ? chalk.red('Error') : chalk.green('Completed');
|
|
161
|
+
tui.appendLog(`${status} — ${(ctx.stats.durationMs / 1000).toFixed(1)}s | $${ctx.stats.cost.toFixed(4)}`);
|
|
162
|
+
} else {
|
|
163
|
+
if (ctx.spinner) ctx.spinner.stop();
|
|
164
|
+
|
|
165
|
+
if (event.is_error) {
|
|
166
|
+
fail(`Claude error: ${event.result}`);
|
|
167
|
+
} else {
|
|
168
|
+
success('Claude completed');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const secs = (ctx.stats.durationMs / 1000).toFixed(1);
|
|
172
|
+
const tokens = ctx.stats.inputTokens + ctx.stats.outputTokens;
|
|
173
|
+
const cost = ctx.stats.cost.toFixed(4);
|
|
174
|
+
info(`${secs}s | ${tokens} tokens | $${cost}`);
|
|
175
|
+
|
|
176
|
+
if (ctx.stats.filesModified.length > 0) {
|
|
177
|
+
info(`Files modified: ${ctx.stats.filesModified.join(', ')}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
emitter.on('error', (err) => {
|
|
183
|
+
if (tui) {
|
|
184
|
+
tui.appendLog(chalk.red(`Error: ${err.message}`));
|
|
185
|
+
} else {
|
|
186
|
+
if (ctx.spinner) ctx.spinner.fail(err.message);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return ctx;
|
|
191
|
+
}
|