groundcrew-cli 0.16.6 → 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 +119 -101
  2. package/package.json +1 -1
  3. package/src/index.ts +156 -119
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import{createRequire}from'module';const require=createRequire(import.meta.url);
4
4
  import fs from "fs/promises";
5
5
  import { existsSync } from "fs";
6
6
  import path from "path";
7
+ import os from "os";
7
8
  import readline from "readline";
8
9
  import { execFile, execFileSync } from "child_process";
9
10
  import { promisify } from "util";
@@ -73,35 +74,25 @@ close access fp`], { timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] });
73
74
  return null;
74
75
  }
75
76
  }
76
- var GROUNDCREW_DIR = ".groundcrew";
77
- var SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
78
- var ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_DIR, "active-sessions.json");
79
- var HISTORY_FILE = path.join(GROUNDCREW_DIR, "history.json");
77
+ var GROUNDCREW_HOME = path.join(os.homedir(), ".groundcrew");
78
+ var SESSIONS_DIR = path.join(GROUNDCREW_HOME, "sessions");
79
+ var ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_HOME, "active-sessions.json");
80
+ var HISTORY_FILE = path.join(GROUNDCREW_HOME, "history.json");
81
+ var REPO_NAME = "";
80
82
  async function resolveRoot() {
81
- let root = null;
83
+ let repoRoot = null;
82
84
  try {
83
- const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
84
- const gitRoot = stdout.trim();
85
- if (gitRoot && existsSync(path.join(gitRoot, ".groundcrew"))) {
86
- root = gitRoot;
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);
87
90
  }
88
91
  } catch {
89
92
  }
90
- if (!root) {
91
- let dir = process.cwd();
92
- while (dir !== path.dirname(dir)) {
93
- if (existsSync(path.join(dir, ".groundcrew"))) {
94
- root = dir;
95
- break;
96
- }
97
- dir = path.dirname(dir);
98
- }
99
- }
100
- if (!root) root = process.cwd();
101
- GROUNDCREW_DIR = path.join(root, ".groundcrew");
102
- SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
103
- ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_DIR, "active-sessions.json");
104
- HISTORY_FILE = path.join(GROUNDCREW_DIR, "history.json");
93
+ if (!repoRoot) repoRoot = process.cwd();
94
+ REPO_NAME = path.basename(repoRoot).replace(/[^a-zA-Z0-9_-]/g, "_") || "unknown";
95
+ await fs.mkdir(SESSIONS_DIR, { recursive: true });
105
96
  }
106
97
  async function readActiveSessions() {
107
98
  try {
@@ -110,48 +101,60 @@ async function readActiveSessions() {
110
101
  return {};
111
102
  }
112
103
  }
104
+ function isRepoSession(sessionId) {
105
+ return sessionId.startsWith(REPO_NAME + "-");
106
+ }
113
107
  async function resolveSessionDir(explicitSession) {
114
108
  if (explicitSession) {
115
109
  const dir = path.join(SESSIONS_DIR, explicitSession);
116
- if (!existsSync(dir)) {
117
- throw new Error(`Session "${explicitSession}" not found.`);
118
- }
119
- return dir;
120
- }
121
- const sessions2 = await readActiveSessions();
122
- const ids = Object.keys(sessions2);
123
- if (ids.length === 0) {
110
+ if (existsSync(dir)) return dir;
124
111
  try {
125
- const dirs = await fs.readdir(SESSIONS_DIR);
126
- if (dirs.length === 1) return path.join(SESSIONS_DIR, dirs[0]);
127
- if (dirs.length > 1) {
128
- let latest2 = { dir: dirs[0], mtime: 0 };
129
- for (const d of dirs) {
130
- try {
131
- const stat = await fs.stat(path.join(SESSIONS_DIR, d, "session.json"));
132
- if (stat.mtimeMs > latest2.mtime) {
133
- latest2 = { dir: d, mtime: stat.mtimeMs };
134
- }
135
- } catch {
136
- }
137
- }
138
- return path.join(SESSIONS_DIR, latest2.dir);
139
- }
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);
140
115
  } catch {
141
116
  }
142
- throw new Error("No active sessions. Start Copilot with groundcrew first.");
117
+ throw new Error(`Session "${explicitSession}" not found.`);
143
118
  }
144
- if (ids.length === 1) {
145
- return path.join(SESSIONS_DIR, ids[0]);
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);
146
129
  }
147
- let latest = { id: ids[0], time: 0 };
148
- for (const id of ids) {
149
- const started = new Date(sessions2[id].started).getTime();
150
- if (started > latest.time) {
151
- latest = { id, time: started };
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 };
152
137
  }
138
+ return path.join(SESSIONS_DIR, latest.id);
153
139
  }
154
- return path.join(SESSIONS_DIR, latest.id);
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);
154
+ }
155
+ } catch {
156
+ }
157
+ throw new Error("No active sessions. Start Copilot with groundcrew first.\n Run 'groundcrew sessions' to see available sessions.");
155
158
  }
156
159
  function sessionQueueFile(sessionDir) {
157
160
  return path.join(sessionDir, "queue.json");
@@ -185,10 +188,6 @@ var cyan = (t) => color(36, t);
185
188
  var dim = (t) => color(2, t);
186
189
  var bold = (t) => color(1, t);
187
190
  var red = (t) => color(31, t);
188
- async function init() {
189
- await fs.mkdir(SESSIONS_DIR, { recursive: true });
190
- console.log(green("Groundcrew initialized.") + ` ${dim(GROUNDCREW_DIR + "/ created")}`);
191
- }
192
191
  async function add(taskText, priority, sessionDir) {
193
192
  const queue = await readQueue(sessionDir);
194
193
  const task = {
@@ -296,7 +295,7 @@ async function history(_sessionDir) {
296
295
  console.log();
297
296
  }
298
297
  }
299
- async function sessions() {
298
+ async function sessions(filterRepo, filterStatus) {
300
299
  const active = await readActiveSessions();
301
300
  const ids = Object.keys(active);
302
301
  let allDirs = [];
@@ -308,23 +307,47 @@ async function sessions() {
308
307
  console.log(dim("No sessions found."));
309
308
  return;
310
309
  }
311
- console.log(bold("Sessions:\n"));
310
+ const byRepo = /* @__PURE__ */ new Map();
312
311
  for (const dir of allDirs) {
313
- const isActive = ids.includes(dir);
314
- const sessionDir = path.join(SESSIONS_DIR, dir);
315
- let info = "";
316
- try {
317
- const session = JSON.parse(await fs.readFile(path.join(sessionDir, "session.json"), "utf-8"));
318
- const startTime = new Date(session.started).getTime();
319
- const minutes = Math.round((Date.now() - startTime) / 6e4);
320
- const statusColor = session.status === "active" ? green : session.status === "parked" ? yellow : dim;
321
- info = `${statusColor(session.status)} | ${minutes}min | ${session.tasksCompleted || 0} tasks done`;
322
- } catch {
323
- info = dim("no session data");
312
+ const dashIdx = dir.lastIndexOf("-");
313
+ const repo = dashIdx > 0 ? dir.substring(0, dashIdx) : "unknown";
314
+ if (filterRepo && repo !== filterRepo) continue;
315
+ if (!byRepo.has(repo)) byRepo.set(repo, []);
316
+ byRepo.get(repo).push(dir);
317
+ }
318
+ if (byRepo.size === 0) {
319
+ console.log(dim(filterRepo ? `No sessions found for repo "${filterRepo}".` : "No sessions found."));
320
+ return;
321
+ }
322
+ console.log(bold("Sessions:\n"));
323
+ for (const [repo, dirs] of byRepo) {
324
+ const isCurrent = repo === REPO_NAME;
325
+ const repoEntries = [];
326
+ for (const dir of dirs) {
327
+ const isActive = ids.includes(dir);
328
+ const sessionDir = path.join(SESSIONS_DIR, dir);
329
+ let sessionStatus = "unknown";
330
+ let info = "";
331
+ try {
332
+ const session = JSON.parse(await fs.readFile(path.join(sessionDir, "session.json"), "utf-8"));
333
+ sessionStatus = session.status || "unknown";
334
+ const startTime = new Date(session.started).getTime();
335
+ const minutes = Math.round((Date.now() - startTime) / 6e4);
336
+ const statusColor = session.status === "active" ? green : session.status === "parked" ? yellow : dim;
337
+ info = `${statusColor(session.status)} | ${minutes}min | ${session.tasksCompleted || 0} tasks done`;
338
+ } catch {
339
+ info = dim("no session data");
340
+ }
341
+ if (filterStatus && sessionStatus !== filterStatus) continue;
342
+ const queue = await readQueue(sessionDir);
343
+ const marker = isActive ? green("*") : " ";
344
+ const shortId = dir.substring(repo.length + 1);
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);
324
350
  }
325
- const queue = await readQueue(sessionDir);
326
- const marker = isActive ? green("*") : " ";
327
- console.log(` ${marker} ${cyan(dir)} ${info} | ${queue.tasks.length} queued`);
328
351
  }
329
352
  if (ids.length > 0) {
330
353
  console.log(dim(`
@@ -381,14 +404,14 @@ async function destroyOne(sessionId) {
381
404
  }
382
405
  async function stopAll() {
383
406
  const active = await readActiveSessions();
384
- const ids = Object.keys(active);
407
+ const ids = Object.keys(active).filter(isRepoSession);
385
408
  let allDirs = [];
386
409
  try {
387
- allDirs = await fs.readdir(SESSIONS_DIR);
410
+ allDirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession);
388
411
  } catch {
389
412
  }
390
413
  if (ids.length === 0 && allDirs.length === 0) {
391
- console.log(dim("No sessions to stop."));
414
+ console.log(dim(`No sessions to stop for repo "${REPO_NAME}".`));
392
415
  return;
393
416
  }
394
417
  for (const id of ids) {
@@ -404,31 +427,26 @@ async function stopAll() {
404
427
  console.log(` ${green("cleaned")} ${cyan(dir)} ${dim("(orphaned)")}`);
405
428
  }
406
429
  }
407
- await fs.writeFile(ACTIVE_SESSIONS_FILE, "{}");
408
- console.log(green("\nAll sessions stopped."));
430
+ const remaining = await readActiveSessions();
431
+ for (const id of ids) delete remaining[id];
432
+ await fs.writeFile(ACTIVE_SESSIONS_FILE, JSON.stringify(remaining, null, 2));
433
+ console.log(green(`
434
+ All ${REPO_NAME} sessions stopped.`));
409
435
  }
410
436
  async function destroyAll() {
411
437
  await stopAll();
412
438
  try {
413
- const dirs = await fs.readdir(SESSIONS_DIR);
439
+ const dirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession);
414
440
  for (const dir of dirs) {
415
441
  await fs.rm(path.join(SESSIONS_DIR, dir), { recursive: true, force: true });
416
442
  }
417
443
  } catch {
418
444
  }
419
445
  try {
420
- await fs.unlink(HISTORY_FILE);
446
+ await fs.unlink(path.join(GROUNDCREW_HOME, "tool-history.csv"));
421
447
  } catch {
422
448
  }
423
- try {
424
- await fs.unlink(ACTIVE_SESSIONS_FILE);
425
- } catch {
426
- }
427
- try {
428
- await fs.unlink(path.join(GROUNDCREW_DIR, "tool-history.csv"));
429
- } catch {
430
- }
431
- console.log(green("All session data and history deleted."));
449
+ console.log(green(`All ${REPO_NAME} session data deleted.`));
432
450
  }
433
451
  async function listSessionChoices() {
434
452
  const active = await readActiveSessions();
@@ -1116,7 +1134,6 @@ ${bold("groundcrew")} \u2014 CLI companion for the Groundcrew Copilot plugin
1116
1134
  ${bold("Usage:")}
1117
1135
  groundcrew chat Interactive chat mode (recommended)
1118
1136
  groundcrew chat --session <id> Chat with a specific session
1119
- groundcrew init Initialize .groundcrew/ in current dir
1120
1137
  groundcrew add <task> Add a task to the queue
1121
1138
  groundcrew add --priority <task> Add an urgent task (processed first)
1122
1139
  groundcrew add --session <id> <task> Add to a specific session
@@ -1124,12 +1141,14 @@ ${bold("Usage:")}
1124
1141
  groundcrew feedback --session <id> <message> Send feedback to a specific session
1125
1142
  groundcrew queue List pending tasks
1126
1143
  groundcrew status Show session status and last update
1127
- 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)
1128
1147
  groundcrew history Show completed tasks
1129
1148
  groundcrew clear Clear all pending tasks
1130
- groundcrew stop Stop all active sessions
1149
+ groundcrew stop Stop all sessions for current repo
1131
1150
  groundcrew stop --session <id> Stop a specific session
1132
- groundcrew destroy Delete all sessions, history, and data
1151
+ groundcrew destroy Delete all sessions for current repo
1133
1152
  groundcrew destroy --session <id> Delete a specific session
1134
1153
 
1135
1154
  ${bold("Session targeting:")}
@@ -1158,17 +1177,16 @@ function extractFlag(args, flag) {
1158
1177
  async function main() {
1159
1178
  await resolveRoot();
1160
1179
  const rawArgs = process.argv.slice(2);
1161
- 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");
1162
1183
  const command = args[0];
1163
1184
  switch (command) {
1164
- case "init":
1165
- await init();
1166
- return;
1167
1185
  case "chat":
1168
1186
  await chat(explicitSession);
1169
1187
  return;
1170
1188
  case "sessions":
1171
- await sessions();
1189
+ await sessions(filterRepo, filterStatus);
1172
1190
  return;
1173
1191
  case "history":
1174
1192
  await history();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundcrew-cli",
3
- "version": "0.16.6",
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
@@ -1,6 +1,7 @@
1
1
  import fs from "fs/promises";
2
2
  import { existsSync } from "fs";
3
3
  import path from "path";
4
+ import os from "os";
4
5
  import readline from "readline";
5
6
  import { execFile, execFileSync } from "child_process";
6
7
  import { promisify } from "util";
@@ -68,49 +69,38 @@ close access fp`], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
68
69
  } catch { return null; }
69
70
  }
70
71
 
71
- // Resolved at startup by resolveRoot() — git-aware project root discovery
72
- let GROUNDCREW_DIR = ".groundcrew";
73
- let SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
74
- let ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_DIR, "active-sessions.json");
75
- let HISTORY_FILE = path.join(GROUNDCREW_DIR, "history.json");
72
+ // ── Centralized storage at ~/.groundcrew ─────────────────────────────────────
73
+ const GROUNDCREW_HOME = path.join(os.homedir(), ".groundcrew");
74
+ const SESSIONS_DIR = path.join(GROUNDCREW_HOME, "sessions");
75
+ const ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_HOME, "active-sessions.json");
76
+ const HISTORY_FILE = path.join(GROUNDCREW_HOME, "history.json");
77
+
78
+ let REPO_NAME = "";
76
79
 
77
80
  /**
78
- * Resolve the project root that contains .groundcrew/.
79
- * Uses git rev-parse --show-toplevel for worktree support, then walks up
80
- * from CWD as fallback. This ensures the CLI works from subdirectories
81
- * and git worktrees.
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
- const gitRoot = stdout.trim();
90
- if (gitRoot && existsSync(path.join(gitRoot, ".groundcrew"))) {
91
- root = gitRoot;
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);
92
94
  }
93
95
  } catch { /* not a git repo or git not installed */ }
94
96
 
95
- // 2. Walk up from CWD looking for .groundcrew/
96
- if (!root) {
97
- let dir = process.cwd();
98
- while (dir !== path.dirname(dir)) {
99
- if (existsSync(path.join(dir, ".groundcrew"))) {
100
- root = dir;
101
- break;
102
- }
103
- dir = path.dirname(dir);
104
- }
105
- }
97
+ // 2. Fallback to CWD
98
+ if (!repoRoot) repoRoot = process.cwd();
106
99
 
107
- // 3. Fallback to CWD (for `groundcrew init`)
108
- if (!root) root = process.cwd();
100
+ REPO_NAME = path.basename(repoRoot).replace(/[^a-zA-Z0-9_-]/g, "_") || "unknown";
109
101
 
110
- GROUNDCREW_DIR = path.join(root, ".groundcrew");
111
- SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
112
- ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_DIR, "active-sessions.json");
113
- HISTORY_FILE = path.join(GROUNDCREW_DIR, "history.json");
102
+ // Ensure centralized dirs exist
103
+ await fs.mkdir(SESSIONS_DIR, { recursive: true });
114
104
  }
115
105
 
116
106
  interface Task {
@@ -147,57 +137,78 @@ async function readActiveSessions(): Promise<Record<string, ActiveSessionEntry>>
147
137
  }
148
138
  }
149
139
 
140
+ /**
141
+ * Filter session IDs that belong to the current repo.
142
+ * Session IDs are prefixed with repo name: "mekari_credit-a1b2c3d4"
143
+ */
144
+ function isRepoSession(sessionId: string): boolean {
145
+ return sessionId.startsWith(REPO_NAME + "-");
146
+ }
147
+
150
148
  /**
151
149
  * Resolve which session to target.
152
- * Priority: --session flag > single active session > error if ambiguous.
150
+ * Priority: --session flag > repo match > any active session > error.
153
151
  */
154
152
  async function resolveSessionDir(explicitSession?: string): Promise<string> {
155
153
  if (explicitSession) {
154
+ // Allow short hex suffix — find the full session ID
156
155
  const dir = path.join(SESSIONS_DIR, explicitSession);
157
- if (!existsSync(dir)) {
158
- throw new Error(`Session "${explicitSession}" not found.`);
159
- }
160
- return dir;
161
- }
162
-
163
- const sessions = await readActiveSessions();
164
- const ids = Object.keys(sessions);
156
+ if (existsSync(dir)) return dir;
165
157
 
166
- if (ids.length === 0) {
167
- // Fallback: check if any session dirs exist (server may have exited without cleanup)
158
+ // Try matching as suffix
168
159
  try {
169
- const dirs = await fs.readdir(SESSIONS_DIR);
170
- if (dirs.length === 1) return path.join(SESSIONS_DIR, dirs[0]);
171
- if (dirs.length > 1) {
172
- // Pick most recently modified
173
- let latest = { dir: dirs[0], mtime: 0 };
174
- for (const d of dirs) {
175
- try {
176
- const stat = await fs.stat(path.join(SESSIONS_DIR, d, "session.json"));
177
- if (stat.mtimeMs > latest.mtime) {
178
- latest = { dir: d, mtime: stat.mtimeMs };
179
- }
180
- } catch { /* skip */ }
181
- }
182
- return path.join(SESSIONS_DIR, latest.dir);
183
- }
184
- } catch { /* no sessions dir */ }
185
- throw new Error("No active sessions. 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.`);
186
166
  }
187
167
 
188
- if (ids.length === 1) {
189
- 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);
190
180
  }
191
181
 
192
- // Multiple sessions pick latest
193
- let latest = { id: ids[0], time: 0 };
194
- for (const id of ids) {
195
- const started = new Date(sessions[id].started).getTime();
196
- if (started > latest.time) {
197
- 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 };
198
190
  }
191
+ return path.join(SESSIONS_DIR, latest.id);
199
192
  }
200
- 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.");
201
212
  }
202
213
 
203
214
  function sessionQueueFile(sessionDir: string): string {
@@ -244,11 +255,6 @@ const red = (t: string) => color(31, t);
244
255
 
245
256
  // ── Commands ──────────────────────────────────────────────────────────────────
246
257
 
247
- async function init(): Promise<void> {
248
- await fs.mkdir(SESSIONS_DIR, { recursive: true });
249
- console.log(green("Groundcrew initialized.") + ` ${dim(GROUNDCREW_DIR + "/ created")}`);
250
- }
251
-
252
258
  async function add(taskText: string, priority: number, sessionDir: string): Promise<void> {
253
259
  const queue = await readQueue(sessionDir);
254
260
  const task: Task = {
@@ -382,11 +388,11 @@ async function history(_sessionDir?: string): Promise<void> {
382
388
  }
383
389
  }
384
390
 
385
- async function sessions(): Promise<void> {
391
+ async function sessions(filterRepo?: string, filterStatus?: string): Promise<void> {
386
392
  const active = await readActiveSessions();
387
393
  const ids = Object.keys(active);
388
394
 
389
- // Also check for session dirs that may not be in active-sessions.json
395
+ // All session dirs on disk
390
396
  let allDirs: string[] = [];
391
397
  try {
392
398
  allDirs = await fs.readdir(SESSIONS_DIR);
@@ -397,27 +403,61 @@ async function sessions(): Promise<void> {
397
403
  return;
398
404
  }
399
405
 
406
+ // Group by repo prefix
407
+ const byRepo = new Map<string, string[]>();
408
+ for (const dir of allDirs) {
409
+ const dashIdx = dir.lastIndexOf("-");
410
+ const repo = dashIdx > 0 ? dir.substring(0, dashIdx) : "unknown";
411
+
412
+ // Apply --repo filter
413
+ if (filterRepo && repo !== filterRepo) continue;
414
+
415
+ if (!byRepo.has(repo)) byRepo.set(repo, []);
416
+ byRepo.get(repo)!.push(dir);
417
+ }
418
+
419
+ if (byRepo.size === 0) {
420
+ console.log(dim(filterRepo ? `No sessions found for repo "${filterRepo}".` : "No sessions found."));
421
+ return;
422
+ }
423
+
400
424
  console.log(bold("Sessions:\n"));
401
425
 
402
- for (const dir of allDirs) {
403
- const isActive = ids.includes(dir);
404
- const sessionDir = path.join(SESSIONS_DIR, dir);
426
+ for (const [repo, dirs] of byRepo) {
427
+ const isCurrent = repo === REPO_NAME;
428
+ const repoEntries: string[] = [];
405
429
 
406
- let info = "";
407
- try {
408
- const session = JSON.parse(await fs.readFile(path.join(sessionDir, "session.json"), "utf-8"));
409
- const startTime = new Date(session.started).getTime();
410
- const minutes = Math.round((Date.now() - startTime) / 60000);
411
- const statusColor = session.status === "active" ? green : session.status === "parked" ? yellow : dim;
412
- info = `${statusColor(session.status)} | ${minutes}min | ${session.tasksCompleted || 0} tasks done`;
413
- } catch {
414
- info = dim("no session data");
415
- }
430
+ for (const dir of dirs) {
431
+ const isActive = ids.includes(dir);
432
+ const sessionDir = path.join(SESSIONS_DIR, dir);
433
+
434
+ let sessionStatus = "unknown";
435
+ let info = "";
436
+ try {
437
+ const session = JSON.parse(await fs.readFile(path.join(sessionDir, "session.json"), "utf-8"));
438
+ sessionStatus = session.status || "unknown";
439
+ const startTime = new Date(session.started).getTime();
440
+ const minutes = Math.round((Date.now() - startTime) / 60000);
441
+ const statusColor = session.status === "active" ? green : session.status === "parked" ? yellow : dim;
442
+ info = `${statusColor(session.status)} | ${minutes}min | ${session.tasksCompleted || 0} tasks done`;
443
+ } catch {
444
+ info = dim("no session data");
445
+ }
446
+
447
+ // Apply --status filter
448
+ if (filterStatus && sessionStatus !== filterStatus) continue;
449
+
450
+ const queue = await readQueue(sessionDir);
451
+ const marker = isActive ? green("*") : " ";
452
+ const shortId = dir.substring(repo.length + 1);
416
453
 
417
- const queue = await readQueue(sessionDir);
418
- const marker = isActive ? green("*") : " ";
454
+ repoEntries.push(` ${marker} ${cyan(shortId)} ${info} | ${queue.tasks.length} queued`);
455
+ }
419
456
 
420
- console.log(` ${marker} ${cyan(dir)} ${info} | ${queue.tasks.length} queued`);
457
+ if (repoEntries.length > 0) {
458
+ console.log(` ${isCurrent ? green(repo) : dim(repo)}`);
459
+ for (const entry of repoEntries) console.log(entry);
460
+ }
421
461
  }
422
462
 
423
463
  if (ids.length > 0) {
@@ -484,13 +524,14 @@ async function destroyOne(sessionId: string): Promise<void> {
484
524
 
485
525
  async function stopAll(): Promise<void> {
486
526
  const active = await readActiveSessions();
487
- const ids = Object.keys(active);
527
+ // Scope to current repo
528
+ const ids = Object.keys(active).filter(isRepoSession);
488
529
 
489
530
  let allDirs: string[] = [];
490
- try { allDirs = await fs.readdir(SESSIONS_DIR); } catch {}
531
+ try { allDirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession); } catch {}
491
532
 
492
533
  if (ids.length === 0 && allDirs.length === 0) {
493
- console.log(dim("No sessions to stop."));
534
+ console.log(dim(`No sessions to stop for repo "${REPO_NAME}".`));
494
535
  return;
495
536
  }
496
537
 
@@ -511,33 +552,29 @@ async function stopAll(): Promise<void> {
511
552
  }
512
553
  }
513
554
 
514
- // Clear active sessions file
515
- await fs.writeFile(ACTIVE_SESSIONS_FILE, "{}");
516
- console.log(green("\nAll sessions stopped."));
555
+ // Remove stopped sessions from active-sessions.json (keep other repos)
556
+ const remaining = await readActiveSessions();
557
+ for (const id of ids) delete remaining[id];
558
+ await fs.writeFile(ACTIVE_SESSIONS_FILE, JSON.stringify(remaining, null, 2));
559
+ console.log(green(`\nAll ${REPO_NAME} sessions stopped.`));
517
560
  }
518
561
 
519
562
  async function destroyAll(): Promise<void> {
520
- // Stop everything first
563
+ // Stop this repo's sessions first
521
564
  await stopAll();
522
565
 
523
- // Delete all session directories
566
+ // Delete this repo's session directories only
524
567
  try {
525
- const dirs = await fs.readdir(SESSIONS_DIR);
568
+ const dirs = (await fs.readdir(SESSIONS_DIR)).filter(isRepoSession);
526
569
  for (const dir of dirs) {
527
570
  await fs.rm(path.join(SESSIONS_DIR, dir), { recursive: true, force: true });
528
571
  }
529
572
  } catch { /* no sessions dir */ }
530
573
 
531
- // Delete history
532
- try { await fs.unlink(HISTORY_FILE); } catch {}
533
-
534
- // Delete active sessions file
535
- try { await fs.unlink(ACTIVE_SESSIONS_FILE); } catch {}
536
-
537
574
  // Delete tool history
538
- try { await fs.unlink(path.join(GROUNDCREW_DIR, "tool-history.csv")); } catch {}
575
+ try { await fs.unlink(path.join(GROUNDCREW_HOME, "tool-history.csv")); } catch {}
539
576
 
540
- console.log(green("All session data and history deleted."));
577
+ console.log(green(`All ${REPO_NAME} session data deleted.`));
541
578
  }
542
579
 
543
580
  // ── Chat Mode ────────────────────────────────────────────────────────────────
@@ -1241,7 +1278,6 @@ ${bold("groundcrew")} — CLI companion for the Groundcrew Copilot plugin
1241
1278
  ${bold("Usage:")}
1242
1279
  groundcrew chat Interactive chat mode (recommended)
1243
1280
  groundcrew chat --session <id> Chat with a specific session
1244
- groundcrew init Initialize .groundcrew/ in current dir
1245
1281
  groundcrew add <task> Add a task to the queue
1246
1282
  groundcrew add --priority <task> Add an urgent task (processed first)
1247
1283
  groundcrew add --session <id> <task> Add to a specific session
@@ -1249,12 +1285,14 @@ ${bold("Usage:")}
1249
1285
  groundcrew feedback --session <id> <message> Send feedback to a specific session
1250
1286
  groundcrew queue List pending tasks
1251
1287
  groundcrew status Show session status and last update
1252
- 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)
1253
1291
  groundcrew history Show completed tasks
1254
1292
  groundcrew clear Clear all pending tasks
1255
- groundcrew stop Stop all active sessions
1293
+ groundcrew stop Stop all sessions for current repo
1256
1294
  groundcrew stop --session <id> Stop a specific session
1257
- groundcrew destroy Delete all sessions, history, and data
1295
+ groundcrew destroy Delete all sessions for current repo
1258
1296
  groundcrew destroy --session <id> Delete a specific session
1259
1297
 
1260
1298
  ${bold("Session targeting:")}
@@ -1288,21 +1326,20 @@ async function main(): Promise<void> {
1288
1326
 
1289
1327
  const rawArgs = process.argv.slice(2);
1290
1328
 
1291
- // Extract --session flag from anywhere in args
1292
- 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");
1293
1333
 
1294
1334
  const command = args[0];
1295
1335
 
1296
1336
  // Commands that don't need a session (handle their own resolution)
1297
1337
  switch (command) {
1298
- case "init":
1299
- await init();
1300
- return;
1301
1338
  case "chat":
1302
1339
  await chat(explicitSession);
1303
1340
  return;
1304
1341
  case "sessions":
1305
- await sessions();
1342
+ await sessions(filterRepo, filterStatus);
1306
1343
  return;
1307
1344
  case "history":
1308
1345
  await history();