code-squad-cli 1.2.16 → 1.2.17

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 CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // dist/index.js
4
4
  import * as path12 from "path";
5
- import * as fs10 from "fs";
6
- import * as os4 from "os";
5
+ import * as fs11 from "fs";
6
+ import * as os5 from "os";
7
7
  import * as crypto from "crypto";
8
8
  import chalk2 from "chalk";
9
9
 
@@ -533,18 +533,36 @@ function collectFlatFiles(rootPath, currentPath, maxDepth, depth = 0, result = {
533
533
  }
534
534
  return result;
535
535
  }
536
+ function getCwdFromSession(req, res) {
537
+ const sessionId = req.query.session_id;
538
+ if (!sessionId) {
539
+ res.status(400).json({ error: "Missing session_id parameter" });
540
+ return null;
541
+ }
542
+ const sessionManager = req.app.locals.sessionManager;
543
+ const session = sessionManager.getSession(sessionId);
544
+ if (!session) {
545
+ res.status(404).json({ error: "Session not found" });
546
+ return null;
547
+ }
548
+ return session.cwd;
549
+ }
536
550
  router.get("/", (req, res) => {
537
- const state = req.app.locals.state;
538
- const tree = buildFileTree(state.cwd, state.cwd, 10);
551
+ const cwd = getCwdFromSession(req, res);
552
+ if (!cwd)
553
+ return;
554
+ const tree = buildFileTree(cwd, cwd, 10);
539
555
  const response = {
540
- root: state.cwd,
556
+ root: cwd,
541
557
  tree
542
558
  };
543
559
  res.json(response);
544
560
  });
545
561
  router.get("/flat", (req, res) => {
546
- const state = req.app.locals.state;
547
- const result = collectFlatFiles(state.cwd, state.cwd, 10);
562
+ const cwd = getCwdFromSession(req, res);
563
+ if (!cwd)
564
+ return;
565
+ const result = collectFlatFiles(cwd, cwd, 10);
548
566
  result.files.sort((a, b) => a.path.localeCompare(b.path));
549
567
  result.filteredDirs.sort();
550
568
  let recentFile = null;
@@ -686,16 +704,32 @@ function detectLanguage(filePath) {
686
704
  }
687
705
  return "text";
688
706
  }
707
+ function getCwdFromSession2(req, res) {
708
+ const sessionId = req.query.session_id;
709
+ if (!sessionId) {
710
+ res.status(400).json({ error: "Missing session_id parameter" });
711
+ return null;
712
+ }
713
+ const sessionManager = req.app.locals.sessionManager;
714
+ const session = sessionManager.getSession(sessionId);
715
+ if (!session) {
716
+ res.status(404).json({ error: "Session not found" });
717
+ return null;
718
+ }
719
+ return session.cwd;
720
+ }
689
721
  router2.get("/", (req, res) => {
690
- const state = req.app.locals.state;
722
+ const cwd = getCwdFromSession2(req, res);
723
+ if (!cwd)
724
+ return;
691
725
  const relativePath = req.query.path;
692
726
  if (!relativePath) {
693
727
  res.status(400).json({ error: "Missing path parameter" });
694
728
  return;
695
729
  }
696
- const filePath = path3.join(state.cwd, relativePath);
730
+ const filePath = path3.join(cwd, relativePath);
697
731
  const resolvedPath = path3.resolve(filePath);
698
- const resolvedCwd = path3.resolve(state.cwd);
732
+ const resolvedCwd = path3.resolve(cwd);
699
733
  if (!resolvedPath.startsWith(resolvedCwd)) {
700
734
  res.status(403).json({ error: "Access denied" });
701
735
  return;
@@ -823,12 +857,28 @@ function createAddedFileDiff(content) {
823
857
  lines: diffLines
824
858
  }];
825
859
  }
860
+ function getCwdFromSession3(req, res) {
861
+ const sessionId = req.query.session_id;
862
+ if (!sessionId) {
863
+ res.status(400).json({ error: "Missing session_id parameter" });
864
+ return null;
865
+ }
866
+ const sessionManager = req.app.locals.sessionManager;
867
+ const session = sessionManager.getSession(sessionId);
868
+ if (!session) {
869
+ res.status(404).json({ error: "Session not found" });
870
+ return null;
871
+ }
872
+ return session.cwd;
873
+ }
826
874
  router3.get("/status", (req, res) => {
827
- const state = req.app.locals.state;
875
+ const cwd = getCwdFromSession3(req, res);
876
+ if (!cwd)
877
+ return;
828
878
  let isGitRepo = false;
829
879
  try {
830
880
  execSync("git rev-parse --git-dir", {
831
- cwd: state.cwd,
881
+ cwd,
832
882
  stdio: "pipe"
833
883
  });
834
884
  isGitRepo = true;
@@ -845,7 +895,7 @@ router3.get("/status", (req, res) => {
845
895
  let unstaged = [];
846
896
  try {
847
897
  const output = execSync("git status --porcelain", {
848
- cwd: state.cwd,
898
+ cwd,
849
899
  encoding: "utf-8"
850
900
  });
851
901
  unstaged = parseGitStatus(output);
@@ -858,17 +908,19 @@ router3.get("/status", (req, res) => {
858
908
  res.json(response);
859
909
  });
860
910
  router3.get("/diff", (req, res) => {
861
- const state = req.app.locals.state;
911
+ const cwd = getCwdFromSession3(req, res);
912
+ if (!cwd)
913
+ return;
862
914
  const relativePath = req.query.path;
863
915
  if (!relativePath) {
864
916
  res.status(400).json({ error: "Missing path parameter" });
865
917
  return;
866
918
  }
867
- const fullPath = path4.join(state.cwd, relativePath);
919
+ const fullPath = path4.join(cwd, relativePath);
868
920
  let fileStatus = "modified";
869
921
  try {
870
922
  const statusOutput = execSync(`git status --porcelain -- "${relativePath}"`, {
871
- cwd: state.cwd,
923
+ cwd,
872
924
  encoding: "utf-8"
873
925
  });
874
926
  if (statusOutput.startsWith("??")) {
@@ -896,7 +948,7 @@ router3.get("/diff", (req, res) => {
896
948
  }
897
949
  try {
898
950
  const diffOutput = execSync(`git diff --no-color -- "${relativePath}"`, {
899
- cwd: state.cwd,
951
+ cwd,
900
952
  encoding: "utf-8"
901
953
  });
902
954
  if (!diffOutput.trim()) {
@@ -1070,8 +1122,12 @@ async function pasteToOriginalSession(sessionId) {
1070
1122
  // dist/flip/routes/submit.js
1071
1123
  var router4 = Router4();
1072
1124
  router4.post("/", async (req, res) => {
1073
- const state = req.app.locals.state;
1125
+ const sessionManager = req.app.locals.sessionManager;
1074
1126
  const body = req.body;
1127
+ if (!body.session_id) {
1128
+ res.status(400).json({ error: "Missing session_id" });
1129
+ return;
1130
+ }
1075
1131
  if (!body.items || body.items.length === 0) {
1076
1132
  res.status(400).json({ error: "No items to submit" });
1077
1133
  return;
@@ -1101,20 +1157,21 @@ router4.post("/", async (req, res) => {
1101
1157
  }
1102
1158
  }
1103
1159
  res.json({ status: "ok" });
1104
- if (state.resolve) {
1105
- state.resolve(formatted);
1106
- }
1160
+ await sessionManager.unregisterSession(body.session_id);
1107
1161
  });
1108
1162
 
1109
1163
  // dist/flip/routes/cancel.js
1110
1164
  import { Router as Router5 } from "express";
1111
1165
  var router5 = Router5();
1112
- router5.post("/", (req, res) => {
1113
- const state = req.app.locals.state;
1114
- res.json({ status: "ok" });
1115
- if (state.resolve) {
1116
- state.resolve(null);
1166
+ router5.post("/", async (req, res) => {
1167
+ const sessionManager = req.app.locals.sessionManager;
1168
+ const body = req.body;
1169
+ if (!body.session_id) {
1170
+ res.status(400).json({ error: "Missing session_id" });
1171
+ return;
1117
1172
  }
1173
+ res.json({ status: "ok" });
1174
+ await sessionManager.unregisterSession(body.session_id);
1118
1175
  });
1119
1176
 
1120
1177
  // dist/flip/routes/static.js
@@ -1149,28 +1206,98 @@ function createStaticRouter() {
1149
1206
  return router6;
1150
1207
  }
1151
1208
 
1152
- // dist/flip/routes/events.js
1209
+ // dist/flip/routes/ping.js
1153
1210
  import { Router as Router7 } from "express";
1154
- function createEventsRouter(sseManager) {
1211
+ function createPingRouter() {
1155
1212
  const router6 = Router7();
1156
- router6.get("/", (req, res) => {
1157
- res.setHeader("Content-Type", "text/event-stream");
1158
- res.setHeader("Cache-Control", "no-cache");
1159
- res.setHeader("Connection", "keep-alive");
1160
- res.setHeader("X-Accel-Buffering", "no");
1161
- res.write('data: {"type":"connected"}\n\n');
1162
- sseManager.addClient(res);
1163
- const heartbeat = setInterval(() => {
1164
- res.write(":heartbeat\n\n");
1165
- }, 3e4);
1166
- req.on("close", () => {
1167
- clearInterval(heartbeat);
1168
- res.end();
1213
+ router6.get("/", (_req, res) => {
1214
+ const response = {
1215
+ id: SERVER_ID,
1216
+ version: "1.0.0"
1217
+ };
1218
+ res.json(response);
1219
+ });
1220
+ return router6;
1221
+ }
1222
+
1223
+ // dist/flip/routes/session.js
1224
+ import { Router as Router8 } from "express";
1225
+ function createSessionRouter(sessionManager) {
1226
+ const router6 = Router8();
1227
+ router6.post("/register", (req, res) => {
1228
+ const body = req.body;
1229
+ if (!body.session_id || !body.cwd) {
1230
+ res.status(400).json({ error: "Missing session_id or cwd" });
1231
+ return;
1232
+ }
1233
+ if (!sessionManager.hasSessions && req.app.locals.clearIdleTimer) {
1234
+ req.app.locals.clearIdleTimer();
1235
+ }
1236
+ sessionManager.registerSession(body.session_id, body.cwd);
1237
+ const response = {
1238
+ status: "ok",
1239
+ session_id: body.session_id
1240
+ };
1241
+ res.json(response);
1242
+ });
1243
+ router6.post("/unregister", async (req, res) => {
1244
+ const body = req.body;
1245
+ if (!body.session_id) {
1246
+ res.status(400).json({ error: "Missing session_id" });
1247
+ return;
1248
+ }
1249
+ await sessionManager.unregisterSession(body.session_id);
1250
+ const response = {
1251
+ status: "ok"
1252
+ };
1253
+ res.json(response);
1254
+ });
1255
+ router6.get("/info", (req, res) => {
1256
+ const sessionId = req.query.session_id;
1257
+ if (!sessionId) {
1258
+ res.status(400).json({ error: "Missing session_id" });
1259
+ return;
1260
+ }
1261
+ const session = sessionManager.getSession(sessionId);
1262
+ if (!session) {
1263
+ res.status(404).json({ error: "Session not found" });
1264
+ return;
1265
+ }
1266
+ res.json({
1267
+ session_id: session.id,
1268
+ cwd: session.cwd,
1269
+ lastActivity: session.lastActivity
1169
1270
  });
1170
1271
  });
1171
1272
  return router6;
1172
1273
  }
1173
1274
 
1275
+ // dist/flip/routes/changes.js
1276
+ import { Router as Router9 } from "express";
1277
+ function createChangesRouter(sessionManager) {
1278
+ const router6 = Router9();
1279
+ router6.get("/", (req, res) => {
1280
+ const sessionId = req.query.session_id;
1281
+ if (!sessionId) {
1282
+ res.status(400).json({ error: "Missing session_id" });
1283
+ return;
1284
+ }
1285
+ sessionManager.touchSession(sessionId);
1286
+ const changes = sessionManager.consumePendingChanges(sessionId);
1287
+ if (!changes) {
1288
+ res.status(404).json({ error: "Session not found" });
1289
+ return;
1290
+ }
1291
+ const response = {
1292
+ filesChanged: changes.filesChanged,
1293
+ gitChanged: changes.gitChanged,
1294
+ changedFiles: Array.from(changes.changedFiles)
1295
+ };
1296
+ res.json(response);
1297
+ });
1298
+ return router6;
1299
+ }
1300
+
1174
1301
  // dist/flip/watcher/FileWatcher.js
1175
1302
  import chokidar from "chokidar";
1176
1303
  import * as path8 from "path";
@@ -1294,87 +1421,263 @@ var FileWatcher = class {
1294
1421
  }
1295
1422
  };
1296
1423
 
1297
- // dist/flip/events/SSEManager.js
1298
- var SSEManager = class {
1299
- clients = /* @__PURE__ */ new Set();
1300
- addClient(res) {
1301
- this.clients.add(res);
1302
- res.on("close", () => {
1303
- this.clients.delete(res);
1304
- });
1424
+ // dist/flip/session/SessionManager.js
1425
+ var log = (...args) => console.error(...args);
1426
+ var SessionManager = class {
1427
+ sessions = /* @__PURE__ */ new Map();
1428
+ options;
1429
+ cleanupInterval = null;
1430
+ constructor(options) {
1431
+ this.options = options;
1305
1432
  }
1306
- removeClient(res) {
1307
- this.clients.delete(res);
1433
+ /**
1434
+ * Start the session cleanup timer
1435
+ */
1436
+ start() {
1437
+ this.cleanupInterval = setInterval(() => {
1438
+ this.cleanupTimedOutSessions();
1439
+ }, 5e3);
1308
1440
  }
1309
- broadcast(event) {
1310
- const data = JSON.stringify(event);
1311
- const message = `data: ${data}
1312
-
1313
- `;
1314
- this.clients.forEach((client) => {
1315
- client.write(message);
1316
- });
1441
+ /**
1442
+ * Stop the session manager and clean up all sessions
1443
+ */
1444
+ async stop() {
1445
+ if (this.cleanupInterval) {
1446
+ clearInterval(this.cleanupInterval);
1447
+ this.cleanupInterval = null;
1448
+ }
1449
+ const stopPromises = Array.from(this.sessions.values()).map((session) => session.watcher.stop());
1450
+ await Promise.all(stopPromises);
1451
+ this.sessions.clear();
1452
+ }
1453
+ /**
1454
+ * Register a new session or update existing one
1455
+ */
1456
+ registerSession(sessionId, cwd) {
1457
+ const existing = this.sessions.get(sessionId);
1458
+ if (existing) {
1459
+ existing.lastActivity = Date.now();
1460
+ if (existing.cwd !== cwd) {
1461
+ existing.watcher.stop();
1462
+ existing.cwd = cwd;
1463
+ existing.watcher = this.createWatcher(sessionId, cwd);
1464
+ existing.pendingChanges = this.createEmptyPendingChanges();
1465
+ }
1466
+ return existing;
1467
+ }
1468
+ const watcher = this.createWatcher(sessionId, cwd);
1469
+ const session = {
1470
+ id: sessionId,
1471
+ cwd,
1472
+ watcher,
1473
+ lastActivity: Date.now(),
1474
+ pendingChanges: this.createEmptyPendingChanges()
1475
+ };
1476
+ this.sessions.set(sessionId, session);
1477
+ log(`[SessionManager] Session registered: ${sessionId} (cwd: ${cwd})`);
1478
+ return session;
1479
+ }
1480
+ /**
1481
+ * Get a session by ID and update activity
1482
+ */
1483
+ getSession(sessionId) {
1484
+ const session = this.sessions.get(sessionId);
1485
+ if (session) {
1486
+ session.lastActivity = Date.now();
1487
+ }
1488
+ return session;
1489
+ }
1490
+ /**
1491
+ * Touch session to update activity timestamp (called on every API request)
1492
+ */
1493
+ touchSession(sessionId) {
1494
+ const session = this.sessions.get(sessionId);
1495
+ if (session) {
1496
+ session.lastActivity = Date.now();
1497
+ }
1498
+ }
1499
+ /**
1500
+ * Unregister a session (e.g., on cancel/submit)
1501
+ */
1502
+ async unregisterSession(sessionId) {
1503
+ const session = this.sessions.get(sessionId);
1504
+ if (session) {
1505
+ await session.watcher.stop();
1506
+ this.sessions.delete(sessionId);
1507
+ log(`[SessionManager] Session unregistered: ${sessionId}`);
1508
+ if (this.sessions.size === 0 && this.options.onAllSessionsGone) {
1509
+ this.options.onAllSessionsGone();
1510
+ }
1511
+ }
1512
+ }
1513
+ /**
1514
+ * Get pending changes for a session and clear them
1515
+ */
1516
+ consumePendingChanges(sessionId) {
1517
+ const session = this.sessions.get(sessionId);
1518
+ if (!session)
1519
+ return null;
1520
+ const changes = { ...session.pendingChanges };
1521
+ changes.changedFiles = new Set(session.pendingChanges.changedFiles);
1522
+ session.pendingChanges = this.createEmptyPendingChanges();
1523
+ return changes;
1317
1524
  }
1318
- closeAll() {
1319
- this.clients.forEach((client) => {
1320
- client.end();
1525
+ /**
1526
+ * Get number of active sessions
1527
+ */
1528
+ get sessionCount() {
1529
+ return this.sessions.size;
1530
+ }
1531
+ /**
1532
+ * Check if any sessions exist
1533
+ */
1534
+ get hasSessions() {
1535
+ return this.sessions.size > 0;
1536
+ }
1537
+ createWatcher(sessionId, cwd) {
1538
+ const watcher = new FileWatcher({ cwd });
1539
+ watcher.onFilesChanged(() => {
1540
+ const session = this.sessions.get(sessionId);
1541
+ if (session) {
1542
+ session.pendingChanges.filesChanged = true;
1543
+ }
1321
1544
  });
1322
- this.clients.clear();
1545
+ watcher.onFileChanged((filePath) => {
1546
+ const session = this.sessions.get(sessionId);
1547
+ if (session) {
1548
+ session.pendingChanges.changedFiles.add(filePath);
1549
+ }
1550
+ });
1551
+ watcher.onGitChanged(() => {
1552
+ const session = this.sessions.get(sessionId);
1553
+ if (session) {
1554
+ session.pendingChanges.gitChanged = true;
1555
+ }
1556
+ });
1557
+ watcher.start();
1558
+ return watcher;
1559
+ }
1560
+ createEmptyPendingChanges() {
1561
+ return {
1562
+ filesChanged: false,
1563
+ gitChanged: false,
1564
+ changedFiles: /* @__PURE__ */ new Set()
1565
+ };
1323
1566
  }
1324
- get clientCount() {
1325
- return this.clients.size;
1567
+ cleanupTimedOutSessions() {
1568
+ const now = Date.now();
1569
+ const timedOut = [];
1570
+ for (const [sessionId, session] of this.sessions) {
1571
+ if (now - session.lastActivity > this.options.sessionTimeoutMs) {
1572
+ timedOut.push(sessionId);
1573
+ }
1574
+ }
1575
+ for (const sessionId of timedOut) {
1576
+ log(`[SessionManager] Session timed out: ${sessionId}`);
1577
+ this.unregisterSession(sessionId);
1578
+ }
1326
1579
  }
1327
1580
  };
1328
1581
 
1329
1582
  // dist/flip/server/Server.js
1583
+ var log2 = (...args) => console.error(...args);
1584
+ var SERVER_ID = "csq-flip";
1330
1585
  var Server = class {
1331
- cwd;
1332
1586
  port;
1333
- constructor(cwd, port) {
1334
- this.cwd = cwd;
1587
+ options;
1588
+ app = null;
1589
+ server = null;
1590
+ sessionManager = null;
1591
+ resolveShutdown = null;
1592
+ constructor(port, options = {}) {
1335
1593
  this.port = port;
1594
+ this.options = {
1595
+ sessionTimeoutMs: options.sessionTimeoutMs ?? 3e4,
1596
+ idleTimeout: options.idleTimeout ?? 0
1597
+ };
1336
1598
  }
1599
+ /**
1600
+ * Start the server and return a promise that resolves when server shuts down
1601
+ */
1337
1602
  async run() {
1338
1603
  return new Promise((resolve2) => {
1339
- const state = {
1340
- cwd: this.cwd,
1341
- resolve: null
1342
- };
1343
- const app = express2();
1344
- app.use(cors());
1345
- app.use(express2.json());
1346
- app.locals.state = state;
1347
- const sseManager = new SSEManager();
1348
- const fileWatcher = new FileWatcher({ cwd: this.cwd });
1349
- fileWatcher.onFilesChanged(() => {
1350
- sseManager.broadcast({ type: "files-changed" });
1351
- });
1352
- fileWatcher.onFileChanged((path13) => {
1353
- sseManager.broadcast({ type: "file-changed", path: path13 });
1354
- });
1355
- fileWatcher.onGitChanged(() => {
1356
- sseManager.broadcast({ type: "git-changed" });
1604
+ this.resolveShutdown = resolve2;
1605
+ this.app = express2();
1606
+ this.app.use(cors());
1607
+ this.app.use(express2.json());
1608
+ this.sessionManager = new SessionManager({
1609
+ sessionTimeoutMs: this.options.sessionTimeoutMs,
1610
+ onAllSessionsGone: () => {
1611
+ log2("[Server] All sessions gone, shutting down...");
1612
+ this.shutdown();
1613
+ }
1357
1614
  });
1358
- fileWatcher.start();
1359
- app.use("/api/files", router);
1360
- app.use("/api/file", router2);
1361
- app.use("/api/git", router3);
1362
- app.use("/api/submit", router4);
1363
- app.use("/api/cancel", router5);
1364
- app.use("/api/events", createEventsRouter(sseManager));
1365
- app.use(createStaticRouter());
1366
- const server = http.createServer(app);
1367
- state.resolve = async (output) => {
1368
- await fileWatcher.stop();
1369
- sseManager.closeAll();
1370
- await new Promise((res) => server.close(() => res()));
1371
- resolve2(output);
1615
+ this.sessionManager.start();
1616
+ this.app.locals.sessionManager = this.sessionManager;
1617
+ this.app.use("/api/ping", createPingRouter());
1618
+ this.app.use("/api/session", createSessionRouter(this.sessionManager));
1619
+ this.app.use("/api/changes", createChangesRouter(this.sessionManager));
1620
+ this.app.use("/api/files", router);
1621
+ this.app.use("/api/file", router2);
1622
+ this.app.use("/api/git", router3);
1623
+ this.app.use("/api/submit", router4);
1624
+ this.app.use("/api/cancel", router5);
1625
+ this.app.use(createStaticRouter());
1626
+ this.server = http.createServer(this.app);
1627
+ const signalHandler = () => {
1628
+ log2("\nShutting down...");
1629
+ this.shutdown();
1372
1630
  };
1373
- server.listen(this.port, "127.0.0.1", () => {
1374
- console.error(`Server running at http://localhost:${this.port}`);
1631
+ process.on("SIGINT", signalHandler);
1632
+ process.on("SIGTERM", signalHandler);
1633
+ let idleTimer = null;
1634
+ if (this.options.idleTimeout && this.options.idleTimeout > 0) {
1635
+ idleTimer = setTimeout(() => {
1636
+ if (!this.sessionManager?.hasSessions) {
1637
+ log2("\nNo session registered, shutting down...");
1638
+ this.shutdown();
1639
+ }
1640
+ }, this.options.idleTimeout);
1641
+ }
1642
+ this.app.locals.idleTimer = idleTimer;
1643
+ this.app.locals.clearIdleTimer = () => {
1644
+ if (idleTimer) {
1645
+ clearTimeout(idleTimer);
1646
+ idleTimer = null;
1647
+ this.app.locals.idleTimer = null;
1648
+ }
1649
+ };
1650
+ this.server.listen(this.port, "127.0.0.1", () => {
1651
+ log2(`[Server] Running at http://localhost:${this.port}`);
1375
1652
  });
1376
1653
  });
1377
1654
  }
1655
+ /**
1656
+ * Shutdown the server gracefully
1657
+ */
1658
+ async shutdown() {
1659
+ if (!this.server)
1660
+ return;
1661
+ if (this.app?.locals.idleTimer) {
1662
+ clearTimeout(this.app.locals.idleTimer);
1663
+ }
1664
+ if (this.sessionManager) {
1665
+ await this.sessionManager.stop();
1666
+ }
1667
+ await new Promise((res) => {
1668
+ this.server.close(() => res());
1669
+ });
1670
+ log2("[Server] Shutdown complete");
1671
+ if (this.resolveShutdown) {
1672
+ this.resolveShutdown();
1673
+ }
1674
+ }
1675
+ /**
1676
+ * Get the session manager
1677
+ */
1678
+ getSessionManager() {
1679
+ return this.sessionManager;
1680
+ }
1378
1681
  };
1379
1682
  async function findFreePort(preferred, maxPort = 65535) {
1380
1683
  return new Promise((resolve2, reject) => {
@@ -1395,13 +1698,53 @@ async function findFreePort(preferred, maxPort = 65535) {
1395
1698
  });
1396
1699
  });
1397
1700
  }
1701
+ async function isFlipServerRunning(port, timeoutMs = 1e3) {
1702
+ return new Promise((resolve2) => {
1703
+ const req = http.request({
1704
+ hostname: "127.0.0.1",
1705
+ port,
1706
+ path: "/api/ping",
1707
+ method: "GET",
1708
+ timeout: timeoutMs
1709
+ }, (res) => {
1710
+ let data = "";
1711
+ res.on("data", (chunk) => data += chunk);
1712
+ res.on("end", () => {
1713
+ try {
1714
+ const json = JSON.parse(data);
1715
+ resolve2(json.id === SERVER_ID);
1716
+ } catch {
1717
+ resolve2(false);
1718
+ }
1719
+ });
1720
+ });
1721
+ req.on("error", () => resolve2(false));
1722
+ req.on("timeout", () => {
1723
+ req.destroy();
1724
+ resolve2(false);
1725
+ });
1726
+ req.end();
1727
+ });
1728
+ }
1729
+ async function findExistingServer(startPort, endPort) {
1730
+ for (let port = startPort; port <= endPort; port++) {
1731
+ if (await isFlipServerRunning(port)) {
1732
+ return port;
1733
+ }
1734
+ }
1735
+ return null;
1736
+ }
1398
1737
 
1399
1738
  // dist/flip/index.js
1400
1739
  import open from "open";
1401
1740
  import path9 from "path";
1741
+ import fs8 from "fs";
1742
+ import os3 from "os";
1743
+ import http2 from "http";
1402
1744
  import { execSync as execSync2 } from "child_process";
1403
1745
  var DEFAULT_PORT = 51234;
1404
- var log = (...args) => console.error(...args);
1746
+ var MAX_PORT = 51240;
1747
+ var log3 = (...args) => console.error(...args);
1405
1748
  function formatTime() {
1406
1749
  const now = /* @__PURE__ */ new Date();
1407
1750
  const hours = String(now.getHours()).padStart(2, "0");
@@ -1409,6 +1752,43 @@ function formatTime() {
1409
1752
  const secs = String(now.getSeconds()).padStart(2, "0");
1410
1753
  return `${hours}:${mins}:${secs}`;
1411
1754
  }
1755
+ function getSessionId() {
1756
+ try {
1757
+ const tty = execSync2("tty", { encoding: "utf-8" }).trim();
1758
+ return tty;
1759
+ } catch {
1760
+ return `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1761
+ }
1762
+ }
1763
+ async function registerSession(port, sessionId, cwd) {
1764
+ return new Promise((resolve2) => {
1765
+ const data = JSON.stringify({ session_id: sessionId, cwd });
1766
+ const req = http2.request({
1767
+ hostname: "127.0.0.1",
1768
+ port,
1769
+ path: "/api/session/register",
1770
+ method: "POST",
1771
+ headers: {
1772
+ "Content-Type": "application/json",
1773
+ "Content-Length": Buffer.byteLength(data)
1774
+ },
1775
+ timeout: 5e3
1776
+ }, (res) => {
1777
+ let body = "";
1778
+ res.on("data", (chunk) => body += chunk);
1779
+ res.on("end", () => {
1780
+ resolve2(res.statusCode === 200);
1781
+ });
1782
+ });
1783
+ req.on("error", () => resolve2(false));
1784
+ req.on("timeout", () => {
1785
+ req.destroy();
1786
+ resolve2(false);
1787
+ });
1788
+ req.write(data);
1789
+ req.end();
1790
+ });
1791
+ }
1412
1792
  async function runFlip(args) {
1413
1793
  let sessionId;
1414
1794
  const sessionIdx = args.indexOf("--session");
@@ -1449,58 +1829,88 @@ async function runFlip(args) {
1449
1829
  command = "oneshot";
1450
1830
  }
1451
1831
  const cwd = pathArg ? path9.resolve(pathArg) : process.cwd();
1832
+ const finalSessionId = sessionId || getSessionId();
1452
1833
  switch (command) {
1453
1834
  case "setup": {
1454
1835
  await setupHotkey();
1455
1836
  return;
1456
1837
  }
1457
1838
  case "serve": {
1458
- const port = await findFreePort(DEFAULT_PORT);
1459
- log(`Server running at http://localhost:${port}`);
1460
- log("Press Ctrl+C to stop");
1461
- log("");
1462
- log("To open browser, run: csq flip open");
1463
- log(`Or use hotkey to open: open http://localhost:${port}`);
1464
- while (true) {
1465
- const server = new Server(cwd, port);
1466
- const result = await server.run();
1467
- if (result) {
1468
- log(`[${formatTime()}] Submitted ${result.length} characters`);
1469
- } else {
1470
- log(`[${formatTime()}] Cancelled`);
1471
- }
1472
- log(`[${formatTime()}] Ready for next session...`);
1839
+ let port = await findExistingServer(DEFAULT_PORT, MAX_PORT);
1840
+ if (port) {
1841
+ log3(`[${formatTime()}] Found existing server at port ${port}`);
1842
+ } else {
1843
+ port = await findFreePort(DEFAULT_PORT, MAX_PORT);
1844
+ log3(`[${formatTime()}] Starting new server at port ${port}`);
1845
+ const server = new Server(port, { sessionTimeoutMs: 4e3 });
1846
+ server.run().catch((err) => {
1847
+ log3(`[${formatTime()}] Server error:`, err);
1848
+ });
1473
1849
  }
1850
+ log3(`[${formatTime()}] Server running at http://localhost:${port}`);
1851
+ log3("Press Ctrl+C to stop");
1852
+ log3("");
1853
+ log3("To open browser, run: csq flip open");
1854
+ log3(`Or use hotkey to open: open http://localhost:${port}`);
1855
+ await new Promise(() => {
1856
+ });
1474
1857
  }
1475
1858
  case "open": {
1476
- const url = `http://localhost:${DEFAULT_PORT}`;
1477
- log(`Opening ${url} in browser...`);
1859
+ const port = await findExistingServer(DEFAULT_PORT, MAX_PORT);
1860
+ if (!port) {
1861
+ log3("No csq flip server running.");
1862
+ log3("Start with: csq flip serve");
1863
+ log3("Or use: csq flip (one-shot mode)");
1864
+ return;
1865
+ }
1866
+ if (!await registerSession(port, finalSessionId, cwd)) {
1867
+ log3("Failed to register session with server");
1868
+ return;
1869
+ }
1870
+ const url = `http://localhost:${port}?session=${encodeURIComponent(finalSessionId)}`;
1871
+ log3(`Opening ${url} in browser...`);
1478
1872
  try {
1479
1873
  await open(url);
1480
1874
  } catch (e) {
1481
- log("Failed to open browser:", e);
1482
- log("Is the server running? Start with: csq flip serve");
1875
+ log3("Failed to open browser:", e);
1876
+ log3(`Please open ${url} manually`);
1483
1877
  }
1484
1878
  break;
1485
1879
  }
1486
1880
  case "oneshot":
1487
1881
  default: {
1488
- const port = await findFreePort(DEFAULT_PORT);
1489
- const url = sessionId ? `http://localhost:${port}?session=${sessionId}` : `http://localhost:${port}`;
1490
- log(`Opening ${url} in browser...`);
1882
+ let port = await findExistingServer(DEFAULT_PORT, MAX_PORT);
1883
+ let serverPromise = null;
1884
+ if (!port) {
1885
+ port = await findFreePort(DEFAULT_PORT, MAX_PORT);
1886
+ log3(`Starting server at http://localhost:${port}...`);
1887
+ const server = new Server(port, {
1888
+ sessionTimeoutMs: 4e3,
1889
+ // 4s (polling is 2s, so 2 missed polls = dead)
1890
+ idleTimeout: 5e3
1891
+ });
1892
+ serverPromise = server.run();
1893
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
1894
+ } else {
1895
+ log3(`Using existing server at http://localhost:${port}`);
1896
+ }
1897
+ if (!await registerSession(port, finalSessionId, cwd)) {
1898
+ log3("Failed to register session with server");
1899
+ return;
1900
+ }
1901
+ const url = `http://localhost:${port}?session=${encodeURIComponent(finalSessionId)}`;
1902
+ log3(`Opening ${url} in browser...`);
1491
1903
  try {
1492
1904
  await open(url);
1493
1905
  } catch (e) {
1494
- log("Failed to open browser:", e);
1495
- log(`Please open ${url} manually`);
1906
+ log3("Failed to open browser:", e);
1907
+ log3(`Please open ${url} manually`);
1496
1908
  }
1497
- const server = new Server(cwd, port);
1498
- const result = await server.run();
1499
- if (result) {
1500
- log(`
1501
- Submitted ${result.length} characters`);
1502
- } else {
1503
- log("\nCancelled");
1909
+ if (serverPromise) {
1910
+ log3(`Session: ${finalSessionId}`);
1911
+ log3("Waiting for session to complete... (Ctrl+C to exit)");
1912
+ await serverPromise;
1913
+ log3(`[${formatTime()}] Done`);
1504
1914
  }
1505
1915
  break;
1506
1916
  }
@@ -1512,12 +1922,12 @@ function printUsage() {
1512
1922
  console.error("Commands:");
1513
1923
  console.error(" serve [path] Start server in daemon mode (keeps running)");
1514
1924
  console.error(" open Open browser to existing server");
1515
- console.error(" setup Setup Alt+; hotkey in shell config");
1925
+ console.error(" setup Setup hotkey in shell config");
1516
1926
  console.error(" (no command) Start server + open browser (one-shot mode)");
1517
1927
  console.error("");
1518
1928
  console.error("Options:");
1519
1929
  console.error(" path Directory to serve (default: current directory)");
1520
- console.error(" --session <uuid> Session ID for paste-back tracking");
1930
+ console.error(" --session <id> Session ID for paste-back tracking (auto-generated from tty)");
1521
1931
  }
1522
1932
  async function setupHotkey() {
1523
1933
  let nodePath;
@@ -1526,8 +1936,44 @@ async function setupHotkey() {
1526
1936
  } catch {
1527
1937
  nodePath = "/usr/local/bin/node";
1528
1938
  }
1529
- const csqPath = new URL(import.meta.url).pathname;
1530
- const command = `${nodePath} ${csqPath} flip`;
1939
+ let csqPath;
1940
+ try {
1941
+ csqPath = execSync2("which csq", { encoding: "utf-8" }).trim();
1942
+ } catch {
1943
+ csqPath = new URL(import.meta.url).pathname;
1944
+ }
1945
+ const nodeDir = path9.dirname(nodePath);
1946
+ const wrapperScript = `#!/bin/bash
1947
+ # Add node to PATH (coprocess doesn't inherit shell PATH)
1948
+ export PATH="${nodeDir}:$PATH"
1949
+
1950
+ # Get current directory from iTerm2
1951
+ CWD=$(osascript -e 'tell application "iTerm2" to tell current session of current tab of current window to return variable named "session.path"' 2>/dev/null)
1952
+
1953
+ # Fallback: try to get from tty
1954
+ if [ -z "$CWD" ] || [ "$CWD" = "missing value" ]; then
1955
+ TTY=$(osascript -e 'tell application "iTerm2" to tell current session of current tab of current window to return tty' 2>/dev/null)
1956
+ if [ -n "$TTY" ]; then
1957
+ PID=$(lsof -t "$TTY" 2>/dev/null | head -1)
1958
+ if [ -n "$PID" ]; then
1959
+ CWD=$(lsof -a -d cwd -p "$PID" -Fn 2>/dev/null | grep ^n | cut -c2-)
1960
+ fi
1961
+ fi
1962
+ fi
1963
+
1964
+ # Run csq flip with cwd
1965
+ if [ -n "$CWD" ] && [ "$CWD" != "missing value" ]; then
1966
+ exec ${csqPath} flip "$CWD"
1967
+ else
1968
+ exec ${csqPath} flip
1969
+ fi
1970
+ `;
1971
+ const scriptDir = path9.join(os3.homedir(), ".config", "csq");
1972
+ const scriptPath = path9.join(scriptDir, "flip-hotkey.sh");
1973
+ if (!fs8.existsSync(scriptDir)) {
1974
+ fs8.mkdirSync(scriptDir, { recursive: true });
1975
+ }
1976
+ fs8.writeFileSync(scriptPath, wrapperScript, { mode: 493 });
1531
1977
  console.log("");
1532
1978
  console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
1533
1979
  console.log("\u2502 Flip Hotkey Setup (iTerm2) \u2502");
@@ -1539,10 +1985,10 @@ async function setupHotkey() {
1539
1985
  console.log('4. Action: "Run Coprocess"');
1540
1986
  console.log("5. Command (\uC544\uB798 \uC790\uB3D9 \uBCF5\uC0AC\uB428):");
1541
1987
  console.log("");
1542
- console.log(` ${command}`);
1988
+ console.log(` ${scriptPath}`);
1543
1989
  console.log("");
1544
1990
  try {
1545
- await copyToClipboard(command);
1991
+ await copyToClipboard(scriptPath);
1546
1992
  console.log(" (Copied to clipboard!)");
1547
1993
  } catch {
1548
1994
  }
@@ -1550,13 +1996,13 @@ async function setupHotkey() {
1550
1996
  }
1551
1997
 
1552
1998
  // dist/config.js
1553
- import * as fs8 from "fs";
1554
- import * as os3 from "os";
1999
+ import * as fs9 from "fs";
2000
+ import * as os4 from "os";
1555
2001
  import * as path10 from "path";
1556
- var GLOBAL_CONFIG_PATH = path10.join(os3.homedir(), ".code-squad", "config.json");
2002
+ var GLOBAL_CONFIG_PATH = path10.join(os4.homedir(), ".code-squad", "config.json");
1557
2003
  async function loadGlobalConfig() {
1558
2004
  try {
1559
- const content = await fs8.promises.readFile(GLOBAL_CONFIG_PATH, "utf-8");
2005
+ const content = await fs9.promises.readFile(GLOBAL_CONFIG_PATH, "utf-8");
1560
2006
  return JSON.parse(content);
1561
2007
  } catch (error) {
1562
2008
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -1587,7 +2033,7 @@ function getWorktreeCopyPatterns(config) {
1587
2033
  }
1588
2034
 
1589
2035
  // dist/fileUtils.js
1590
- import * as fs9 from "fs";
2036
+ import * as fs10 from "fs";
1591
2037
  import * as path11 from "path";
1592
2038
  import fg from "fast-glob";
1593
2039
  async function copyFilesWithPatterns(sourceRoot, destRoot, patterns) {
@@ -1624,8 +2070,8 @@ async function copySingleFile(absolutePath, sourceRoot, destRoot) {
1624
2070
  const relativePath = path11.relative(sourceRoot, absolutePath);
1625
2071
  const destPath = path11.join(destRoot, relativePath);
1626
2072
  const destDir = path11.dirname(destPath);
1627
- await fs9.promises.mkdir(destDir, { recursive: true });
1628
- await fs9.promises.copyFile(absolutePath, destPath);
2073
+ await fs10.promises.mkdir(destDir, { recursive: true });
2074
+ await fs10.promises.copyFile(absolutePath, destPath);
1629
2075
  }
1630
2076
 
1631
2077
  // dist/index.js
@@ -1681,12 +2127,12 @@ function getProjectHash(workspaceRoot) {
1681
2127
  function getSessionsPath(workspaceRoot) {
1682
2128
  const projectHash = getProjectHash(workspaceRoot);
1683
2129
  const projectName = path12.basename(workspaceRoot);
1684
- return path12.join(os4.homedir(), ".code-squad", "sessions", `${projectName}-${projectHash}.json`);
2130
+ return path12.join(os5.homedir(), ".code-squad", "sessions", `${projectName}-${projectHash}.json`);
1685
2131
  }
1686
2132
  async function loadLocalThreads(workspaceRoot) {
1687
2133
  const sessionsPath = getSessionsPath(workspaceRoot);
1688
2134
  try {
1689
- const content = await fs10.promises.readFile(sessionsPath, "utf-8");
2135
+ const content = await fs11.promises.readFile(sessionsPath, "utf-8");
1690
2136
  const data = JSON.parse(content);
1691
2137
  return data.localThreads || [];
1692
2138
  } catch {
@@ -1696,8 +2142,8 @@ async function loadLocalThreads(workspaceRoot) {
1696
2142
  async function saveLocalThreads(workspaceRoot, threads) {
1697
2143
  const sessionsPath = getSessionsPath(workspaceRoot);
1698
2144
  const dir = path12.dirname(sessionsPath);
1699
- await fs10.promises.mkdir(dir, { recursive: true });
1700
- await fs10.promises.writeFile(sessionsPath, JSON.stringify({ localThreads: threads }, null, 2));
2145
+ await fs11.promises.mkdir(dir, { recursive: true });
2146
+ await fs11.promises.writeFile(sessionsPath, JSON.stringify({ localThreads: threads }, null, 2));
1701
2147
  }
1702
2148
  async function addLocalThread(workspaceRoot, name) {
1703
2149
  const threads = await loadLocalThreads(workspaceRoot);