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.
- package/dist/index.js +119 -101
- package/package.json +1 -1
- 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
|
|
77
|
-
var SESSIONS_DIR = path.join(
|
|
78
|
-
var ACTIVE_SESSIONS_FILE = path.join(
|
|
79
|
-
var HISTORY_FILE = path.join(
|
|
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
|
|
83
|
+
let repoRoot = null;
|
|
82
84
|
try {
|
|
83
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "--
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
|
|
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 (!
|
|
91
|
-
|
|
92
|
-
|
|
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 (
|
|
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
|
|
126
|
-
|
|
127
|
-
if (
|
|
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("
|
|
117
|
+
throw new Error(`Session "${explicitSession}" not found.`);
|
|
143
118
|
}
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
312
311
|
for (const dir of allDirs) {
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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(
|
|
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
|
|
408
|
-
|
|
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(
|
|
446
|
+
await fs.unlink(path.join(GROUNDCREW_HOME, "tool-history.csv"));
|
|
421
447
|
} catch {
|
|
422
448
|
}
|
|
423
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
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
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
*
|
|
79
|
-
*
|
|
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
|
|
85
|
+
let repoRoot: string | null = null;
|
|
85
86
|
|
|
86
|
-
// 1. Try git
|
|
87
|
+
// 1. Try git --git-common-dir (resolves worktrees to main repo)
|
|
87
88
|
try {
|
|
88
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "--
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
|
|
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.
|
|
96
|
-
if (!
|
|
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
|
-
|
|
108
|
-
if (!root) root = process.cwd();
|
|
100
|
+
REPO_NAME = path.basename(repoRoot).replace(/[^a-zA-Z0-9_-]/g, "_") || "unknown";
|
|
109
101
|
|
|
110
|
-
|
|
111
|
-
|
|
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 >
|
|
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 (
|
|
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
|
-
|
|
167
|
-
// Fallback: check if any session dirs exist (server may have exited without cleanup)
|
|
158
|
+
// Try matching as suffix
|
|
168
159
|
try {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
403
|
-
const
|
|
404
|
-
const
|
|
426
|
+
for (const [repo, dirs] of byRepo) {
|
|
427
|
+
const isCurrent = repo === REPO_NAME;
|
|
428
|
+
const repoEntries: string[] = [];
|
|
405
429
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
418
|
-
|
|
454
|
+
repoEntries.push(` ${marker} ${cyan(shortId)} ${info} | ${queue.tasks.length} queued`);
|
|
455
|
+
}
|
|
419
456
|
|
|
420
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
515
|
-
await
|
|
516
|
-
|
|
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
|
|
563
|
+
// Stop this repo's sessions first
|
|
521
564
|
await stopAll();
|
|
522
565
|
|
|
523
|
-
// Delete
|
|
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(
|
|
575
|
+
try { await fs.unlink(path.join(GROUNDCREW_HOME, "tool-history.csv")); } catch {}
|
|
539
576
|
|
|
540
|
-
console.log(green(
|
|
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
|
|
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
|
|
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
|
|
1292
|
-
const { value: explicitSession, remaining:
|
|
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();
|