squads-cli 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +521 -288
  2. package/dist/auth-YW3UPFSB.js +23 -0
  3. package/dist/auth-YW3UPFSB.js.map +1 -0
  4. package/dist/autonomy-PSVZVX7A.js +105 -0
  5. package/dist/autonomy-PSVZVX7A.js.map +1 -0
  6. package/dist/chunk-67RO2HKR.js +174 -0
  7. package/dist/chunk-67RO2HKR.js.map +1 -0
  8. package/dist/chunk-7OCVIDC7.js +12 -0
  9. package/dist/chunk-7OCVIDC7.js.map +1 -0
  10. package/dist/chunk-BODLDQY7.js +452 -0
  11. package/dist/chunk-BODLDQY7.js.map +1 -0
  12. package/dist/chunk-EHQJHRIW.js +103 -0
  13. package/dist/chunk-EHQJHRIW.js.map +1 -0
  14. package/dist/chunk-FFFCFZ6A.js +121 -0
  15. package/dist/chunk-FFFCFZ6A.js.map +1 -0
  16. package/dist/chunk-FIWT2NMM.js +165 -0
  17. package/dist/chunk-FIWT2NMM.js.map +1 -0
  18. package/dist/chunk-HF4WR7RA.js +154 -0
  19. package/dist/chunk-HF4WR7RA.js.map +1 -0
  20. package/dist/chunk-J6QF4ZQX.js +230 -0
  21. package/dist/chunk-J6QF4ZQX.js.map +1 -0
  22. package/dist/chunk-LOA3KWYJ.js +294 -0
  23. package/dist/chunk-LOA3KWYJ.js.map +1 -0
  24. package/dist/chunk-M5FXNY6Y.js +384 -0
  25. package/dist/chunk-M5FXNY6Y.js.map +1 -0
  26. package/dist/chunk-QHNUMM4V.js +87 -0
  27. package/dist/chunk-QHNUMM4V.js.map +1 -0
  28. package/dist/chunk-QJ7C7CMB.js +223 -0
  29. package/dist/chunk-QJ7C7CMB.js.map +1 -0
  30. package/dist/chunk-RM6BWILN.js +74 -0
  31. package/dist/chunk-RM6BWILN.js.map +1 -0
  32. package/dist/chunk-TYFTF53O.js +613 -0
  33. package/dist/chunk-TYFTF53O.js.map +1 -0
  34. package/dist/chunk-TZXD6WFN.js +420 -0
  35. package/dist/chunk-TZXD6WFN.js.map +1 -0
  36. package/dist/chunk-WVOIY5GW.js +621 -0
  37. package/dist/chunk-WVOIY5GW.js.map +1 -0
  38. package/dist/chunk-Z2UKDBNL.js +162 -0
  39. package/dist/chunk-Z2UKDBNL.js.map +1 -0
  40. package/dist/chunk-ZTQ7ISUR.js +338 -0
  41. package/dist/chunk-ZTQ7ISUR.js.map +1 -0
  42. package/dist/cli.js +2483 -5902
  43. package/dist/cli.js.map +1 -1
  44. package/dist/context-GWPF4SEY.js +291 -0
  45. package/dist/context-GWPF4SEY.js.map +1 -0
  46. package/dist/context-feed-AJGVAR6H.js +394 -0
  47. package/dist/context-feed-AJGVAR6H.js.map +1 -0
  48. package/dist/cost-XBCDJ7XC.js +275 -0
  49. package/dist/cost-XBCDJ7XC.js.map +1 -0
  50. package/dist/create-BLFGG6PF.js +286 -0
  51. package/dist/create-BLFGG6PF.js.map +1 -0
  52. package/dist/dashboard-LGT2B2BL.js +951 -0
  53. package/dist/dashboard-LGT2B2BL.js.map +1 -0
  54. package/dist/dashboard-RMK2BOD2.js +794 -0
  55. package/dist/dashboard-RMK2BOD2.js.map +1 -0
  56. package/dist/doctor-XPUIIBHJ.js +374 -0
  57. package/dist/doctor-XPUIIBHJ.js.map +1 -0
  58. package/dist/env-config-SQEI3Y7Y.js +21 -0
  59. package/dist/env-config-SQEI3Y7Y.js.map +1 -0
  60. package/dist/exec-OUXM7JBF.js +223 -0
  61. package/dist/exec-OUXM7JBF.js.map +1 -0
  62. package/dist/feedback-KNAOG5QK.js +229 -0
  63. package/dist/feedback-KNAOG5QK.js.map +1 -0
  64. package/dist/github-UQTM5KMS.js +23 -0
  65. package/dist/github-UQTM5KMS.js.map +1 -0
  66. package/dist/goal-BVHV5573.js +168 -0
  67. package/dist/goal-BVHV5573.js.map +1 -0
  68. package/dist/health-4UXN44PF.js +218 -0
  69. package/dist/health-4UXN44PF.js.map +1 -0
  70. package/dist/history-ILH3SWHB.js +232 -0
  71. package/dist/history-ILH3SWHB.js.map +1 -0
  72. package/dist/index.d.ts +736 -8
  73. package/dist/index.js +1312 -6
  74. package/dist/index.js.map +1 -1
  75. package/dist/init-XQZ7BOGT.js +812 -0
  76. package/dist/init-XQZ7BOGT.js.map +1 -0
  77. package/dist/kpi-RQIU7WGK.js +413 -0
  78. package/dist/kpi-RQIU7WGK.js.map +1 -0
  79. package/dist/learn-OIFUVZAS.js +269 -0
  80. package/dist/learn-OIFUVZAS.js.map +1 -0
  81. package/dist/login-DXZANWZY.js +155 -0
  82. package/dist/login-DXZANWZY.js.map +1 -0
  83. package/dist/memory-T3ACCS7E.js +560 -0
  84. package/dist/memory-T3ACCS7E.js.map +1 -0
  85. package/dist/memory-VNF2VFRB.js +23 -0
  86. package/dist/memory-VNF2VFRB.js.map +1 -0
  87. package/dist/progress-DAUZMT3N.js +202 -0
  88. package/dist/progress-DAUZMT3N.js.map +1 -0
  89. package/dist/providers-3P5D2XL5.js +65 -0
  90. package/dist/providers-3P5D2XL5.js.map +1 -0
  91. package/dist/results-UECWGLTB.js +224 -0
  92. package/dist/results-UECWGLTB.js.map +1 -0
  93. package/dist/run-I6KAXU6U.js +4049 -0
  94. package/dist/run-I6KAXU6U.js.map +1 -0
  95. package/dist/session-HBU6KZOD.js +64 -0
  96. package/dist/session-HBU6KZOD.js.map +1 -0
  97. package/dist/sessions-CK25VGPL.js +333 -0
  98. package/dist/sessions-CK25VGPL.js.map +1 -0
  99. package/dist/squad-parser-DCG65BJS.js +35 -0
  100. package/dist/squad-parser-DCG65BJS.js.map +1 -0
  101. package/dist/stats-G6NAU5BD.js +334 -0
  102. package/dist/stats-G6NAU5BD.js.map +1 -0
  103. package/dist/status-AQNLDZVN.js +352 -0
  104. package/dist/status-AQNLDZVN.js.map +1 -0
  105. package/dist/sync-ZI3MHA4G.js +836 -0
  106. package/dist/sync-ZI3MHA4G.js.map +1 -0
  107. package/dist/templates/core/AGENTS.md.template +51 -0
  108. package/dist/templates/core/BUSINESS_BRIEF.md.template +29 -0
  109. package/dist/templates/core/CLAUDE.md.template +48 -0
  110. package/dist/templates/core/provider.yaml.template +5 -0
  111. package/dist/templates/first-squad/SQUAD.md.template +23 -0
  112. package/dist/templates/first-squad/lead.md.template +44 -0
  113. package/dist/templates/memory/getting-started/state.md.template +19 -0
  114. package/dist/templates/seed/BUSINESS_BRIEF.md.template +27 -0
  115. package/dist/templates/seed/CLAUDE.md.template +119 -0
  116. package/dist/templates/seed/README.md.template +42 -0
  117. package/dist/templates/seed/config/SYSTEM.md +52 -0
  118. package/dist/templates/seed/config/provider.yaml +4 -0
  119. package/dist/templates/seed/hooks/settings.json.template +31 -0
  120. package/dist/templates/seed/memory/company/directives.md +37 -0
  121. package/dist/templates/seed/memory/company/manager/state.md +16 -0
  122. package/dist/templates/seed/memory/engineering/issue-solver/state.md +12 -0
  123. package/dist/templates/seed/memory/intelligence/intel-lead/state.md +9 -0
  124. package/dist/templates/seed/memory/marketing/content-drafter/state.md +12 -0
  125. package/dist/templates/seed/memory/operations/ops-lead/state.md +12 -0
  126. package/dist/templates/seed/memory/product/lead/state.md +14 -0
  127. package/dist/templates/seed/memory/research/lead/state.md +14 -0
  128. package/dist/templates/seed/skills/gh/SKILL.md +57 -0
  129. package/dist/templates/seed/skills/squads-cli/SKILL.md +84 -0
  130. package/dist/templates/seed/squads/company/SQUAD.md +51 -0
  131. package/dist/templates/seed/squads/company/company-critic.md +49 -0
  132. package/dist/templates/seed/squads/company/company-eval.md +49 -0
  133. package/dist/templates/seed/squads/company/event-dispatcher.md +43 -0
  134. package/dist/templates/seed/squads/company/goal-tracker.md +43 -0
  135. package/dist/templates/seed/squads/company/manager.md +54 -0
  136. package/dist/templates/seed/squads/engineering/SQUAD.md +48 -0
  137. package/dist/templates/seed/squads/engineering/code-reviewer.md +57 -0
  138. package/dist/templates/seed/squads/engineering/issue-solver.md +58 -0
  139. package/dist/templates/seed/squads/engineering/test-writer.md +50 -0
  140. package/dist/templates/seed/squads/intelligence/SQUAD.md +38 -0
  141. package/dist/templates/seed/squads/intelligence/intel-critic.md +36 -0
  142. package/dist/templates/seed/squads/intelligence/intel-eval.md +31 -0
  143. package/dist/templates/seed/squads/intelligence/intel-lead.md +71 -0
  144. package/dist/templates/seed/squads/marketing/SQUAD.md +47 -0
  145. package/dist/templates/seed/squads/marketing/content-drafter.md +71 -0
  146. package/dist/templates/seed/squads/marketing/growth-analyst.md +49 -0
  147. package/dist/templates/seed/squads/marketing/social-poster.md +44 -0
  148. package/dist/templates/seed/squads/operations/SQUAD.md +45 -0
  149. package/dist/templates/seed/squads/operations/finance-tracker.md +47 -0
  150. package/dist/templates/seed/squads/operations/goal-tracker.md +48 -0
  151. package/dist/templates/seed/squads/operations/ops-lead.md +58 -0
  152. package/dist/templates/seed/squads/product/SQUAD.md +41 -0
  153. package/dist/templates/seed/squads/product/lead.md +56 -0
  154. package/dist/templates/seed/squads/product/scanner.md +50 -0
  155. package/dist/templates/seed/squads/product/worker.md +55 -0
  156. package/dist/templates/seed/squads/research/SQUAD.md +38 -0
  157. package/dist/templates/seed/squads/research/analyst.md +50 -0
  158. package/dist/templates/seed/squads/research/lead.md +52 -0
  159. package/dist/templates/seed/squads/research/synthesizer.md +59 -0
  160. package/dist/templates/skills/squads-learn/SKILL.md +86 -0
  161. package/dist/templates/skills/squads-workflow/instruction.md +70 -0
  162. package/dist/terminal-FBQFQTKZ.js +55 -0
  163. package/dist/terminal-FBQFQTKZ.js.map +1 -0
  164. package/dist/update-D7CGIZ3M.js +18 -0
  165. package/dist/update-D7CGIZ3M.js.map +1 -0
  166. package/dist/update-STU276HR.js +83 -0
  167. package/dist/update-STU276HR.js.map +1 -0
  168. package/package.json +31 -13
  169. package/templates/core/AGENTS.md.template +51 -0
  170. package/templates/core/BUSINESS_BRIEF.md.template +29 -0
  171. package/templates/core/CLAUDE.md.template +48 -0
  172. package/templates/core/provider.yaml.template +5 -0
  173. package/templates/first-squad/SQUAD.md.template +23 -0
  174. package/templates/first-squad/lead.md.template +44 -0
  175. package/templates/memory/getting-started/state.md.template +19 -0
  176. package/templates/seed/BUSINESS_BRIEF.md.template +27 -0
  177. package/templates/seed/CLAUDE.md.template +119 -0
  178. package/templates/seed/README.md.template +42 -0
  179. package/templates/seed/config/SYSTEM.md +52 -0
  180. package/templates/seed/config/provider.yaml +4 -0
  181. package/templates/seed/hooks/settings.json.template +31 -0
  182. package/templates/seed/memory/company/directives.md +37 -0
  183. package/templates/seed/memory/company/manager/state.md +16 -0
  184. package/templates/seed/memory/engineering/issue-solver/state.md +12 -0
  185. package/templates/seed/memory/intelligence/intel-lead/state.md +9 -0
  186. package/templates/seed/memory/marketing/content-drafter/state.md +12 -0
  187. package/templates/seed/memory/operations/ops-lead/state.md +12 -0
  188. package/templates/seed/memory/product/lead/state.md +14 -0
  189. package/templates/seed/memory/research/lead/state.md +14 -0
  190. package/templates/seed/skills/gh/SKILL.md +57 -0
  191. package/templates/seed/skills/squads-cli/SKILL.md +84 -0
  192. package/templates/seed/squads/company/SQUAD.md +51 -0
  193. package/templates/seed/squads/company/company-critic.md +49 -0
  194. package/templates/seed/squads/company/company-eval.md +49 -0
  195. package/templates/seed/squads/company/event-dispatcher.md +43 -0
  196. package/templates/seed/squads/company/goal-tracker.md +43 -0
  197. package/templates/seed/squads/company/manager.md +54 -0
  198. package/templates/seed/squads/engineering/SQUAD.md +48 -0
  199. package/templates/seed/squads/engineering/code-reviewer.md +57 -0
  200. package/templates/seed/squads/engineering/issue-solver.md +58 -0
  201. package/templates/seed/squads/engineering/test-writer.md +50 -0
  202. package/templates/seed/squads/intelligence/SQUAD.md +38 -0
  203. package/templates/seed/squads/intelligence/intel-critic.md +36 -0
  204. package/templates/seed/squads/intelligence/intel-eval.md +31 -0
  205. package/templates/seed/squads/intelligence/intel-lead.md +71 -0
  206. package/templates/seed/squads/marketing/SQUAD.md +47 -0
  207. package/templates/seed/squads/marketing/content-drafter.md +71 -0
  208. package/templates/seed/squads/marketing/growth-analyst.md +49 -0
  209. package/templates/seed/squads/marketing/social-poster.md +44 -0
  210. package/templates/seed/squads/operations/SQUAD.md +45 -0
  211. package/templates/seed/squads/operations/finance-tracker.md +47 -0
  212. package/templates/seed/squads/operations/goal-tracker.md +48 -0
  213. package/templates/seed/squads/operations/ops-lead.md +58 -0
  214. package/templates/seed/squads/product/SQUAD.md +41 -0
  215. package/templates/seed/squads/product/lead.md +56 -0
  216. package/templates/seed/squads/product/scanner.md +50 -0
  217. package/templates/seed/squads/product/worker.md +55 -0
  218. package/templates/seed/squads/research/SQUAD.md +38 -0
  219. package/templates/seed/squads/research/analyst.md +50 -0
  220. package/templates/seed/squads/research/lead.md +52 -0
  221. package/templates/seed/squads/research/synthesizer.md +59 -0
  222. package/templates/skills/squads-learn/SKILL.md +86 -0
  223. package/templates/skills/squads-workflow/instruction.md +70 -0
@@ -0,0 +1,452 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ __require
4
+ } from "./chunk-7OCVIDC7.js";
5
+
6
+ // src/lib/sessions.ts
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, appendFileSync, createReadStream } from "fs";
8
+ import { join, dirname } from "path";
9
+ import { randomBytes } from "crypto";
10
+ import { createInterface } from "readline";
11
+ import { execSync } from "child_process";
12
+ var AI_TOOL_PATTERNS = [
13
+ { pattern: /^claude$/, name: "claude" },
14
+ { pattern: /^cursor$/i, name: "cursor" },
15
+ { pattern: /^aider$/, name: "aider" },
16
+ { pattern: /^gemini$/i, name: "gemini" },
17
+ { pattern: /^copilot$/i, name: "copilot" },
18
+ { pattern: /^cody$/i, name: "cody" },
19
+ { pattern: /^continue$/i, name: "continue" }
20
+ ];
21
+ var STALE_THRESHOLD_MS = 5 * 60 * 1e3;
22
+ var HISTORY_FILE = "history.jsonl";
23
+ var ACTIVE_DIR = "active";
24
+ var SQUAD_DIR_MAP = {
25
+ "hq": "company",
26
+ "agents-squads-web": "website",
27
+ "company": "company",
28
+ "product": "product",
29
+ "engineering": "engineering",
30
+ "research": "research",
31
+ "intelligence": "intelligence",
32
+ "customer": "customer",
33
+ "finance": "finance",
34
+ "marketing": "marketing"
35
+ };
36
+ function findAgentsDir() {
37
+ let dir = process.cwd();
38
+ for (let i = 0; i < 5; i++) {
39
+ const agentsPath = join(dir, ".agents");
40
+ if (existsSync(agentsPath)) {
41
+ return agentsPath;
42
+ }
43
+ const parent = dirname(dir);
44
+ if (parent === dir) break;
45
+ dir = parent;
46
+ }
47
+ return null;
48
+ }
49
+ function getSessionsBaseDir() {
50
+ const agentsDir = findAgentsDir();
51
+ if (!agentsDir) return null;
52
+ const sessionsDir = join(agentsDir, "sessions");
53
+ if (!existsSync(sessionsDir)) {
54
+ mkdirSync(sessionsDir, { recursive: true });
55
+ }
56
+ return sessionsDir;
57
+ }
58
+ function getSessionsDir() {
59
+ const baseDir = getSessionsBaseDir();
60
+ if (!baseDir) return null;
61
+ const activeDir = join(baseDir, ACTIVE_DIR);
62
+ if (!existsSync(activeDir)) {
63
+ mkdirSync(activeDir, { recursive: true });
64
+ }
65
+ return activeDir;
66
+ }
67
+ function getHistoryFilePath() {
68
+ const baseDir = getSessionsBaseDir();
69
+ if (!baseDir) return null;
70
+ return join(baseDir, HISTORY_FILE);
71
+ }
72
+ function appendEvent(event) {
73
+ const historyPath = getHistoryFilePath();
74
+ if (!historyPath) return;
75
+ try {
76
+ const line = JSON.stringify(event) + "\n";
77
+ appendFileSync(historyPath, line);
78
+ } catch {
79
+ }
80
+ }
81
+ function detectSquad(cwd = process.cwd()) {
82
+ const match = cwd.match(/agents-squads\/([^/]+)/);
83
+ if (match) {
84
+ const dir = match[1];
85
+ return SQUAD_DIR_MAP[dir] || dir;
86
+ }
87
+ return null;
88
+ }
89
+ function detectAIProcessesFast() {
90
+ const processes = [];
91
+ try {
92
+ const psOutput = execSync("ps -eo pid,tty,comm 2>/dev/null", {
93
+ encoding: "utf-8",
94
+ timeout: 2e3
95
+ // Reduced from 5s for faster CLI response
96
+ }).trim();
97
+ if (!psOutput) return [];
98
+ const lines = psOutput.split("\n").filter((line) => line.trim());
99
+ for (const line of lines) {
100
+ const parts = line.trim().split(/\s+/);
101
+ if (parts.length < 3) continue;
102
+ const pid = parseInt(parts[0], 10);
103
+ const tty = parts[1];
104
+ const comm = parts.slice(2).join(" ");
105
+ if (isNaN(pid)) continue;
106
+ let toolName = null;
107
+ for (const { pattern, name } of AI_TOOL_PATTERNS) {
108
+ if (pattern.test(comm)) {
109
+ toolName = name;
110
+ break;
111
+ }
112
+ }
113
+ if (!toolName) continue;
114
+ processes.push({
115
+ pid,
116
+ tty,
117
+ cwd: "",
118
+ // No cwd in fast mode
119
+ squad: null,
120
+ // No squad detection in fast mode
121
+ tool: toolName
122
+ });
123
+ }
124
+ } catch {
125
+ }
126
+ return processes;
127
+ }
128
+ async function getProcessCwd(pid) {
129
+ return new Promise((resolve) => {
130
+ try {
131
+ const { exec } = __require("child_process");
132
+ exec(`lsof -p ${pid} 2>/dev/null | grep cwd | awk '{print $NF}'`, {
133
+ encoding: "utf-8",
134
+ timeout: 3e3
135
+ }, (error, stdout) => {
136
+ resolve(error ? "" : stdout.trim());
137
+ });
138
+ } catch {
139
+ resolve("");
140
+ }
141
+ });
142
+ }
143
+ async function enrichProcessesWithSquad(processes) {
144
+ if (processes.length === 0) return processes;
145
+ const cwdPromises = processes.map((p) => getProcessCwd(p.pid));
146
+ const cwds = await Promise.all(cwdPromises);
147
+ return processes.map((proc, i) => ({
148
+ ...proc,
149
+ cwd: cwds[i],
150
+ squad: detectSquad(cwds[i])
151
+ }));
152
+ }
153
+ async function getLiveSessionSummaryAsync(projectRoot) {
154
+ const processes = detectAIProcessesFast();
155
+ const enrichedProcesses = await enrichProcessesWithSquad(processes);
156
+ const root = projectRoot ?? process.cwd();
157
+ const projectProcesses = enrichedProcesses.filter((p) => p.cwd && p.cwd.startsWith(root));
158
+ const bySquad = {};
159
+ const byTool = {};
160
+ for (const proc of projectProcesses) {
161
+ const squad = proc.squad || "unknown";
162
+ bySquad[squad] = (bySquad[squad] || 0) + 1;
163
+ byTool[proc.tool] = (byTool[proc.tool] || 0) + 1;
164
+ }
165
+ return {
166
+ totalSessions: projectProcesses.length,
167
+ bySquad,
168
+ squadCount: Object.keys(bySquad).length,
169
+ byTool
170
+ };
171
+ }
172
+ function generateSessionId() {
173
+ return randomBytes(8).toString("hex");
174
+ }
175
+ var currentSessionId = null;
176
+ function getSessionId() {
177
+ if (currentSessionId) return currentSessionId;
178
+ const sessionsDir = getSessionsDir();
179
+ if (sessionsDir) {
180
+ const pid = process.pid;
181
+ const files = readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
182
+ for (const file of files) {
183
+ try {
184
+ const content = readFileSync(join(sessionsDir, file), "utf-8");
185
+ const session = JSON.parse(content);
186
+ if (session.pid === pid) {
187
+ currentSessionId = session.sessionId;
188
+ return currentSessionId;
189
+ }
190
+ } catch {
191
+ }
192
+ }
193
+ }
194
+ currentSessionId = generateSessionId();
195
+ return currentSessionId;
196
+ }
197
+ function startSession(squad) {
198
+ const sessionsDir = getSessionsDir();
199
+ if (!sessionsDir) return null;
200
+ const sessionId = getSessionId();
201
+ const now = (/* @__PURE__ */ new Date()).toISOString();
202
+ const cwd = process.cwd();
203
+ const detectedSquad = squad || detectSquad(cwd);
204
+ const session = {
205
+ sessionId,
206
+ squad: detectedSquad,
207
+ startedAt: now,
208
+ lastHeartbeat: now,
209
+ cwd,
210
+ pid: process.pid
211
+ };
212
+ const sessionPath = join(sessionsDir, `${sessionId}.json`);
213
+ writeFileSync(sessionPath, JSON.stringify(session, null, 2));
214
+ appendEvent({
215
+ type: "start",
216
+ sessionId,
217
+ squad: detectedSquad,
218
+ ts: now,
219
+ cwd,
220
+ pid: process.pid
221
+ });
222
+ return session;
223
+ }
224
+ function updateHeartbeat() {
225
+ const sessionsDir = getSessionsDir();
226
+ if (!sessionsDir) return false;
227
+ const sessionId = getSessionId();
228
+ const sessionPath = join(sessionsDir, `${sessionId}.json`);
229
+ if (!existsSync(sessionPath)) {
230
+ startSession();
231
+ return true;
232
+ }
233
+ try {
234
+ const content = readFileSync(sessionPath, "utf-8");
235
+ const session = JSON.parse(content);
236
+ session.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
237
+ writeFileSync(sessionPath, JSON.stringify(session, null, 2));
238
+ return true;
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+ function stopSession() {
244
+ const sessionsDir = getSessionsDir();
245
+ if (!sessionsDir) return false;
246
+ const sessionId = getSessionId();
247
+ const sessionPath = join(sessionsDir, `${sessionId}.json`);
248
+ if (existsSync(sessionPath)) {
249
+ let squad = null;
250
+ let durationMs;
251
+ try {
252
+ const content = readFileSync(sessionPath, "utf-8");
253
+ const session = JSON.parse(content);
254
+ squad = session.squad;
255
+ durationMs = Date.now() - new Date(session.startedAt).getTime();
256
+ } catch {
257
+ }
258
+ unlinkSync(sessionPath);
259
+ currentSessionId = null;
260
+ appendEvent({
261
+ type: "stop",
262
+ sessionId,
263
+ squad,
264
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
265
+ durationMs
266
+ });
267
+ return true;
268
+ }
269
+ return false;
270
+ }
271
+ function getActiveSessions() {
272
+ const sessionsDir = getSessionsDir();
273
+ if (!sessionsDir) return [];
274
+ const now = Date.now();
275
+ const sessions = [];
276
+ try {
277
+ const files = readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
278
+ for (const file of files) {
279
+ try {
280
+ const filePath = join(sessionsDir, file);
281
+ const content = readFileSync(filePath, "utf-8");
282
+ const session = JSON.parse(content);
283
+ const lastHeartbeat = new Date(session.lastHeartbeat).getTime();
284
+ if (now - lastHeartbeat < STALE_THRESHOLD_MS) {
285
+ sessions.push(session);
286
+ }
287
+ } catch {
288
+ }
289
+ }
290
+ } catch {
291
+ }
292
+ return sessions;
293
+ }
294
+ function getSessionSummary() {
295
+ const sessions = getActiveSessions();
296
+ const bySquad = {};
297
+ for (const session of sessions) {
298
+ const squad = session.squad || "unknown";
299
+ bySquad[squad] = (bySquad[squad] || 0) + 1;
300
+ }
301
+ return {
302
+ totalSessions: sessions.length,
303
+ bySquad,
304
+ squadCount: Object.keys(bySquad).length
305
+ };
306
+ }
307
+ function cleanupStaleSessions() {
308
+ const sessionsDir = getSessionsDir();
309
+ if (!sessionsDir) return 0;
310
+ const now = Date.now();
311
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
312
+ let cleaned = 0;
313
+ try {
314
+ const files = readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
315
+ for (const file of files) {
316
+ try {
317
+ const filePath = join(sessionsDir, file);
318
+ const content = readFileSync(filePath, "utf-8");
319
+ const session = JSON.parse(content);
320
+ const lastHeartbeat = new Date(session.lastHeartbeat).getTime();
321
+ if (now - lastHeartbeat >= STALE_THRESHOLD_MS) {
322
+ const durationMs = now - new Date(session.startedAt).getTime();
323
+ unlinkSync(filePath);
324
+ cleaned++;
325
+ appendEvent({
326
+ type: "stale_cleanup",
327
+ sessionId: session.sessionId,
328
+ squad: session.squad,
329
+ ts: nowIso,
330
+ durationMs
331
+ });
332
+ }
333
+ } catch {
334
+ try {
335
+ unlinkSync(join(sessionsDir, file));
336
+ cleaned++;
337
+ } catch {
338
+ }
339
+ }
340
+ }
341
+ } catch {
342
+ }
343
+ return cleaned;
344
+ }
345
+ async function readSessionHistory(options = {}) {
346
+ const historyPath = getHistoryFilePath();
347
+ if (!historyPath || !existsSync(historyPath)) return [];
348
+ const events = [];
349
+ const sinceMs = options.since?.getTime() || 0;
350
+ const untilMs = options.until?.getTime() || Date.now();
351
+ return new Promise((resolve) => {
352
+ const rl = createInterface({
353
+ input: createReadStream(historyPath),
354
+ crlfDelay: Infinity
355
+ });
356
+ rl.on("line", (line) => {
357
+ if (!line.trim()) return;
358
+ try {
359
+ const event = JSON.parse(line);
360
+ const eventMs = new Date(event.ts).getTime();
361
+ if (eventMs < sinceMs || eventMs > untilMs) return;
362
+ if (options.squad && event.squad !== options.squad) return;
363
+ if (options.type && event.type !== options.type) return;
364
+ events.push(event);
365
+ } catch {
366
+ }
367
+ });
368
+ rl.on("close", () => {
369
+ if (options.limit && events.length > options.limit) {
370
+ resolve(events.slice(-options.limit));
371
+ } else {
372
+ resolve(events);
373
+ }
374
+ });
375
+ rl.on("error", () => {
376
+ resolve([]);
377
+ });
378
+ });
379
+ }
380
+ async function getSessionHistoryStats(options = {}) {
381
+ const events = await readSessionHistory(options);
382
+ const stats = {
383
+ totalSessions: 0,
384
+ totalDurationMs: 0,
385
+ avgDurationMs: 0,
386
+ bySquad: {},
387
+ byDate: {},
388
+ peakConcurrent: 0
389
+ };
390
+ const activeSessions = /* @__PURE__ */ new Map();
391
+ let currentConcurrent = 0;
392
+ for (const event of events) {
393
+ const squad = event.squad || "unknown";
394
+ const date = event.ts.split("T")[0];
395
+ if (event.type === "start") {
396
+ stats.totalSessions++;
397
+ stats.byDate[date] = (stats.byDate[date] || 0) + 1;
398
+ if (!stats.bySquad[squad]) {
399
+ stats.bySquad[squad] = { count: 0, durationMs: 0 };
400
+ }
401
+ stats.bySquad[squad].count++;
402
+ activeSessions.set(event.sessionId, {
403
+ squad: event.squad,
404
+ startTs: new Date(event.ts).getTime()
405
+ });
406
+ currentConcurrent++;
407
+ stats.peakConcurrent = Math.max(stats.peakConcurrent, currentConcurrent);
408
+ }
409
+ if (event.type === "stop" || event.type === "stale_cleanup") {
410
+ const duration = event.durationMs || 0;
411
+ stats.totalDurationMs += duration;
412
+ if (stats.bySquad[squad]) {
413
+ stats.bySquad[squad].durationMs += duration;
414
+ }
415
+ if (activeSessions.has(event.sessionId)) {
416
+ activeSessions.delete(event.sessionId);
417
+ currentConcurrent = Math.max(0, currentConcurrent - 1);
418
+ }
419
+ }
420
+ }
421
+ const completedSessions = events.filter((e) => e.type === "stop" || e.type === "stale_cleanup").length;
422
+ if (completedSessions > 0) {
423
+ stats.avgDurationMs = Math.round(stats.totalDurationMs / completedSessions);
424
+ }
425
+ return stats;
426
+ }
427
+ async function getRecentSessions(limit = 20) {
428
+ const events = await readSessionHistory({ limit: limit * 3 });
429
+ const sessionEvents = /* @__PURE__ */ new Map();
430
+ for (const event of events) {
431
+ if (!sessionEvents.has(event.sessionId)) {
432
+ sessionEvents.set(event.sessionId, []);
433
+ }
434
+ sessionEvents.get(event.sessionId).push(event);
435
+ }
436
+ const startEvents = events.filter((e) => e.type === "start").slice(-limit);
437
+ return startEvents.reverse();
438
+ }
439
+
440
+ export {
441
+ detectSquad,
442
+ getLiveSessionSummaryAsync,
443
+ startSession,
444
+ updateHeartbeat,
445
+ stopSession,
446
+ getActiveSessions,
447
+ getSessionSummary,
448
+ cleanupStaleSessions,
449
+ getSessionHistoryStats,
450
+ getRecentSessions
451
+ };
452
+ //# sourceMappingURL=chunk-BODLDQY7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/sessions.ts"],"sourcesContent":["/**\n * Session tracking for active Claude Code sessions\n * Provides heartbeat-based session state management\n *\n * Storage:\n * - Active sessions: .agents/sessions/active/{id}.json (quick lookup)\n * - Event history: .agents/sessions/history.jsonl (analytics)\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, appendFileSync, createReadStream } from 'fs';\nimport { join, dirname } from 'path';\nimport { randomBytes } from 'crypto';\nimport { createInterface } from 'readline';\nimport { execSync } from 'child_process';\n\nexport interface SessionState {\n sessionId: string;\n squad: string | null;\n startedAt: string;\n lastHeartbeat: string;\n cwd: string;\n pid: number;\n}\n\nexport interface SessionSummary {\n totalSessions: number;\n bySquad: Record<string, number>;\n squadCount: number;\n byTool?: Record<string, number>; // Optional: breakdown by AI tool\n}\n\n// Event types for JSONL history\nexport type SessionEventType = 'start' | 'heartbeat' | 'stop' | 'stale_cleanup';\n\nexport interface SessionEvent {\n type: SessionEventType;\n sessionId: string;\n squad: string | null;\n ts: string;\n cwd?: string;\n pid?: number;\n durationMs?: number; // For stop events\n}\n\nexport interface SessionHistoryStats {\n totalSessions: number;\n totalDurationMs: number;\n avgDurationMs: number;\n bySquad: Record<string, { count: number; durationMs: number }>;\n byDate: Record<string, number>; // YYYY-MM-DD -> count\n peakConcurrent: number;\n}\n\n// Live AI coding assistant process info\nexport interface AIProcess {\n pid: number;\n tty: string;\n cwd: string;\n squad: string | null;\n tool: string; // 'claude', 'cursor', 'aider', 'gemini', etc.\n}\n\n// Supported AI coding tools (process names to detect)\nconst AI_TOOL_PATTERNS: { pattern: RegExp; name: string }[] = [\n { pattern: /^claude$/, name: 'claude' },\n { pattern: /^cursor$/i, name: 'cursor' },\n { pattern: /^aider$/, name: 'aider' },\n { pattern: /^gemini$/i, name: 'gemini' },\n { pattern: /^copilot$/i, name: 'copilot' },\n { pattern: /^cody$/i, name: 'cody' },\n { pattern: /^continue$/i, name: 'continue' },\n];\n\n// Session is stale after 5 minutes without heartbeat\nconst STALE_THRESHOLD_MS = 5 * 60 * 1000;\n\n// History file name\nconst HISTORY_FILE = 'history.jsonl';\n\n// Active sessions subdirectory\nconst ACTIVE_DIR = 'active';\n\n// Directory mapping for squad detection\nconst SQUAD_DIR_MAP: Record<string, string> = {\n 'hq': 'company',\n 'agents-squads-web': 'website',\n 'company': 'company',\n 'product': 'product',\n 'engineering': 'engineering',\n 'research': 'research',\n 'intelligence': 'intelligence',\n 'customer': 'customer',\n 'finance': 'finance',\n 'marketing': 'marketing',\n};\n\n/**\n * Find the .agents directory (sessions live at .agents/sessions/)\n */\nexport function findAgentsDir(): string | null {\n let dir = process.cwd();\n\n for (let i = 0; i < 5; i++) {\n const agentsPath = join(dir, '.agents');\n if (existsSync(agentsPath)) {\n return agentsPath;\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n return null;\n}\n\n/**\n * Get the sessions base directory\n */\nexport function getSessionsBaseDir(): string | null {\n const agentsDir = findAgentsDir();\n if (!agentsDir) return null;\n\n const sessionsDir = join(agentsDir, 'sessions');\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n }\n return sessionsDir;\n}\n\n/**\n * Get the active sessions directory, creating it if needed\n */\nexport function getSessionsDir(): string | null {\n const baseDir = getSessionsBaseDir();\n if (!baseDir) return null;\n\n const activeDir = join(baseDir, ACTIVE_DIR);\n if (!existsSync(activeDir)) {\n mkdirSync(activeDir, { recursive: true });\n }\n return activeDir;\n}\n\n/**\n * Get the history file path\n */\nexport function getHistoryFilePath(): string | null {\n const baseDir = getSessionsBaseDir();\n if (!baseDir) return null;\n return join(baseDir, HISTORY_FILE);\n}\n\n/**\n * Append an event to the history JSONL file\n */\nfunction appendEvent(event: SessionEvent): void {\n const historyPath = getHistoryFilePath();\n if (!historyPath) return;\n\n try {\n const line = JSON.stringify(event) + '\\n';\n appendFileSync(historyPath, line);\n } catch {\n // Silently fail - history is optional\n }\n}\n\n/**\n * Detect which squad based on current working directory\n */\nexport function detectSquad(cwd: string = process.cwd()): string | null {\n // Pattern: .../agents-squads/{domain}/...\n const match = cwd.match(/agents-squads\\/([^/]+)/);\n if (match) {\n const dir = match[1];\n return SQUAD_DIR_MAP[dir] || dir;\n }\n return null;\n}\n\n/**\n * Detect running AI coding assistant processes (fast version - no lsof)\n * Returns processes immediately without cwd/squad info\n * Supports: Claude, Cursor, Aider, Gemini, Copilot, Cody, Continue\n */\nexport function detectAIProcessesFast(): AIProcess[] {\n const processes: AIProcess[] = [];\n\n try {\n // Get all processes - we'll filter for AI tools\n // Short timeout for responsiveness - this is called during dashboard render\n const psOutput = execSync('ps -eo pid,tty,comm 2>/dev/null', {\n encoding: 'utf-8',\n timeout: 2000, // Reduced from 5s for faster CLI response\n }).trim();\n\n if (!psOutput) return [];\n\n const lines = psOutput.split('\\n').filter(line => line.trim());\n\n for (const line of lines) {\n const parts = line.trim().split(/\\s+/);\n if (parts.length < 3) continue;\n\n const pid = parseInt(parts[0], 10);\n const tty = parts[1];\n const comm = parts.slice(2).join(' '); // Process name (may have spaces)\n\n if (isNaN(pid)) continue;\n\n // Check if this is an AI coding tool\n let toolName: string | null = null;\n for (const { pattern, name } of AI_TOOL_PATTERNS) {\n if (pattern.test(comm)) {\n toolName = name;\n break;\n }\n }\n\n if (!toolName) continue;\n\n processes.push({\n pid,\n tty,\n cwd: '', // No cwd in fast mode\n squad: null, // No squad detection in fast mode\n tool: toolName,\n });\n }\n } catch {\n // Process detection failed (command not found, timeout, etc.)\n // Return empty array - graceful degradation\n }\n\n return processes;\n}\n\n/**\n * Get cwd for a single process using lsof (async)\n */\nasync function getProcessCwd(pid: number): Promise<string> {\n return new Promise((resolve) => {\n try {\n const { exec } = require('child_process');\n exec(`lsof -p ${pid} 2>/dev/null | grep cwd | awk '{print $NF}'`, {\n encoding: 'utf-8',\n timeout: 3000,\n }, (error: Error | null, stdout: string) => {\n resolve(error ? '' : stdout.trim());\n });\n } catch {\n resolve('');\n }\n });\n}\n\n/**\n * Enrich processes with cwd and squad info (async, parallel lsof)\n * Call this after detectAIProcessesFast() when you need squad info\n */\nexport async function enrichProcessesWithSquad(processes: AIProcess[]): Promise<AIProcess[]> {\n if (processes.length === 0) return processes;\n\n // Run lsof for all processes in parallel\n const cwdPromises = processes.map(p => getProcessCwd(p.pid));\n const cwds = await Promise.all(cwdPromises);\n\n // Enrich each process with cwd and squad\n return processes.map((proc, i) => ({\n ...proc,\n cwd: cwds[i],\n squad: detectSquad(cwds[i]),\n }));\n}\n\n/**\n * Detect running AI coding assistant processes (full version with squad info)\n * Synchronous wrapper for backwards compatibility - calls lsof sequentially\n * For better performance, use detectAIProcessesFast() + enrichProcessesWithSquad()\n */\nexport function detectAIProcesses(): AIProcess[] {\n const processes = detectAIProcessesFast();\n\n // Synchronously enrich with cwd/squad (backwards compatible behavior)\n for (const proc of processes) {\n try {\n const lsofOutput = execSync(`lsof -p ${proc.pid} 2>/dev/null | grep cwd | awk '{print $NF}'`, {\n encoding: 'utf-8',\n timeout: 3000,\n }).trim();\n proc.cwd = lsofOutput || '';\n proc.squad = detectSquad(proc.cwd);\n } catch {\n // Keep empty cwd and null squad\n }\n }\n\n return processes;\n}\n\n// Backwards compatibility alias\nexport const detectClaudeProcesses = detectAIProcesses;\n\n/**\n * Get live session summary using real process detection (fast version)\n * Returns count immediately, squad breakdown shows 'unknown' until enriched\n */\nexport function getLiveSessionSummaryFast(): SessionSummary {\n const processes = detectAIProcessesFast();\n const bySquad: Record<string, number> = {};\n const byTool: Record<string, number> = {};\n\n for (const proc of processes) {\n const squad = proc.squad || 'unknown';\n bySquad[squad] = (bySquad[squad] || 0) + 1;\n byTool[proc.tool] = (byTool[proc.tool] || 0) + 1;\n }\n\n return {\n totalSessions: processes.length,\n bySquad,\n squadCount: Object.keys(bySquad).length,\n byTool,\n };\n}\n\n/**\n * Get live session summary with full squad detection (async, parallel lsof)\n * Use this when you need accurate squad breakdown\n */\nexport async function getLiveSessionSummaryAsync(projectRoot?: string): Promise<SessionSummary> {\n const processes = detectAIProcessesFast();\n const enrichedProcesses = await enrichProcessesWithSquad(processes);\n\n // Filter to only sessions within the current project root\n const root = projectRoot ?? process.cwd();\n const projectProcesses = enrichedProcesses.filter(p => p.cwd && p.cwd.startsWith(root));\n\n const bySquad: Record<string, number> = {};\n const byTool: Record<string, number> = {};\n\n for (const proc of projectProcesses) {\n const squad = proc.squad || 'unknown';\n bySquad[squad] = (bySquad[squad] || 0) + 1;\n byTool[proc.tool] = (byTool[proc.tool] || 0) + 1;\n }\n\n return {\n totalSessions: projectProcesses.length,\n bySquad,\n squadCount: Object.keys(bySquad).length,\n byTool,\n };\n}\n\n/**\n * Get live session summary using real process detection (synchronous, backwards compatible)\n * For better performance, use getLiveSessionSummaryFast() or getLiveSessionSummaryAsync()\n */\nexport function getLiveSessionSummary(): SessionSummary {\n const processes = detectAIProcesses();\n const bySquad: Record<string, number> = {};\n const byTool: Record<string, number> = {};\n\n for (const proc of processes) {\n const squad = proc.squad || 'unknown';\n bySquad[squad] = (bySquad[squad] || 0) + 1;\n byTool[proc.tool] = (byTool[proc.tool] || 0) + 1;\n }\n\n return {\n totalSessions: processes.length,\n bySquad,\n squadCount: Object.keys(bySquad).length,\n byTool,\n };\n}\n\n/**\n * Generate a unique session ID\n */\nfunction generateSessionId(): string {\n return randomBytes(8).toString('hex');\n}\n\n/**\n * Get current session ID from environment or generate new one\n */\nlet currentSessionId: string | null = null;\n\nexport function getSessionId(): string {\n if (currentSessionId) return currentSessionId;\n\n // Check if we have a session file for this PID\n const sessionsDir = getSessionsDir();\n if (sessionsDir) {\n const pid = process.pid;\n const files = readdirSync(sessionsDir).filter(f => f.endsWith('.json'));\n\n for (const file of files) {\n try {\n const content = readFileSync(join(sessionsDir, file), 'utf-8');\n const session = JSON.parse(content) as SessionState;\n if (session.pid === pid) {\n currentSessionId = session.sessionId;\n return currentSessionId;\n }\n } catch {\n // Ignore parse errors\n }\n }\n }\n\n // Generate new session ID\n currentSessionId = generateSessionId();\n return currentSessionId;\n}\n\n/**\n * Start a new session (write initial state file and log event)\n */\nexport function startSession(squad?: string): SessionState | null {\n const sessionsDir = getSessionsDir();\n if (!sessionsDir) return null;\n\n const sessionId = getSessionId();\n const now = new Date().toISOString();\n const cwd = process.cwd();\n const detectedSquad = squad || detectSquad(cwd);\n\n const session: SessionState = {\n sessionId,\n squad: detectedSquad,\n startedAt: now,\n lastHeartbeat: now,\n cwd,\n pid: process.pid,\n };\n\n const sessionPath = join(sessionsDir, `${sessionId}.json`);\n writeFileSync(sessionPath, JSON.stringify(session, null, 2));\n\n // Log start event to history\n appendEvent({\n type: 'start',\n sessionId,\n squad: detectedSquad,\n ts: now,\n cwd,\n pid: process.pid,\n });\n\n return session;\n}\n\n/**\n * Update heartbeat for current session\n */\nexport function updateHeartbeat(): boolean {\n const sessionsDir = getSessionsDir();\n if (!sessionsDir) return false;\n\n const sessionId = getSessionId();\n const sessionPath = join(sessionsDir, `${sessionId}.json`);\n\n if (!existsSync(sessionPath)) {\n // Session doesn't exist, start a new one\n startSession();\n return true;\n }\n\n try {\n const content = readFileSync(sessionPath, 'utf-8');\n const session = JSON.parse(content) as SessionState;\n session.lastHeartbeat = new Date().toISOString();\n writeFileSync(sessionPath, JSON.stringify(session, null, 2));\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Stop current session (remove state file and log event)\n */\nexport function stopSession(): boolean {\n const sessionsDir = getSessionsDir();\n if (!sessionsDir) return false;\n\n const sessionId = getSessionId();\n const sessionPath = join(sessionsDir, `${sessionId}.json`);\n\n if (existsSync(sessionPath)) {\n // Read session to get start time and squad for duration calculation\n let squad: string | null = null;\n let durationMs: number | undefined;\n\n try {\n const content = readFileSync(sessionPath, 'utf-8');\n const session = JSON.parse(content) as SessionState;\n squad = session.squad;\n durationMs = Date.now() - new Date(session.startedAt).getTime();\n } catch {\n // Ignore parse errors\n }\n\n unlinkSync(sessionPath);\n currentSessionId = null;\n\n // Log stop event to history\n appendEvent({\n type: 'stop',\n sessionId,\n squad,\n ts: new Date().toISOString(),\n durationMs,\n });\n\n return true;\n }\n\n return false;\n}\n\n/**\n * Get all active sessions (non-stale)\n */\nexport function getActiveSessions(): SessionState[] {\n const sessionsDir = getSessionsDir();\n if (!sessionsDir) return [];\n\n const now = Date.now();\n const sessions: SessionState[] = [];\n\n try {\n const files = readdirSync(sessionsDir).filter(f => f.endsWith('.json'));\n\n for (const file of files) {\n try {\n const filePath = join(sessionsDir, file);\n const content = readFileSync(filePath, 'utf-8');\n const session = JSON.parse(content) as SessionState;\n\n // Check if session is stale\n const lastHeartbeat = new Date(session.lastHeartbeat).getTime();\n if (now - lastHeartbeat < STALE_THRESHOLD_MS) {\n sessions.push(session);\n }\n } catch {\n // Ignore parse errors\n }\n }\n } catch {\n // Directory read error\n }\n\n return sessions;\n}\n\n/**\n * Get session summary for dashboard\n */\nexport function getSessionSummary(): SessionSummary {\n const sessions = getActiveSessions();\n const bySquad: Record<string, number> = {};\n\n for (const session of sessions) {\n const squad = session.squad || 'unknown';\n bySquad[squad] = (bySquad[squad] || 0) + 1;\n }\n\n return {\n totalSessions: sessions.length,\n bySquad,\n squadCount: Object.keys(bySquad).length,\n };\n}\n\n/**\n * Clean up stale sessions (older than threshold) and log events\n */\nexport function cleanupStaleSessions(): number {\n const sessionsDir = getSessionsDir();\n if (!sessionsDir) return 0;\n\n const now = Date.now();\n const nowIso = new Date().toISOString();\n let cleaned = 0;\n\n try {\n const files = readdirSync(sessionsDir).filter(f => f.endsWith('.json'));\n\n for (const file of files) {\n try {\n const filePath = join(sessionsDir, file);\n const content = readFileSync(filePath, 'utf-8');\n const session = JSON.parse(content) as SessionState;\n\n const lastHeartbeat = new Date(session.lastHeartbeat).getTime();\n if (now - lastHeartbeat >= STALE_THRESHOLD_MS) {\n const durationMs = now - new Date(session.startedAt).getTime();\n\n unlinkSync(filePath);\n cleaned++;\n\n // Log stale cleanup event\n appendEvent({\n type: 'stale_cleanup',\n sessionId: session.sessionId,\n squad: session.squad,\n ts: nowIso,\n durationMs,\n });\n }\n } catch {\n // If we can't parse, remove the file\n try {\n unlinkSync(join(sessionsDir, file));\n cleaned++;\n } catch {\n // Ignore\n }\n }\n }\n } catch {\n // Directory read error\n }\n\n return cleaned;\n}\n\n/**\n * Read all events from history file\n */\nexport async function readSessionHistory(options: {\n since?: Date;\n until?: Date;\n squad?: string;\n type?: SessionEventType;\n limit?: number;\n} = {}): Promise<SessionEvent[]> {\n const historyPath = getHistoryFilePath();\n if (!historyPath || !existsSync(historyPath)) return [];\n\n const events: SessionEvent[] = [];\n const sinceMs = options.since?.getTime() || 0;\n const untilMs = options.until?.getTime() || Date.now();\n\n return new Promise((resolve) => {\n const rl = createInterface({\n input: createReadStream(historyPath),\n crlfDelay: Infinity,\n });\n\n rl.on('line', (line) => {\n if (!line.trim()) return;\n\n try {\n const event = JSON.parse(line) as SessionEvent;\n const eventMs = new Date(event.ts).getTime();\n\n // Apply filters\n if (eventMs < sinceMs || eventMs > untilMs) return;\n if (options.squad && event.squad !== options.squad) return;\n if (options.type && event.type !== options.type) return;\n\n events.push(event);\n } catch {\n // Ignore parse errors\n }\n });\n\n rl.on('close', () => {\n // Apply limit (from end, most recent first)\n if (options.limit && events.length > options.limit) {\n resolve(events.slice(-options.limit));\n } else {\n resolve(events);\n }\n });\n\n rl.on('error', () => {\n resolve([]);\n });\n });\n}\n\n/**\n * Get session history statistics\n */\nexport async function getSessionHistoryStats(options: {\n since?: Date;\n until?: Date;\n squad?: string;\n} = {}): Promise<SessionHistoryStats> {\n const events = await readSessionHistory(options);\n\n const stats: SessionHistoryStats = {\n totalSessions: 0,\n totalDurationMs: 0,\n avgDurationMs: 0,\n bySquad: {},\n byDate: {},\n peakConcurrent: 0,\n };\n\n // Track active sessions for peak concurrent calculation\n const activeSessions = new Map<string, { squad: string | null; startTs: number }>();\n let currentConcurrent = 0;\n\n for (const event of events) {\n const squad = event.squad || 'unknown';\n const date = event.ts.split('T')[0]; // YYYY-MM-DD\n\n if (event.type === 'start') {\n stats.totalSessions++;\n stats.byDate[date] = (stats.byDate[date] || 0) + 1;\n\n if (!stats.bySquad[squad]) {\n stats.bySquad[squad] = { count: 0, durationMs: 0 };\n }\n stats.bySquad[squad].count++;\n\n // Track for concurrent calculation\n activeSessions.set(event.sessionId, {\n squad: event.squad,\n startTs: new Date(event.ts).getTime(),\n });\n currentConcurrent++;\n stats.peakConcurrent = Math.max(stats.peakConcurrent, currentConcurrent);\n }\n\n if (event.type === 'stop' || event.type === 'stale_cleanup') {\n const duration = event.durationMs || 0;\n stats.totalDurationMs += duration;\n\n if (stats.bySquad[squad]) {\n stats.bySquad[squad].durationMs += duration;\n }\n\n // Remove from concurrent tracking\n if (activeSessions.has(event.sessionId)) {\n activeSessions.delete(event.sessionId);\n currentConcurrent = Math.max(0, currentConcurrent - 1);\n }\n }\n }\n\n // Calculate average duration\n const completedSessions = events.filter(e => e.type === 'stop' || e.type === 'stale_cleanup').length;\n if (completedSessions > 0) {\n stats.avgDurationMs = Math.round(stats.totalDurationMs / completedSessions);\n }\n\n return stats;\n}\n\n/**\n * Get recent session events (for display)\n */\nexport async function getRecentSessions(limit: number = 20): Promise<SessionEvent[]> {\n const events = await readSessionHistory({ limit: limit * 3 }); // Get more to filter\n\n // Group by session and get start/stop pairs\n const sessionEvents = new Map<string, SessionEvent[]>();\n for (const event of events) {\n if (!sessionEvents.has(event.sessionId)) {\n sessionEvents.set(event.sessionId, []);\n }\n sessionEvents.get(event.sessionId)!.push(event);\n }\n\n // Get the most recent start events\n const startEvents = events\n .filter(e => e.type === 'start')\n .slice(-limit);\n\n return startEvents.reverse(); // Most recent first\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/env-config.ts
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ var CONFIG_DIR = join(homedir(), ".squads");
8
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
9
+ var DEFAULT_CONFIG = {
10
+ current: "prod",
11
+ environments: {
12
+ local: {
13
+ api_url: process.env.SQUADS_API_URL || "",
14
+ admin_api_url: process.env.SQUADS_ADMIN_API_URL || "",
15
+ console_url: process.env.SQUADS_CONSOLE_URL || "",
16
+ bridge_url: process.env.SQUADS_BRIDGE_URL || "",
17
+ database_url: process.env.SQUADS_DATABASE_URL || "",
18
+ redis_url: process.env.REDIS_URL || "",
19
+ execution: "local"
20
+ },
21
+ staging: {
22
+ api_url: "https://api-staging.agents-squads.com",
23
+ admin_api_url: "https://api-staging.agents-squads.com",
24
+ console_url: "https://console-staging.agents-squads.com",
25
+ bridge_url: "",
26
+ database_url: "",
27
+ redis_url: "",
28
+ execution: "cloud"
29
+ },
30
+ prod: {
31
+ api_url: "https://api.agents-squads.com",
32
+ admin_api_url: "https://api.agents-squads.com",
33
+ console_url: "https://console.agents-squads.com",
34
+ bridge_url: "",
35
+ database_url: "",
36
+ redis_url: "",
37
+ execution: "cloud"
38
+ }
39
+ }
40
+ };
41
+ function loadConfig() {
42
+ if (!existsSync(CONFIG_PATH)) {
43
+ saveConfig(DEFAULT_CONFIG);
44
+ return DEFAULT_CONFIG;
45
+ }
46
+ try {
47
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
48
+ const parsed = JSON.parse(raw);
49
+ return {
50
+ current: parsed.current || "local",
51
+ environments: {
52
+ ...DEFAULT_CONFIG.environments,
53
+ ...parsed.environments
54
+ }
55
+ };
56
+ } catch {
57
+ return DEFAULT_CONFIG;
58
+ }
59
+ }
60
+ function saveConfig(config) {
61
+ if (!existsSync(CONFIG_DIR)) {
62
+ mkdirSync(CONFIG_DIR, { recursive: true });
63
+ }
64
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
65
+ }
66
+ function getEnv() {
67
+ const config = loadConfig();
68
+ const envName = process.env.SQUADS_ENV || config.current;
69
+ const env = config.environments[envName] || config.environments.local;
70
+ return {
71
+ api_url: process.env.SQUADS_API_URL || env.api_url,
72
+ admin_api_url: process.env.SQUADS_ADMIN_API_URL || env.admin_api_url,
73
+ console_url: process.env.SQUADS_CONSOLE_URL || env.console_url,
74
+ bridge_url: process.env.SQUADS_BRIDGE_URL || env.bridge_url,
75
+ database_url: process.env.SQUADS_DATABASE_URL || env.database_url,
76
+ redis_url: process.env.REDIS_URL || env.redis_url,
77
+ execution: env.execution
78
+ };
79
+ }
80
+ function getEnvName() {
81
+ const config = loadConfig();
82
+ return process.env.SQUADS_ENV || config.current;
83
+ }
84
+ function getApiUrl() {
85
+ return getEnv().api_url;
86
+ }
87
+ function getBridgeUrl() {
88
+ return getEnv().bridge_url;
89
+ }
90
+ function getConsoleUrl() {
91
+ return getEnv().console_url;
92
+ }
93
+
94
+ export {
95
+ loadConfig,
96
+ saveConfig,
97
+ getEnv,
98
+ getEnvName,
99
+ getApiUrl,
100
+ getBridgeUrl,
101
+ getConsoleUrl
102
+ };
103
+ //# sourceMappingURL=chunk-EHQJHRIW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/env-config.ts"],"sourcesContent":["/**\n * Environment configuration — single source of truth for all service URLs.\n *\n * Usage:\n * squads config use local Switch to local environment\n * squads config use staging Switch to staging\n * squads config use prod Switch to production\n * squads config show Show current config\n *\n * Config stored at ~/.squads/config.json\n * Env vars override config values (for CI/CD and one-off overrides).\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface EnvironmentConfig {\n api_url: string;\n admin_api_url: string;\n console_url: string;\n bridge_url: string;\n database_url: string;\n redis_url: string;\n execution: 'local' | 'cloud';\n}\n\nexport interface SquadsConfig {\n current: string;\n environments: Record<string, EnvironmentConfig>;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst CONFIG_DIR = join(homedir(), '.squads');\nconst CONFIG_PATH = join(CONFIG_DIR, 'config.json');\n\nconst DEFAULT_CONFIG: SquadsConfig = {\n current: 'prod',\n environments: {\n local: {\n api_url: process.env.SQUADS_API_URL || '',\n admin_api_url: process.env.SQUADS_ADMIN_API_URL || '',\n console_url: process.env.SQUADS_CONSOLE_URL || '',\n bridge_url: process.env.SQUADS_BRIDGE_URL || '',\n database_url: process.env.SQUADS_DATABASE_URL || '',\n redis_url: process.env.REDIS_URL || '',\n execution: 'local',\n },\n staging: {\n api_url: 'https://api-staging.agents-squads.com',\n admin_api_url: 'https://api-staging.agents-squads.com',\n console_url: 'https://console-staging.agents-squads.com',\n bridge_url: '',\n database_url: '',\n redis_url: '',\n execution: 'cloud',\n },\n prod: {\n api_url: 'https://api.agents-squads.com',\n admin_api_url: 'https://api.agents-squads.com',\n console_url: 'https://console.agents-squads.com',\n bridge_url: '',\n database_url: '',\n redis_url: '',\n execution: 'cloud',\n },\n },\n};\n\n// ---------------------------------------------------------------------------\n// Load / Save\n// ---------------------------------------------------------------------------\n\nexport function loadConfig(): SquadsConfig {\n if (!existsSync(CONFIG_PATH)) {\n saveConfig(DEFAULT_CONFIG);\n return DEFAULT_CONFIG;\n }\n\n try {\n const raw = readFileSync(CONFIG_PATH, 'utf-8');\n const parsed = JSON.parse(raw) as Partial<SquadsConfig>;\n return {\n current: parsed.current || 'local',\n environments: {\n ...DEFAULT_CONFIG.environments,\n ...parsed.environments,\n },\n };\n } catch {\n return DEFAULT_CONFIG;\n }\n}\n\nexport function saveConfig(config: SquadsConfig): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n }\n writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\\n');\n}\n\n// ---------------------------------------------------------------------------\n// Resolve — env vars override config\n// ---------------------------------------------------------------------------\n\nexport function getEnv(): EnvironmentConfig {\n const config = loadConfig();\n const envName = process.env.SQUADS_ENV || config.current;\n const env = config.environments[envName] || config.environments.local;\n\n return {\n api_url: process.env.SQUADS_API_URL || env.api_url,\n admin_api_url: process.env.SQUADS_ADMIN_API_URL || env.admin_api_url,\n console_url: process.env.SQUADS_CONSOLE_URL || env.console_url,\n bridge_url: process.env.SQUADS_BRIDGE_URL || env.bridge_url,\n database_url: process.env.SQUADS_DATABASE_URL || env.database_url,\n redis_url: process.env.REDIS_URL || env.redis_url,\n execution: env.execution,\n };\n}\n\nexport function getEnvName(): string {\n const config = loadConfig();\n return process.env.SQUADS_ENV || config.current;\n}\n\nexport function getApiUrl(): string {\n return getEnv().api_url;\n}\n\nexport function getBridgeUrl(): string {\n return getEnv().bridge_url;\n}\n\nexport function getConsoleUrl(): string {\n return getEnv().console_url;\n}\n"],"mappings":";;;AAaA,SAAS,YAAY,cAAc,eAAe,iBAAiB;AACnE,SAAS,YAAY;AACrB,SAAS,eAAe;AAyBxB,IAAM,aAAa,KAAK,QAAQ,GAAG,SAAS;AAC5C,IAAM,cAAc,KAAK,YAAY,aAAa;AAElD,IAAM,iBAA+B;AAAA,EACnC,SAAS;AAAA,EACT,cAAc;AAAA,IACZ,OAAO;AAAA,MACL,SAAS,QAAQ,IAAI,kBAAkB;AAAA,MACvC,eAAe,QAAQ,IAAI,wBAAwB;AAAA,MACnD,aAAa,QAAQ,IAAI,sBAAsB;AAAA,MAC/C,YAAY,QAAQ,IAAI,qBAAqB;AAAA,MAC7C,cAAc,QAAQ,IAAI,uBAAuB;AAAA,MACjD,WAAW,QAAQ,IAAI,aAAa;AAAA,MACpC,WAAW;AAAA,IACb;AAAA,IACA,SAAS;AAAA,MACP,SAAS;AAAA,MACT,eAAe;AAAA,MACf,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,IACA,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,eAAe;AAAA,MACf,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,EACF;AACF;AAMO,SAAS,aAA2B;AACzC,MAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,eAAW,cAAc;AACzB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,MAAM,aAAa,aAAa,OAAO;AAC7C,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO;AAAA,MACL,SAAS,OAAO,WAAW;AAAA,MAC3B,cAAc;AAAA,QACZ,GAAG,eAAe;AAAA,QAClB,GAAG,OAAO;AAAA,MACZ;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,WAAW,QAA4B;AACrD,MAAI,CAAC,WAAW,UAAU,GAAG;AAC3B,cAAU,YAAY,EAAE,WAAW,KAAK,CAAC;AAAA,EAC3C;AACA,gBAAc,aAAa,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,IAAI;AACnE;AAMO,SAAS,SAA4B;AAC1C,QAAM,SAAS,WAAW;AAC1B,QAAM,UAAU,QAAQ,IAAI,cAAc,OAAO;AACjD,QAAM,MAAM,OAAO,aAAa,OAAO,KAAK,OAAO,aAAa;AAEhE,SAAO;AAAA,IACL,SAAS,QAAQ,IAAI,kBAAkB,IAAI;AAAA,IAC3C,eAAe,QAAQ,IAAI,wBAAwB,IAAI;AAAA,IACvD,aAAa,QAAQ,IAAI,sBAAsB,IAAI;AAAA,IACnD,YAAY,QAAQ,IAAI,qBAAqB,IAAI;AAAA,IACjD,cAAc,QAAQ,IAAI,uBAAuB,IAAI;AAAA,IACrD,WAAW,QAAQ,IAAI,aAAa,IAAI;AAAA,IACxC,WAAW,IAAI;AAAA,EACjB;AACF;AAEO,SAAS,aAAqB;AACnC,QAAM,SAAS,WAAW;AAC1B,SAAO,QAAQ,IAAI,cAAc,OAAO;AAC1C;AAEO,SAAS,YAAoB;AAClC,SAAO,OAAO,EAAE;AAClB;AAEO,SAAS,eAAuB;AACrC,SAAO,OAAO,EAAE;AAClB;AAEO,SAAS,gBAAwB;AACtC,SAAO,OAAO,EAAE;AAClB;","names":[]}