@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.
@@ -1,5 +1,5 @@
1
- You are a code research assistant analyzing an external repository. Your goal is to answer the
2
- user's question accurately and thoroughly by exploring the codebase.
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
- ## Approach
29
+ ## Guidelines
30
30
 
31
- 1. **Explore before answering** - Don't guess. Use Glob and Grep to find relevant files, then Read
32
- to understand them.
33
- 2. **Trace the code** - Follow imports, function calls, and data flow to build a complete picture.
34
- 3. **Check history when relevant** - Use git log/blame/show to understand why code exists or how it
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
- ## Response style
36
+ ```
37
+ The cache is invalidated whenever a user updates their profile. [^1]
42
38
 
43
- - Be thorough but focused on the question asked
44
- - Include code snippets when they help explain concepts
45
- - Explain the "why" not just the "what"
46
- - If you're uncertain about something, say so and explain what you did find
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.
@@ -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 { getRepoCachePath, normalizeRemote, parseRemote } from '../lib/paths.ts';
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
- // validate remote URL
45
- const parsed = parseRemote(args.remote);
46
- if (!parsed) {
47
- console.error(`error: invalid remote URL: ${args.remote}`);
48
- console.error('expected format: host/owner/repo, e.g.:');
49
- console.error(' github.com/user/repo');
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
- // get cache path
56
- const cachePath = getRepoCachePath(args.remote);
57
- if (!cachePath) {
58
- console.error(`error: could not determine cache path for: ${args.remote}`);
59
- process.exit(1);
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
- // clone or update repository
63
- const remoteUrl = normalizeRemote(args.remote);
64
- console.error(`preparing repository: ${parsed.host}/${parsed.owner}/${parsed.repo}`);
65
- try {
66
- await ensureRepo(remoteUrl, cachePath, args.branch);
67
- } catch (err) {
68
- console.error(`error: failed to prepare repository: ${err}`);
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
- // build context for append prompt
73
- const repoDisplay = `${parsed.host}/${parsed.owner}/${parsed.repo}`;
74
- const branchDisplay = args.branch ?? 'default branch';
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
- claude.on('close', (code) => {
97
- process.exit(code ?? 0);
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
- claude.on('error', (err) => {
101
- console.error(`error: failed to spawn claude: ${err}`);
102
- process.exit(1);
103
- });
232
+ process.exit(exitCode);
233
+ // #endregion
104
234
  };