code-squad-cli 1.0.8 → 1.1.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.
@@ -0,0 +1 @@
1
+ export declare function runFlip(args: string[]): Promise<void>;
@@ -0,0 +1,246 @@
1
+ import { Server, findFreePort } from './server/Server.js';
2
+ import open from 'open';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import os from 'os';
6
+ import { execSync } from 'child_process';
7
+ import { confirm } from '@inquirer/prompts';
8
+ import clipboardy from 'clipboardy';
9
+ const DEFAULT_PORT = 51234;
10
+ function formatTime() {
11
+ const now = new Date();
12
+ const hours = String(now.getHours()).padStart(2, '0');
13
+ const mins = String(now.getMinutes()).padStart(2, '0');
14
+ const secs = String(now.getSeconds()).padStart(2, '0');
15
+ return `${hours}:${mins}:${secs}`;
16
+ }
17
+ export async function runFlip(args) {
18
+ // Parse --session flag from anywhere in args
19
+ let sessionId;
20
+ const sessionIdx = args.indexOf('--session');
21
+ if (sessionIdx !== -1 && args[sessionIdx + 1]) {
22
+ sessionId = args[sessionIdx + 1];
23
+ }
24
+ // Filter out --session and its value for further parsing
25
+ const filteredArgs = args.filter((arg, idx) => {
26
+ if (arg === '--session')
27
+ return false;
28
+ if (idx > 0 && args[idx - 1] === '--session')
29
+ return false;
30
+ return true;
31
+ });
32
+ // Parse subcommand
33
+ let command;
34
+ let pathArg;
35
+ if (filteredArgs.length > 0) {
36
+ switch (filteredArgs[0]) {
37
+ case 'serve':
38
+ command = 'serve';
39
+ pathArg = filteredArgs[1];
40
+ break;
41
+ case 'open':
42
+ command = 'open';
43
+ break;
44
+ case 'setup':
45
+ command = 'setup';
46
+ break;
47
+ case '--help':
48
+ case '-h':
49
+ printUsage();
50
+ return;
51
+ default:
52
+ command = 'oneshot';
53
+ pathArg = filteredArgs[0];
54
+ break;
55
+ }
56
+ }
57
+ else {
58
+ command = 'oneshot';
59
+ }
60
+ const cwd = pathArg ? path.resolve(pathArg) : process.cwd();
61
+ switch (command) {
62
+ case 'setup': {
63
+ await setupHotkey();
64
+ return;
65
+ }
66
+ case 'serve': {
67
+ // Daemon mode: start server, keep running
68
+ const port = await findFreePort(DEFAULT_PORT);
69
+ console.log(`Server running at http://localhost:${port}`);
70
+ console.log('Press Ctrl+C to stop');
71
+ console.log('');
72
+ console.log('To open browser, run: csq flip open');
73
+ console.log(`Or use hotkey to open: open http://localhost:${port}`);
74
+ // Run server in loop (restarts after each submit/cancel)
75
+ while (true) {
76
+ const server = new Server(cwd, port);
77
+ const result = await server.run();
78
+ if (result) {
79
+ console.log(`[${formatTime()}] Submitted ${result.length} characters`);
80
+ }
81
+ else {
82
+ console.log(`[${formatTime()}] Cancelled`);
83
+ }
84
+ console.log(`[${formatTime()}] Ready for next session...`);
85
+ }
86
+ }
87
+ case 'open': {
88
+ // Just open browser to existing server
89
+ const url = `http://localhost:${DEFAULT_PORT}`;
90
+ console.log(`Opening ${url} in browser...`);
91
+ try {
92
+ await open(url);
93
+ }
94
+ catch (e) {
95
+ console.error('Failed to open browser:', e);
96
+ console.error('Is the server running? Start with: csq flip serve');
97
+ }
98
+ break;
99
+ }
100
+ case 'oneshot':
101
+ default: {
102
+ // Original behavior: start server, open browser, exit after submit/cancel
103
+ const port = await findFreePort(DEFAULT_PORT);
104
+ const url = sessionId
105
+ ? `http://localhost:${port}?session=${sessionId}`
106
+ : `http://localhost:${port}`;
107
+ console.log(`Opening ${url} in browser...`);
108
+ try {
109
+ await open(url);
110
+ }
111
+ catch (e) {
112
+ console.error('Failed to open browser:', e);
113
+ console.log(`Please open ${url} manually`);
114
+ }
115
+ const server = new Server(cwd, port);
116
+ const result = await server.run();
117
+ if (result) {
118
+ console.log(`\nSubmitted ${result.length} characters`);
119
+ }
120
+ else {
121
+ console.log('\nCancelled');
122
+ }
123
+ break;
124
+ }
125
+ }
126
+ }
127
+ function printUsage() {
128
+ console.error('Usage: csq flip [command] [options]');
129
+ console.error('');
130
+ console.error('Commands:');
131
+ console.error(' serve [path] Start server in daemon mode (keeps running)');
132
+ console.error(' open Open browser to existing server');
133
+ console.error(' setup Setup iTerm2 hotkey');
134
+ console.error(' (no command) Start server + open browser (one-shot mode)');
135
+ console.error('');
136
+ console.error('Options:');
137
+ console.error(' path Directory to serve (default: current directory)');
138
+ console.error(' --session <uuid> Session ID for paste-back tracking');
139
+ }
140
+ async function setupHotkey() {
141
+ const configDir = path.join(os.homedir(), '.config', 'flip');
142
+ const applescriptPath = path.join(configDir, 'flip.applescript');
143
+ const shPath = path.join(configDir, 'flip.sh');
144
+ // Get node path
145
+ let nodePath;
146
+ try {
147
+ nodePath = execSync('which node', { encoding: 'utf-8' }).trim();
148
+ }
149
+ catch {
150
+ nodePath = '/usr/local/bin/node';
151
+ }
152
+ // Get csq path (this script's location)
153
+ const csqPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../index.js');
154
+ // Create config directory
155
+ fs.mkdirSync(configDir, { recursive: true });
156
+ // Write AppleScript
157
+ const applescriptContent = `#!/usr/bin/osascript
158
+
159
+ -- flip hotkey script
160
+ -- Generated by: csq flip setup
161
+
162
+ tell application "iTerm2"
163
+ tell current session of current window
164
+ set originalSessionId to id
165
+ set sessionUUID to do shell script "uuidgen"
166
+ set currentPath to variable named "path"
167
+ do shell script "echo '" & originalSessionId & "' > /tmp/flip-view-session-" & sessionUUID
168
+ do shell script "nohup ${nodePath} ${csqPath} flip --session " & sessionUUID & " " & quoted form of currentPath & " > /tmp/flip.log 2>&1 &"
169
+ end tell
170
+ end tell
171
+
172
+ return ""`;
173
+ fs.writeFileSync(applescriptPath, applescriptContent);
174
+ fs.chmodSync(applescriptPath, '755');
175
+ // Write shell wrapper
176
+ const shContent = `#!/bin/bash
177
+ osascript ${applescriptPath} > /dev/null 2>&1
178
+ `;
179
+ fs.writeFileSync(shPath, shContent);
180
+ fs.chmodSync(shPath, '755');
181
+ console.log('');
182
+ console.log('┌─────────────────────────────────────────────────┐');
183
+ console.log('│ Flip Hotkey Setup Wizard │');
184
+ console.log('└─────────────────────────────────────────────────┘');
185
+ console.log('');
186
+ console.log('✓ Scripts generated:');
187
+ console.log(` ${shPath}`);
188
+ console.log('');
189
+ // Step 1: Open iTerm2 Settings
190
+ const openSettings = await confirm({
191
+ message: 'Open iTerm2 Settings? (Keys → Key Bindings)',
192
+ default: true,
193
+ });
194
+ if (openSettings) {
195
+ try {
196
+ execSync(`osascript -e 'tell application "iTerm2" to activate' -e 'tell application "System Events" to keystroke "," using command down'`);
197
+ console.log('');
198
+ console.log(' → iTerm2 Settings opened. Navigate to: Keys → Key Bindings');
199
+ }
200
+ catch {
201
+ console.log(' → Could not open settings automatically. Open manually: iTerm2 → Settings → Keys → Key Bindings');
202
+ }
203
+ }
204
+ console.log('');
205
+ // Step 2: Guide through adding key binding
206
+ const ready = await confirm({
207
+ message: 'Ready to add Key Binding? (Click + button in Key Bindings tab)',
208
+ default: true,
209
+ });
210
+ if (!ready) {
211
+ console.log('');
212
+ console.log('Run `csq flip setup` again when ready.');
213
+ return;
214
+ }
215
+ console.log('');
216
+ console.log('┌─────────────────────────────────────────────────┐');
217
+ console.log('│ Configure the Key Binding: │');
218
+ console.log('├─────────────────────────────────────────────────┤');
219
+ console.log('│ 1. Keyboard Shortcut: Press your hotkey │');
220
+ console.log('│ (e.g., ⌘⇧F) │');
221
+ console.log('│ │');
222
+ console.log('│ 2. Action: Select "Run Coprocess" │');
223
+ console.log('│ │');
224
+ console.log('│ 3. Command: Paste the path (copied below) │');
225
+ console.log('└─────────────────────────────────────────────────┘');
226
+ console.log('');
227
+ // Copy path to clipboard
228
+ await clipboardy.write(shPath);
229
+ console.log(`✓ Copied to clipboard: ${shPath}`);
230
+ console.log('');
231
+ await confirm({
232
+ message: 'Done configuring? (Paste the path and click OK)',
233
+ default: true,
234
+ });
235
+ console.log('');
236
+ console.log('┌─────────────────────────────────────────────────┐');
237
+ console.log('│ Setup Complete! │');
238
+ console.log('├─────────────────────────────────────────────────┤');
239
+ console.log('│ Usage: │');
240
+ console.log('│ 1. Focus on any terminal panel │');
241
+ console.log('│ 2. Press your hotkey → browser opens │');
242
+ console.log('│ 3. Select files, add comments │');
243
+ console.log('│ 4. Submit → text appears in terminal │');
244
+ console.log('└─────────────────────────────────────────────────┘');
245
+ console.log('');
246
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Schedule automatic paste to the original iTerm session
3
+ * @param sessionId The UUID that identifies this flip-view session
4
+ */
5
+ export declare function schedulePaste(sessionId: string): Promise<void>;
@@ -0,0 +1,80 @@
1
+ import { execSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ /**
6
+ * Schedule automatic paste to the original iTerm session
7
+ * @param sessionId The UUID that identifies this flip-view session
8
+ */
9
+ export async function schedulePaste(sessionId) {
10
+ if (process.platform !== 'darwin') {
11
+ console.error('Auto-paste not supported on this platform. Please paste manually (Ctrl+V).');
12
+ return;
13
+ }
14
+ await pasteToOriginalSession(sessionId);
15
+ }
16
+ async function pasteToOriginalSession(sessionId) {
17
+ // Read the original iTerm session ID from session-specific temp file
18
+ const sessionFile = path.join(os.tmpdir(), `flip-view-session-${sessionId}`);
19
+ let itermSessionId;
20
+ try {
21
+ itermSessionId = fs.readFileSync(sessionFile, 'utf-8').trim();
22
+ }
23
+ catch {
24
+ // Session file not found
25
+ }
26
+ let script;
27
+ if (itermSessionId) {
28
+ // Find the original session by ID and paste to it
29
+ script = `
30
+ tell application "iTerm2"
31
+ set found to false
32
+ repeat with w in windows
33
+ repeat with t in tabs of w
34
+ repeat with s in sessions of t
35
+ if id of s is "${itermSessionId}" then
36
+ set found to true
37
+ select s
38
+ tell s to write text (the clipboard)
39
+ tell s to write text ""
40
+ return
41
+ end if
42
+ end repeat
43
+ end repeat
44
+ end repeat
45
+
46
+ if not found then
47
+ display notification "Original session closed. Output copied to clipboard." with title "flip"
48
+ end if
49
+ end tell
50
+ `;
51
+ // Cleanup the session file after use
52
+ try {
53
+ fs.unlinkSync(sessionFile);
54
+ }
55
+ catch {
56
+ // Ignore cleanup errors
57
+ }
58
+ }
59
+ else {
60
+ // Fallback: paste to current iTerm session
61
+ script = `
62
+ tell application "iTerm2"
63
+ activate
64
+ delay 0.1
65
+ tell current session of current window
66
+ write text (the clipboard)
67
+ write text ""
68
+ end tell
69
+ end tell
70
+ `;
71
+ }
72
+ try {
73
+ execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, {
74
+ stdio: 'pipe',
75
+ });
76
+ }
77
+ catch (e) {
78
+ console.error('Failed to paste to iTerm:', e);
79
+ }
80
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Copy text to system clipboard
3
+ */
4
+ export declare function copyToClipboard(text: string): Promise<void>;
@@ -0,0 +1,7 @@
1
+ import clipboardy from 'clipboardy';
2
+ /**
3
+ * Copy text to system clipboard
4
+ */
5
+ export async function copyToClipboard(text) {
6
+ await clipboardy.write(text);
7
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Minimal Comment interface compatible with @code-squad/core Comment
3
+ */
4
+ export interface CommentLike {
5
+ file: string;
6
+ line: number;
7
+ endLine?: number;
8
+ text: string;
9
+ }
10
+ /**
11
+ * Format Comment-like entities for AI agent consumption
12
+ */
13
+ export declare function formatComments(comments: CommentLike[]): string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Format Comment-like entities for AI agent consumption
3
+ */
4
+ export function formatComments(comments) {
5
+ if (comments.length === 0) {
6
+ return '';
7
+ }
8
+ let output = 'The user has annotated the following code locations:\n\n';
9
+ for (const comment of comments) {
10
+ const lineRange = comment.endLine && comment.endLine !== comment.line
11
+ ? `${comment.line}-${comment.endLine}`
12
+ : `${comment.line}`;
13
+ const location = `${comment.file}:${lineRange}`;
14
+ output += `- ${location} -> "${comment.text}"\n`;
15
+ }
16
+ output += '\nPlease review these comments and address them.';
17
+ return output;
18
+ }
@@ -0,0 +1,4 @@
1
+ export { formatComments } from './formatter.js';
2
+ export type { CommentLike } from './formatter.js';
3
+ export { copyToClipboard } from './clipboard.js';
4
+ export { schedulePaste } from './autopaste.js';
@@ -0,0 +1,3 @@
1
+ export { formatComments } from './formatter.js';
2
+ export { copyToClipboard } from './clipboard.js';
3
+ export { schedulePaste } from './autopaste.js';
@@ -0,0 +1,6 @@
1
+ import type { Router as IRouter } from 'express';
2
+ export interface CancelResponse {
3
+ status: string;
4
+ }
5
+ declare const router: IRouter;
6
+ export { router as cancelRouter };
@@ -0,0 +1,13 @@
1
+ import { Router } from 'express';
2
+ const router = Router();
3
+ // POST /api/cancel
4
+ router.post('/', (req, res) => {
5
+ const state = req.app.locals.state;
6
+ // Send response before shutdown
7
+ res.json({ status: 'ok' });
8
+ // Trigger shutdown without output
9
+ if (state.resolve) {
10
+ state.resolve(null);
11
+ }
12
+ });
13
+ export { router as cancelRouter };
@@ -0,0 +1,8 @@
1
+ import type { Router as IRouter } from 'express';
2
+ export interface FileResponse {
3
+ path: string;
4
+ content: string;
5
+ language: string;
6
+ }
7
+ declare const router: IRouter;
8
+ export { router as fileRouter };
@@ -0,0 +1,77 @@
1
+ import { Router } from 'express';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ const router = Router();
5
+ function detectLanguage(filePath) {
6
+ const ext = path.extname(filePath).slice(1).toLowerCase();
7
+ const languageMap = {
8
+ rs: 'rust',
9
+ js: 'javascript',
10
+ ts: 'typescript',
11
+ tsx: 'tsx',
12
+ jsx: 'jsx',
13
+ py: 'python',
14
+ go: 'go',
15
+ java: 'java',
16
+ c: 'c',
17
+ cpp: 'cpp',
18
+ cc: 'cpp',
19
+ cxx: 'cpp',
20
+ h: 'cpp',
21
+ hpp: 'cpp',
22
+ md: 'markdown',
23
+ json: 'json',
24
+ yaml: 'yaml',
25
+ yml: 'yaml',
26
+ toml: 'toml',
27
+ html: 'html',
28
+ css: 'css',
29
+ scss: 'scss',
30
+ sh: 'bash',
31
+ bash: 'bash',
32
+ sql: 'sql',
33
+ rb: 'ruby',
34
+ swift: 'swift',
35
+ kt: 'kotlin',
36
+ kts: 'kotlin',
37
+ xml: 'xml',
38
+ vue: 'vue',
39
+ };
40
+ return languageMap[ext] || 'plaintext';
41
+ }
42
+ // GET /api/file?path=<path>
43
+ router.get('/', (req, res) => {
44
+ const state = req.app.locals.state;
45
+ const relativePath = req.query.path;
46
+ if (!relativePath) {
47
+ res.status(400).json({ error: 'Missing path parameter' });
48
+ return;
49
+ }
50
+ const filePath = path.join(state.cwd, relativePath);
51
+ // Security check: ensure path is within cwd
52
+ const resolvedPath = path.resolve(filePath);
53
+ const resolvedCwd = path.resolve(state.cwd);
54
+ if (!resolvedPath.startsWith(resolvedCwd)) {
55
+ res.status(403).json({ error: 'Access denied' });
56
+ return;
57
+ }
58
+ try {
59
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
60
+ const language = detectLanguage(relativePath);
61
+ const response = {
62
+ path: relativePath,
63
+ content,
64
+ language,
65
+ };
66
+ res.json(response);
67
+ }
68
+ catch (err) {
69
+ if (err.code === 'ENOENT') {
70
+ res.status(404).json({ error: 'File not found' });
71
+ }
72
+ else {
73
+ res.status(500).json({ error: 'Failed to read file' });
74
+ }
75
+ }
76
+ });
77
+ export { router as fileRouter };
@@ -0,0 +1,16 @@
1
+ import type { Router as IRouter } from 'express';
2
+ export interface FileNode {
3
+ path: string;
4
+ name: string;
5
+ type: 'file' | 'directory';
6
+ children?: FileNode[];
7
+ }
8
+ export interface FilesResponse {
9
+ root: string;
10
+ tree: FileNode[];
11
+ }
12
+ export interface FlatFilesResponse {
13
+ files: string[];
14
+ }
15
+ declare const router: IRouter;
16
+ export { router as filesRouter };
@@ -0,0 +1,102 @@
1
+ import { Router } from 'express';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ const router = Router();
5
+ // Default ignore patterns (similar to .gitignore behavior)
6
+ const DEFAULT_IGNORES = [
7
+ 'node_modules',
8
+ '.git',
9
+ '.svn',
10
+ '.hg',
11
+ 'dist',
12
+ 'build',
13
+ 'out',
14
+ '.cache',
15
+ '.next',
16
+ '.nuxt',
17
+ 'coverage',
18
+ '__pycache__',
19
+ '.pytest_cache',
20
+ 'target',
21
+ 'Cargo.lock',
22
+ 'package-lock.json',
23
+ 'pnpm-lock.yaml',
24
+ 'yarn.lock',
25
+ '.DS_Store',
26
+ ];
27
+ function shouldIgnore(name) {
28
+ if (name.startsWith('.') && name !== '.gitignore' && name !== '.env.example') {
29
+ return true;
30
+ }
31
+ return DEFAULT_IGNORES.includes(name);
32
+ }
33
+ function buildFileTree(rootPath, currentPath, maxDepth, depth = 0) {
34
+ if (depth > maxDepth)
35
+ return [];
36
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
37
+ const nodes = [];
38
+ // Sort: directories first, then alphabetically
39
+ entries.sort((a, b) => {
40
+ if (a.isDirectory() && !b.isDirectory())
41
+ return -1;
42
+ if (!a.isDirectory() && b.isDirectory())
43
+ return 1;
44
+ return a.name.localeCompare(b.name);
45
+ });
46
+ for (const entry of entries) {
47
+ if (shouldIgnore(entry.name))
48
+ continue;
49
+ const fullPath = path.join(currentPath, entry.name);
50
+ const relativePath = path.relative(rootPath, fullPath);
51
+ const node = {
52
+ path: relativePath,
53
+ name: entry.name,
54
+ type: entry.isDirectory() ? 'directory' : 'file',
55
+ };
56
+ if (entry.isDirectory()) {
57
+ node.children = buildFileTree(rootPath, fullPath, maxDepth, depth + 1);
58
+ }
59
+ nodes.push(node);
60
+ }
61
+ return nodes;
62
+ }
63
+ function collectFlatFiles(rootPath, currentPath, maxDepth, depth = 0) {
64
+ if (depth > maxDepth)
65
+ return [];
66
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
67
+ const files = [];
68
+ for (const entry of entries) {
69
+ if (shouldIgnore(entry.name))
70
+ continue;
71
+ const fullPath = path.join(currentPath, entry.name);
72
+ const relativePath = path.relative(rootPath, fullPath);
73
+ if (entry.isFile()) {
74
+ files.push(relativePath);
75
+ }
76
+ else if (entry.isDirectory()) {
77
+ files.push(...collectFlatFiles(rootPath, fullPath, maxDepth, depth + 1));
78
+ }
79
+ }
80
+ return files;
81
+ }
82
+ // GET /api/files - Get file tree structure
83
+ router.get('/', (req, res) => {
84
+ const state = req.app.locals.state;
85
+ const tree = buildFileTree(state.cwd, state.cwd, 10);
86
+ const response = {
87
+ root: state.cwd,
88
+ tree,
89
+ };
90
+ res.json(response);
91
+ });
92
+ // GET /api/files/flat - Get flat list of all files
93
+ router.get('/flat', (req, res) => {
94
+ const state = req.app.locals.state;
95
+ const files = collectFlatFiles(state.cwd, state.cwd, 10);
96
+ files.sort();
97
+ const response = {
98
+ files,
99
+ };
100
+ res.json(response);
101
+ });
102
+ export { router as filesRouter };
@@ -0,0 +1,11 @@
1
+ import type { Router as IRouter } from 'express';
2
+ export interface UnstagedFile {
3
+ path: string;
4
+ status: string;
5
+ }
6
+ export interface GitStatusResponse {
7
+ isGitRepo: boolean;
8
+ unstaged: UnstagedFile[];
9
+ }
10
+ declare const router: IRouter;
11
+ export { router as gitRouter };