groundcrew-cli 0.17.0 → 0.18.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 (3) hide show
  1. package/dist/index.js +73 -45
  2. package/package.json +1 -1
  3. package/src/index.ts +94 -58
package/dist/index.js CHANGED
@@ -80,14 +80,18 @@ var ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_HOME, "active-sessions.json");
80
80
  var HISTORY_FILE = path.join(GROUNDCREW_HOME, "history.json");
81
81
  var REPO_NAME = "";
82
82
  async function resolveRoot() {
83
- let root = null;
83
+ let repoRoot = null;
84
84
  try {
85
- const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
86
- root = stdout.trim() || null;
85
+ const { stdout: gitCommonDir } = await execFileAsync("git", ["rev-parse", "--git-common-dir"]);
86
+ const trimmed = gitCommonDir.trim();
87
+ if (trimmed) {
88
+ const absGitDir = path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
89
+ repoRoot = path.dirname(absGitDir);
90
+ }
87
91
  } catch {
88
92
  }
89
- if (!root) root = process.cwd();
90
- REPO_NAME = path.basename(root).replace(/[^a-zA-Z0-9_-]/g, "_") || "unknown";
93
+ if (!repoRoot) repoRoot = process.cwd();
94
+ REPO_NAME = path.basename(repoRoot).replace(/[^a-zA-Z0-9_-]/g, "_") || "unknown";
91
95
  await fs.mkdir(SESSIONS_DIR, { recursive: true });
92
96
  }
93
97
  async function readActiveSessions() {
@@ -103,45 +107,54 @@ function isRepoSession(sessionId) {
103
107
  async function resolveSessionDir(explicitSession) {
104
108
  if (explicitSession) {
105
109
  const dir = path.join(SESSIONS_DIR, explicitSession);
106
- if (!existsSync(dir)) {
107
- throw new Error(`Session "${explicitSession}" not found.`);
108
- }
109
- return dir;
110
- }
111
- const sessions2 = await readActiveSessions();
112
- const ids = Object.keys(sessions2).filter(isRepoSession);
113
- if (ids.length === 0) {
110
+ if (existsSync(dir)) return dir;
114
111
  try {
115
- const dirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession);
116
- if (dirs.length === 1) return path.join(SESSIONS_DIR, dirs[0]);
117
- if (dirs.length > 1) {
118
- let latest2 = { dir: dirs[0], mtime: 0 };
119
- for (const d of dirs) {
120
- try {
121
- const stat = await fs.stat(path.join(SESSIONS_DIR, d, "session.json"));
122
- if (stat.mtimeMs > latest2.mtime) {
123
- latest2 = { dir: d, mtime: stat.mtimeMs };
124
- }
125
- } catch {
126
- }
127
- }
128
- return path.join(SESSIONS_DIR, latest2.dir);
129
- }
112
+ const allDirs = await fs.readdir(SESSIONS_DIR);
113
+ const match = allDirs.find((d) => d.endsWith("-" + explicitSession));
114
+ if (match) return path.join(SESSIONS_DIR, match);
130
115
  } catch {
131
116
  }
132
- throw new Error(`No active sessions for repo "${REPO_NAME}". Start Copilot with groundcrew first.`);
117
+ throw new Error(`Session "${explicitSession}" not found.`);
118
+ }
119
+ const active = await readActiveSessions();
120
+ const repoIds = Object.keys(active).filter(isRepoSession);
121
+ if (repoIds.length === 1) return path.join(SESSIONS_DIR, repoIds[0]);
122
+ if (repoIds.length > 1) {
123
+ let latest = { id: repoIds[0], time: 0 };
124
+ for (const id of repoIds) {
125
+ const started = new Date(active[id].started).getTime();
126
+ if (started > latest.time) latest = { id, time: started };
127
+ }
128
+ return path.join(SESSIONS_DIR, latest.id);
133
129
  }
134
- if (ids.length === 1) {
135
- return path.join(SESSIONS_DIR, ids[0]);
130
+ const allIds = Object.keys(active);
131
+ if (allIds.length === 1) return path.join(SESSIONS_DIR, allIds[0]);
132
+ if (allIds.length > 1) {
133
+ let latest = { id: allIds[0], time: 0 };
134
+ for (const id of allIds) {
135
+ const started = new Date(active[id].started).getTime();
136
+ if (started > latest.time) latest = { id, time: started };
137
+ }
138
+ return path.join(SESSIONS_DIR, latest.id);
136
139
  }
137
- let latest = { id: ids[0], time: 0 };
138
- for (const id of ids) {
139
- const started = new Date(sessions2[id].started).getTime();
140
- if (started > latest.time) {
141
- latest = { id, time: started };
140
+ try {
141
+ const dirs = await fs.readdir(SESSIONS_DIR);
142
+ const repoDirs = dirs.filter(isRepoSession);
143
+ const target = repoDirs.length > 0 ? repoDirs : dirs;
144
+ if (target.length >= 1) {
145
+ let latest = { dir: target[0], mtime: 0 };
146
+ for (const d of target) {
147
+ try {
148
+ const stat = await fs.stat(path.join(SESSIONS_DIR, d, "session.json"));
149
+ if (stat.mtimeMs > latest.mtime) latest = { dir: d, mtime: stat.mtimeMs };
150
+ } catch {
151
+ }
152
+ }
153
+ return path.join(SESSIONS_DIR, latest.dir);
142
154
  }
155
+ } catch {
143
156
  }
144
- return path.join(SESSIONS_DIR, latest.id);
157
+ throw new Error("No active sessions. Start Copilot with groundcrew first.\n Run 'groundcrew sessions' to see available sessions.");
145
158
  }
146
159
  function sessionQueueFile(sessionDir) {
147
160
  return path.join(sessionDir, "queue.json");
@@ -282,7 +295,7 @@ async function history(_sessionDir) {
282
295
  console.log();
283
296
  }
284
297
  }
285
- async function sessions() {
298
+ async function sessions(filterRepo, filterStatus) {
286
299
  const active = await readActiveSessions();
287
300
  const ids = Object.keys(active);
288
301
  let allDirs = [];
@@ -298,19 +311,26 @@ async function sessions() {
298
311
  for (const dir of allDirs) {
299
312
  const dashIdx = dir.lastIndexOf("-");
300
313
  const repo = dashIdx > 0 ? dir.substring(0, dashIdx) : "unknown";
314
+ if (filterRepo && repo !== filterRepo) continue;
301
315
  if (!byRepo.has(repo)) byRepo.set(repo, []);
302
316
  byRepo.get(repo).push(dir);
303
317
  }
318
+ if (byRepo.size === 0) {
319
+ console.log(dim(filterRepo ? `No sessions found for repo "${filterRepo}".` : "No sessions found."));
320
+ return;
321
+ }
304
322
  console.log(bold("Sessions:\n"));
305
323
  for (const [repo, dirs] of byRepo) {
306
324
  const isCurrent = repo === REPO_NAME;
307
- console.log(` ${isCurrent ? green(repo) : dim(repo)}`);
325
+ const repoEntries = [];
308
326
  for (const dir of dirs) {
309
327
  const isActive = ids.includes(dir);
310
328
  const sessionDir = path.join(SESSIONS_DIR, dir);
329
+ let sessionStatus = "unknown";
311
330
  let info = "";
312
331
  try {
313
332
  const session = JSON.parse(await fs.readFile(path.join(sessionDir, "session.json"), "utf-8"));
333
+ sessionStatus = session.status || "unknown";
314
334
  const startTime = new Date(session.started).getTime();
315
335
  const minutes = Math.round((Date.now() - startTime) / 6e4);
316
336
  const statusColor = session.status === "active" ? green : session.status === "parked" ? yellow : dim;
@@ -318,10 +338,15 @@ async function sessions() {
318
338
  } catch {
319
339
  info = dim("no session data");
320
340
  }
341
+ if (filterStatus && sessionStatus !== filterStatus) continue;
321
342
  const queue = await readQueue(sessionDir);
322
343
  const marker = isActive ? green("*") : " ";
323
344
  const shortId = dir.substring(repo.length + 1);
324
- console.log(` ${marker} ${cyan(shortId)} ${info} | ${queue.tasks.length} queued`);
345
+ repoEntries.push(` ${marker} ${cyan(shortId)} ${info} | ${queue.tasks.length} queued`);
346
+ }
347
+ if (repoEntries.length > 0) {
348
+ console.log(` ${isCurrent ? green(repo) : dim(repo)}`);
349
+ for (const entry of repoEntries) console.log(entry);
325
350
  }
326
351
  }
327
352
  if (ids.length > 0) {
@@ -427,7 +452,6 @@ async function listSessionChoices() {
427
452
  const active = await readActiveSessions();
428
453
  const choices = [];
429
454
  for (const [id, entry] of Object.entries(active)) {
430
- if (!isRepoSession(id)) continue;
431
455
  const dir = path.join(SESSIONS_DIR, id);
432
456
  let status2 = "active";
433
457
  let minutes = 0;
@@ -452,7 +476,7 @@ async function listSessionChoices() {
452
476
  async function pickSession(rl) {
453
477
  const choices = await listSessionChoices();
454
478
  if (choices.length === 0) {
455
- console.log(red(`No active sessions for repo "${REPO_NAME}". Start Copilot with groundcrew first.`));
479
+ console.log(red("No active sessions. Start Copilot with groundcrew first."));
456
480
  return null;
457
481
  }
458
482
  if (choices.length === 1) {
@@ -1117,7 +1141,9 @@ ${bold("Usage:")}
1117
1141
  groundcrew feedback --session <id> <message> Send feedback to a specific session
1118
1142
  groundcrew queue List pending tasks
1119
1143
  groundcrew status Show session status and last update
1120
- groundcrew sessions List all sessions
1144
+ groundcrew sessions List all sessions (all repos)
1145
+ groundcrew sessions --repo mekari_credit Filter by repo
1146
+ groundcrew sessions --status active Filter by status (active/parked/ended)
1121
1147
  groundcrew history Show completed tasks
1122
1148
  groundcrew clear Clear all pending tasks
1123
1149
  groundcrew stop Stop all sessions for current repo
@@ -1151,14 +1177,16 @@ function extractFlag(args, flag) {
1151
1177
  async function main() {
1152
1178
  await resolveRoot();
1153
1179
  const rawArgs = process.argv.slice(2);
1154
- const { value: explicitSession, remaining: args } = extractFlag(rawArgs, "--session");
1180
+ const { value: explicitSession, remaining: args1 } = extractFlag(rawArgs, "--session");
1181
+ const { value: filterRepo, remaining: args2 } = extractFlag(args1, "--repo");
1182
+ const { value: filterStatus, remaining: args } = extractFlag(args2, "--status");
1155
1183
  const command = args[0];
1156
1184
  switch (command) {
1157
1185
  case "chat":
1158
1186
  await chat(explicitSession);
1159
1187
  return;
1160
1188
  case "sessions":
1161
- await sessions();
1189
+ await sessions(filterRepo, filterStatus);
1162
1190
  return;
1163
1191
  case "history":
1164
1192
  await history();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundcrew-cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "CLI companion for Groundcrew — queue tasks, send feedback, monitor your Copilot agent from another terminal.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -78,21 +78,26 @@ const HISTORY_FILE = path.join(GROUNDCREW_HOME, "history.json");
78
78
  let REPO_NAME = "";
79
79
 
80
80
  /**
81
- * Derive repo name from CWD (git-aware for worktree/subdirectory support).
81
+ * Derive repo name from CWD. Uses git --git-common-dir to resolve through
82
+ * worktrees to the main repo root, so all worktrees share one session namespace.
82
83
  */
83
84
  async function resolveRoot(): Promise<void> {
84
- let root: string | null = null;
85
+ let repoRoot: string | null = null;
85
86
 
86
- // 1. Try git rev-parse --show-toplevel (worktree-aware)
87
+ // 1. Try git --git-common-dir (resolves worktrees to main repo)
87
88
  try {
88
- const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
89
- root = stdout.trim() || null;
89
+ const { stdout: gitCommonDir } = await execFileAsync("git", ["rev-parse", "--git-common-dir"]);
90
+ const trimmed = gitCommonDir.trim();
91
+ if (trimmed) {
92
+ const absGitDir = path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
93
+ repoRoot = path.dirname(absGitDir);
94
+ }
90
95
  } catch { /* not a git repo or git not installed */ }
91
96
 
92
97
  // 2. Fallback to CWD
93
- if (!root) root = process.cwd();
98
+ if (!repoRoot) repoRoot = process.cwd();
94
99
 
95
- REPO_NAME = path.basename(root).replace(/[^a-zA-Z0-9_-]/g, "_") || "unknown";
100
+ REPO_NAME = path.basename(repoRoot).replace(/[^a-zA-Z0-9_-]/g, "_") || "unknown";
96
101
 
97
102
  // Ensure centralized dirs exist
98
103
  await fs.mkdir(SESSIONS_DIR, { recursive: true });
@@ -142,56 +147,68 @@ function isRepoSession(sessionId: string): boolean {
142
147
 
143
148
  /**
144
149
  * Resolve which session to target.
145
- * Priority: --session flag > single active repo session > error if ambiguous.
150
+ * Priority: --session flag > repo match > any active session > error.
146
151
  */
147
152
  async function resolveSessionDir(explicitSession?: string): Promise<string> {
148
153
  if (explicitSession) {
154
+ // Allow short hex suffix — find the full session ID
149
155
  const dir = path.join(SESSIONS_DIR, explicitSession);
150
- if (!existsSync(dir)) {
151
- throw new Error(`Session "${explicitSession}" not found.`);
152
- }
153
- return dir;
154
- }
156
+ if (existsSync(dir)) return dir;
155
157
 
156
- const sessions = await readActiveSessions();
157
- // Filter to sessions for THIS repo
158
- const ids = Object.keys(sessions).filter(isRepoSession);
159
-
160
- if (ids.length === 0) {
161
- // Fallback: check session dirs on disk for this repo
158
+ // Try matching as suffix
162
159
  try {
163
- const dirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession);
164
- if (dirs.length === 1) return path.join(SESSIONS_DIR, dirs[0]);
165
- if (dirs.length > 1) {
166
- // Pick most recently modified
167
- let latest = { dir: dirs[0], mtime: 0 };
168
- for (const d of dirs) {
169
- try {
170
- const stat = await fs.stat(path.join(SESSIONS_DIR, d, "session.json"));
171
- if (stat.mtimeMs > latest.mtime) {
172
- latest = { dir: d, mtime: stat.mtimeMs };
173
- }
174
- } catch { /* skip */ }
175
- }
176
- return path.join(SESSIONS_DIR, latest.dir);
177
- }
178
- } catch { /* no sessions dir */ }
179
- throw new Error(`No active sessions for repo "${REPO_NAME}". Start Copilot with groundcrew first.`);
160
+ const allDirs = await fs.readdir(SESSIONS_DIR);
161
+ const match = allDirs.find((d) => d.endsWith("-" + explicitSession));
162
+ if (match) return path.join(SESSIONS_DIR, match);
163
+ } catch {}
164
+
165
+ throw new Error(`Session "${explicitSession}" not found.`);
180
166
  }
181
167
 
182
- if (ids.length === 1) {
183
- return path.join(SESSIONS_DIR, ids[0]);
168
+ const active = await readActiveSessions();
169
+
170
+ // 1. Try sessions for current repo first
171
+ const repoIds = Object.keys(active).filter(isRepoSession);
172
+ if (repoIds.length === 1) return path.join(SESSIONS_DIR, repoIds[0]);
173
+ if (repoIds.length > 1) {
174
+ let latest = { id: repoIds[0], time: 0 };
175
+ for (const id of repoIds) {
176
+ const started = new Date(active[id].started).getTime();
177
+ if (started > latest.time) latest = { id, time: started };
178
+ }
179
+ return path.join(SESSIONS_DIR, latest.id);
184
180
  }
185
181
 
186
- // Multiple sessions pick latest
187
- let latest = { id: ids[0], time: 0 };
188
- for (const id of ids) {
189
- const started = new Date(sessions[id].started).getTime();
190
- if (started > latest.time) {
191
- latest = { id, time: started };
182
+ // 2. Fall back to ANY active session
183
+ const allIds = Object.keys(active);
184
+ if (allIds.length === 1) return path.join(SESSIONS_DIR, allIds[0]);
185
+ if (allIds.length > 1) {
186
+ let latest = { id: allIds[0], time: 0 };
187
+ for (const id of allIds) {
188
+ const started = new Date(active[id].started).getTime();
189
+ if (started > latest.time) latest = { id, time: started };
192
190
  }
191
+ return path.join(SESSIONS_DIR, latest.id);
193
192
  }
194
- return path.join(SESSIONS_DIR, latest.id);
193
+
194
+ // 3. Check dirs on disk
195
+ try {
196
+ const dirs = await fs.readdir(SESSIONS_DIR);
197
+ const repoDirs = dirs.filter(isRepoSession);
198
+ const target = repoDirs.length > 0 ? repoDirs : dirs;
199
+ if (target.length >= 1) {
200
+ let latest = { dir: target[0], mtime: 0 };
201
+ for (const d of target) {
202
+ try {
203
+ const stat = await fs.stat(path.join(SESSIONS_DIR, d, "session.json"));
204
+ if (stat.mtimeMs > latest.mtime) latest = { dir: d, mtime: stat.mtimeMs };
205
+ } catch {}
206
+ }
207
+ return path.join(SESSIONS_DIR, latest.dir);
208
+ }
209
+ } catch {}
210
+
211
+ throw new Error("No active sessions. Start Copilot with groundcrew first.\n Run 'groundcrew sessions' to see available sessions.");
195
212
  }
196
213
 
197
214
  function sessionQueueFile(sessionDir: string): string {
@@ -371,11 +388,11 @@ async function history(_sessionDir?: string): Promise<void> {
371
388
  }
372
389
  }
373
390
 
374
- async function sessions(): Promise<void> {
391
+ async function sessions(filterRepo?: string, filterStatus?: string): Promise<void> {
375
392
  const active = await readActiveSessions();
376
393
  const ids = Object.keys(active);
377
394
 
378
- // Also check for session dirs that may not be in active-sessions.json
395
+ // All session dirs on disk
379
396
  let allDirs: string[] = [];
380
397
  try {
381
398
  allDirs = await fs.readdir(SESSIONS_DIR);
@@ -391,23 +408,34 @@ async function sessions(): Promise<void> {
391
408
  for (const dir of allDirs) {
392
409
  const dashIdx = dir.lastIndexOf("-");
393
410
  const repo = dashIdx > 0 ? dir.substring(0, dashIdx) : "unknown";
411
+
412
+ // Apply --repo filter
413
+ if (filterRepo && repo !== filterRepo) continue;
414
+
394
415
  if (!byRepo.has(repo)) byRepo.set(repo, []);
395
416
  byRepo.get(repo)!.push(dir);
396
417
  }
397
418
 
419
+ if (byRepo.size === 0) {
420
+ console.log(dim(filterRepo ? `No sessions found for repo "${filterRepo}".` : "No sessions found."));
421
+ return;
422
+ }
423
+
398
424
  console.log(bold("Sessions:\n"));
399
425
 
400
426
  for (const [repo, dirs] of byRepo) {
401
427
  const isCurrent = repo === REPO_NAME;
402
- console.log(` ${isCurrent ? green(repo) : dim(repo)}`);
428
+ const repoEntries: string[] = [];
403
429
 
404
430
  for (const dir of dirs) {
405
431
  const isActive = ids.includes(dir);
406
432
  const sessionDir = path.join(SESSIONS_DIR, dir);
407
433
 
434
+ let sessionStatus = "unknown";
408
435
  let info = "";
409
436
  try {
410
437
  const session = JSON.parse(await fs.readFile(path.join(sessionDir, "session.json"), "utf-8"));
438
+ sessionStatus = session.status || "unknown";
411
439
  const startTime = new Date(session.started).getTime();
412
440
  const minutes = Math.round((Date.now() - startTime) / 60000);
413
441
  const statusColor = session.status === "active" ? green : session.status === "parked" ? yellow : dim;
@@ -416,12 +444,19 @@ async function sessions(): Promise<void> {
416
444
  info = dim("no session data");
417
445
  }
418
446
 
447
+ // Apply --status filter
448
+ if (filterStatus && sessionStatus !== filterStatus) continue;
449
+
419
450
  const queue = await readQueue(sessionDir);
420
451
  const marker = isActive ? green("*") : " ";
421
- // Show only the hex suffix for cleaner display
422
452
  const shortId = dir.substring(repo.length + 1);
423
453
 
424
- console.log(` ${marker} ${cyan(shortId)} ${info} | ${queue.tasks.length} queued`);
454
+ repoEntries.push(` ${marker} ${cyan(shortId)} ${info} | ${queue.tasks.length} queued`);
455
+ }
456
+
457
+ if (repoEntries.length > 0) {
458
+ console.log(` ${isCurrent ? green(repo) : dim(repo)}`);
459
+ for (const entry of repoEntries) console.log(entry);
425
460
  }
426
461
  }
427
462
 
@@ -559,9 +594,6 @@ async function listSessionChoices(): Promise<SessionChoice[]> {
559
594
  const choices: SessionChoice[] = [];
560
595
 
561
596
  for (const [id, entry] of Object.entries(active)) {
562
- // Only show sessions for the current repo
563
- if (!isRepoSession(id)) continue;
564
-
565
597
  const dir = path.join(SESSIONS_DIR, id);
566
598
  let status = "active";
567
599
  let minutes = 0;
@@ -590,7 +622,7 @@ async function pickSession(rl: readline.Interface): Promise<SessionChoice | null
590
622
  const choices = await listSessionChoices();
591
623
 
592
624
  if (choices.length === 0) {
593
- console.log(red(`No active sessions for repo "${REPO_NAME}". Start Copilot with groundcrew first.`));
625
+ console.log(red("No active sessions. Start Copilot with groundcrew first."));
594
626
  return null;
595
627
  }
596
628
 
@@ -1253,7 +1285,9 @@ ${bold("Usage:")}
1253
1285
  groundcrew feedback --session <id> <message> Send feedback to a specific session
1254
1286
  groundcrew queue List pending tasks
1255
1287
  groundcrew status Show session status and last update
1256
- groundcrew sessions List all sessions
1288
+ groundcrew sessions List all sessions (all repos)
1289
+ groundcrew sessions --repo mekari_credit Filter by repo
1290
+ groundcrew sessions --status active Filter by status (active/parked/ended)
1257
1291
  groundcrew history Show completed tasks
1258
1292
  groundcrew clear Clear all pending tasks
1259
1293
  groundcrew stop Stop all sessions for current repo
@@ -1292,8 +1326,10 @@ async function main(): Promise<void> {
1292
1326
 
1293
1327
  const rawArgs = process.argv.slice(2);
1294
1328
 
1295
- // Extract --session flag from anywhere in args
1296
- const { value: explicitSession, remaining: args } = extractFlag(rawArgs, "--session");
1329
+ // Extract flags from anywhere in args
1330
+ const { value: explicitSession, remaining: args1 } = extractFlag(rawArgs, "--session");
1331
+ const { value: filterRepo, remaining: args2 } = extractFlag(args1, "--repo");
1332
+ const { value: filterStatus, remaining: args } = extractFlag(args2, "--status");
1297
1333
 
1298
1334
  const command = args[0];
1299
1335
 
@@ -1303,7 +1339,7 @@ async function main(): Promise<void> {
1303
1339
  await chat(explicitSession);
1304
1340
  return;
1305
1341
  case "sessions":
1306
- await sessions();
1342
+ await sessions(filterRepo, filterStatus);
1307
1343
  return;
1308
1344
  case "history":
1309
1345
  await history();