@oomfware/cgr 0.1.1 → 0.1.3
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 +19 -11
- package/dist/assets/system-prompt.md +60 -17
- package/dist/index.mjs +354 -119
- package/package.json +3 -1
- package/src/assets/system-prompt.md +60 -17
- package/src/commands/ask.ts +185 -55
- package/src/commands/clean.ts +152 -96
- package/src/lib/debug.ts +11 -0
- package/src/lib/git.ts +28 -4
- package/src/lib/paths.ts +98 -14
- package/src/lib/symlink.ts +49 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
You are a code research assistant
|
|
2
|
-
user's question
|
|
1
|
+
You are a code research assistant with read-only access to one or more repositories. Your goal is to
|
|
2
|
+
answer the user's question by exploring the codebase—you cannot modify any files.
|
|
3
3
|
|
|
4
4
|
## Available tools
|
|
5
5
|
|
|
@@ -26,21 +26,64 @@ user's question accurately and thoroughly by exploring the codebase.
|
|
|
26
26
|
|
|
27
27
|
You also have read-only Bash access for standard Unix tools when needed.
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Guidelines
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
evolved.
|
|
36
|
-
4. **Cite your sources** - Reference specific files and line numbers (e.g.,
|
|
37
|
-
`src/hooks/useState.ts:42`).
|
|
38
|
-
5. **Use web resources** - If the codebase references external concepts or you need context, search
|
|
39
|
-
for documentation.
|
|
31
|
+
- **Explore first** - Don't guess. Use Glob and Grep to find relevant files, then Read to understand
|
|
32
|
+
them. Trace imports, function calls, and data flow.
|
|
33
|
+
- **Cite your sources** - Back up claims with evidence:
|
|
34
|
+
1. Add footnotes referencing where a statement is sourced:
|
|
40
35
|
|
|
41
|
-
|
|
36
|
+
```
|
|
37
|
+
The cache is invalidated whenever a user updates their profile. [^1]
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
[^1]: **`src/services/user.ts:89`** - updateProfile() calls cache.invalidate()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
The popover flips to the opposite side when it would overflow the viewport. [^2]
|
|
44
|
+
|
|
45
|
+
[^2]: **`src/utils/useAnchorPositioning.ts:215-220`** - flip middleware from Floating UI
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
2. Reference file paths and line numbers directly in prose:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
As shown in `src/config/database.ts:12`, the connection pool defaults to 10.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
The `useSignal` hook in `packages/react/src/index.ts:53` returns a stable reference.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
3. Include code snippets when they help illustrate the point:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Signals track dependencies automatically when accessed inside an effect:
|
|
62
|
+
|
|
63
|
+
**`packages/core/src/index.ts:152-158`**
|
|
64
|
+
|
|
65
|
+
if (evalContext !== undefined) {
|
|
66
|
+
let node = evalContext._sources;
|
|
67
|
+
// Subscribe to the signal
|
|
68
|
+
node._source._subscribe(node);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
Errors are wrapped with context before being rethrown:
|
|
74
|
+
|
|
75
|
+
**`src/utils/errors.ts:22-26`**
|
|
76
|
+
|
|
77
|
+
catch (err) {
|
|
78
|
+
throw new AppError(`Failed to ${operation}`, { cause: err });
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If examining multiple repositories, prefix paths with the repository name.
|
|
83
|
+
|
|
84
|
+
- **Explain the why** - Don't just describe what code does; explain why it exists and how it fits
|
|
85
|
+
into the larger picture.
|
|
86
|
+
- **Compare implementations** - When examining multiple repositories, highlight differences in
|
|
87
|
+
approach. Tables work well for summarizing tradeoffs.
|
|
88
|
+
- **Use history** - When relevant, use git log/blame/show to understand how code evolved.
|
|
89
|
+
- **Admit uncertainty** - If you're unsure about something, say so and explain what you did find.
|
package/src/commands/ask.ts
CHANGED
|
@@ -2,10 +2,18 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { argument, choice, constant, type InferValue, message, object, option, string } from '@optique/core';
|
|
5
|
-
import { optional, withDefault } from '@optique/core/modifiers';
|
|
5
|
+
import { multiple, optional, withDefault } from '@optique/core/modifiers';
|
|
6
6
|
|
|
7
7
|
import { ensureRepo } from '../lib/git.ts';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
cleanupSessionDir,
|
|
10
|
+
createSessionDir,
|
|
11
|
+
getRepoCachePath,
|
|
12
|
+
normalizeRemote,
|
|
13
|
+
parseRemote,
|
|
14
|
+
parseRemoteWithBranch,
|
|
15
|
+
} from '../lib/paths.ts';
|
|
16
|
+
import { buildSymlinkDir, type RepoEntry } from '../lib/symlink.ts';
|
|
9
17
|
|
|
10
18
|
// resolve asset paths relative to the bundle (dist/index.mjs -> dist/assets/)
|
|
11
19
|
const assetsDir = join(import.meta.dirname, 'assets');
|
|
@@ -20,13 +28,22 @@ export const schema = object({
|
|
|
20
28
|
}),
|
|
21
29
|
'haiku',
|
|
22
30
|
),
|
|
31
|
+
// TODO: deprecated in favor of #branch syntax, remove in future version
|
|
23
32
|
branch: optional(
|
|
24
33
|
option('-b', '--branch', string(), {
|
|
25
|
-
description: message`branch to checkout`,
|
|
34
|
+
description: message`branch to checkout (deprecated: use repo#branch instead)`,
|
|
26
35
|
}),
|
|
27
36
|
),
|
|
37
|
+
with: withDefault(
|
|
38
|
+
multiple(
|
|
39
|
+
option('-w', '--with', string(), {
|
|
40
|
+
description: message`additional repository to include`,
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
[],
|
|
44
|
+
),
|
|
28
45
|
remote: argument(string({ metavar: 'REPO' }), {
|
|
29
|
-
description: message`git remote URL (HTTP/HTTPS/SSH)`,
|
|
46
|
+
description: message`git remote URL (HTTP/HTTPS/SSH), optionally with #branch`,
|
|
30
47
|
}),
|
|
31
48
|
question: argument(string({ metavar: 'QUESTION' }), {
|
|
32
49
|
description: message`question to ask about the repository`,
|
|
@@ -35,70 +52,183 @@ export const schema = object({
|
|
|
35
52
|
|
|
36
53
|
export type Args = InferValue<typeof schema>;
|
|
37
54
|
|
|
55
|
+
/**
|
|
56
|
+
* prints an error message for invalid remote URL and exits.
|
|
57
|
+
* @param remote the invalid remote URL
|
|
58
|
+
*/
|
|
59
|
+
const exitInvalidRemote = (remote: string): never => {
|
|
60
|
+
console.error(`error: invalid remote URL: ${remote}`);
|
|
61
|
+
console.error('expected format: host/path, e.g.:');
|
|
62
|
+
console.error(' github.com/user/repo');
|
|
63
|
+
console.error(' gitlab.com/group/subgroup/repo');
|
|
64
|
+
console.error(' https://github.com/user/repo');
|
|
65
|
+
console.error(' git@github.com:user/repo.git');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* validates and parses a remote string (with optional #branch).
|
|
71
|
+
* @param input the remote string, possibly with #branch suffix
|
|
72
|
+
* @returns parsed repo entry or exits on error
|
|
73
|
+
*/
|
|
74
|
+
const parseRepoInput = (input: string): RepoEntry => {
|
|
75
|
+
const { remote, branch } = parseRemoteWithBranch(input);
|
|
76
|
+
const parsed = parseRemote(remote);
|
|
77
|
+
if (!parsed) {
|
|
78
|
+
return exitInvalidRemote(remote);
|
|
79
|
+
}
|
|
80
|
+
const cachePath = getRepoCachePath(remote);
|
|
81
|
+
if (!cachePath) {
|
|
82
|
+
console.error(`error: could not determine cache path for: ${remote}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
return { remote, parsed, cachePath, branch };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* builds a context prompt for a single repository.
|
|
90
|
+
* @param repo the repo entry
|
|
91
|
+
* @returns context prompt string
|
|
92
|
+
*/
|
|
93
|
+
const buildSingleRepoContext = (repo: RepoEntry): string => {
|
|
94
|
+
const repoDisplay = `${repo.parsed.host}/${repo.parsed.path}`;
|
|
95
|
+
const branchDisplay = repo.branch ?? 'default branch';
|
|
96
|
+
return `You are examining ${repoDisplay} (checked out on ${branchDisplay}).`;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* builds a context prompt for multiple repositories.
|
|
101
|
+
* @param dirMap map of directory name -> repo entry
|
|
102
|
+
* @returns context prompt string
|
|
103
|
+
*/
|
|
104
|
+
const buildMultiRepoContext = (dirMap: Map<string, RepoEntry>): string => {
|
|
105
|
+
const lines = ['You are examining multiple repositories:', ''];
|
|
106
|
+
for (const [dirName, repo] of dirMap) {
|
|
107
|
+
const repoDisplay = `${repo.parsed.host}/${repo.parsed.path}`;
|
|
108
|
+
const branchDisplay = repo.branch ?? 'default branch';
|
|
109
|
+
lines.push(`- ${dirName}/ -> ${repoDisplay} (checked out on ${branchDisplay})`);
|
|
110
|
+
}
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* spawns Claude Code and waits for it to exit.
|
|
116
|
+
* @param cwd working directory
|
|
117
|
+
* @param contextPrompt context prompt for Claude
|
|
118
|
+
* @param args command arguments
|
|
119
|
+
* @returns promise that resolves with exit code
|
|
120
|
+
*/
|
|
121
|
+
const spawnClaude = (cwd: string, contextPrompt: string, args: Args): Promise<number> =>
|
|
122
|
+
new Promise((resolve, reject) => {
|
|
123
|
+
const claudeArgs = [
|
|
124
|
+
'-p',
|
|
125
|
+
args.question,
|
|
126
|
+
'--model',
|
|
127
|
+
args.model,
|
|
128
|
+
'--settings',
|
|
129
|
+
settingsPath,
|
|
130
|
+
'--system-prompt-file',
|
|
131
|
+
systemPromptPath,
|
|
132
|
+
'--append-system-prompt',
|
|
133
|
+
contextPrompt,
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
console.error('spawning claude...');
|
|
137
|
+
const claude = spawn('claude', claudeArgs, {
|
|
138
|
+
cwd,
|
|
139
|
+
stdio: 'inherit',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
claude.on('close', (code) => {
|
|
143
|
+
resolve(code ?? 0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
claude.on('error', (err) => {
|
|
147
|
+
reject(new Error(`failed to spawn claude: ${err}`));
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
38
151
|
/**
|
|
39
152
|
* handles the ask command.
|
|
40
153
|
* clones/updates the repository and spawns Claude Code to answer the question.
|
|
41
154
|
* @param args parsed command arguments
|
|
42
155
|
*/
|
|
43
156
|
export const handler = async (args: Args): Promise<void> => {
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
console.error(' https://github.com/user/repo');
|
|
51
|
-
console.error(' git@github.com:user/repo.git');
|
|
52
|
-
process.exit(1);
|
|
157
|
+
// parse main remote (with optional #branch)
|
|
158
|
+
const mainRepo = parseRepoInput(args.remote);
|
|
159
|
+
|
|
160
|
+
// #branch takes precedence over -b flag
|
|
161
|
+
if (!mainRepo.branch && args.branch) {
|
|
162
|
+
mainRepo.branch = args.branch;
|
|
53
163
|
}
|
|
54
164
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
165
|
+
// #region single repo mode
|
|
166
|
+
if (args.with.length === 0) {
|
|
167
|
+
// clone or update repository
|
|
168
|
+
const remoteUrl = normalizeRemote(mainRepo.remote);
|
|
169
|
+
console.error(`preparing repository: ${mainRepo.parsed.host}/${mainRepo.parsed.path}`);
|
|
170
|
+
try {
|
|
171
|
+
await ensureRepo(remoteUrl, mainRepo.cachePath, mainRepo.branch);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(`error: failed to prepare repository: ${err}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const contextPrompt = buildSingleRepoContext(mainRepo);
|
|
178
|
+
const exitCode = await spawnClaude(mainRepo.cachePath, contextPrompt, args);
|
|
179
|
+
process.exit(exitCode);
|
|
60
180
|
}
|
|
181
|
+
// #endregion
|
|
61
182
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
183
|
+
// #region multi repo mode
|
|
184
|
+
// parse all -w remotes
|
|
185
|
+
const additionalRepos = args.with.map(parseRepoInput);
|
|
186
|
+
const allRepos = [mainRepo, ...additionalRepos];
|
|
187
|
+
|
|
188
|
+
// clone/update all repos in parallel
|
|
189
|
+
console.error('preparing repositories...');
|
|
190
|
+
const prepareResults = await Promise.allSettled(
|
|
191
|
+
allRepos.map(async (repo) => {
|
|
192
|
+
const remoteUrl = normalizeRemote(repo.remote);
|
|
193
|
+
const display = `${repo.parsed.host}/${repo.parsed.path}`;
|
|
194
|
+
console.error(` preparing: ${display}`);
|
|
195
|
+
await ensureRepo(remoteUrl, repo.cachePath, repo.branch);
|
|
196
|
+
return repo;
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// check for failures
|
|
201
|
+
const failures: string[] = [];
|
|
202
|
+
for (let i = 0; i < prepareResults.length; i++) {
|
|
203
|
+
const result = prepareResults[i]!;
|
|
204
|
+
if (result.status === 'rejected') {
|
|
205
|
+
const repo = allRepos[i]!;
|
|
206
|
+
const display = `${repo.parsed.host}/${repo.parsed.path}`;
|
|
207
|
+
failures.push(` ${display}: ${result.reason}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (failures.length > 0) {
|
|
212
|
+
console.error('error: failed to prepare repositories:');
|
|
213
|
+
for (const failure of failures) {
|
|
214
|
+
console.error(failure);
|
|
215
|
+
}
|
|
69
216
|
process.exit(1);
|
|
70
217
|
}
|
|
71
218
|
|
|
72
|
-
//
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const contextPrompt = `You are examining ${repoDisplay} (checked out on ${branchDisplay}).`;
|
|
76
|
-
|
|
77
|
-
// spawn Claude Code
|
|
78
|
-
const claudeArgs = [
|
|
79
|
-
'-p',
|
|
80
|
-
args.question,
|
|
81
|
-
'--model',
|
|
82
|
-
args.model,
|
|
83
|
-
'--settings',
|
|
84
|
-
settingsPath,
|
|
85
|
-
'--system-prompt-file',
|
|
86
|
-
systemPromptPath,
|
|
87
|
-
'--append-system-prompt',
|
|
88
|
-
contextPrompt,
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
const claude = spawn('claude', claudeArgs, {
|
|
92
|
-
cwd: cachePath,
|
|
93
|
-
stdio: 'inherit',
|
|
94
|
-
});
|
|
219
|
+
// create session directory and symlinks
|
|
220
|
+
const sessionPath = await createSessionDir();
|
|
221
|
+
let exitCode = 1;
|
|
95
222
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
223
|
+
try {
|
|
224
|
+
const dirMap = await buildSymlinkDir(sessionPath, allRepos);
|
|
225
|
+
const contextPrompt = buildMultiRepoContext(dirMap);
|
|
226
|
+
exitCode = await spawnClaude(sessionPath, contextPrompt, args);
|
|
227
|
+
} finally {
|
|
228
|
+
// always clean up session directory
|
|
229
|
+
await cleanupSessionDir(sessionPath);
|
|
230
|
+
}
|
|
99
231
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
process.exit(1);
|
|
103
|
-
});
|
|
232
|
+
process.exit(exitCode);
|
|
233
|
+
// #endregion
|
|
104
234
|
};
|