panopticon-cli 0.3.2 → 0.3.4

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/cli/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  restoreBackup,
17
17
  saveConfig,
18
18
  syncHooks
19
- } from "../chunk-B2JBBOJN.js";
19
+ } from "../chunk-C6A7S65K.js";
20
20
  import {
21
21
  PROJECTS_CONFIG_FILE,
22
22
  getProject,
@@ -26,7 +26,7 @@ import {
26
26
  registerProject,
27
27
  resolveProjectFromIssue,
28
28
  unregisterProject
29
- } from "../chunk-PSJRCUOA.js";
29
+ } from "../chunk-BH6BR26M.js";
30
30
  import {
31
31
  AGENTS_DIR,
32
32
  CERTS_DIR,
@@ -41,1149 +41,999 @@ import {
41
41
  SOURCE_TRAEFIK_TEMPLATES,
42
42
  SYNC_TARGETS,
43
43
  TRAEFIK_CERTS_DIR,
44
- TRAEFIK_DIR,
45
- __commonJS,
46
- __esm,
47
- __export,
48
- __require,
49
- __toCommonJS,
50
- init_esm_shims,
51
- init_paths
52
- } from "../chunk-SG7O6I7R.js";
44
+ TRAEFIK_DIR
45
+ } from "../chunk-P5TQ5C3J.js";
53
46
 
54
- // src/lib/tmux.ts
55
- import { execSync } from "child_process";
56
- function listSessions() {
57
- try {
58
- const output = execSync('tmux list-sessions -F "#{session_name}|#{session_created}|#{session_attached}|#{session_windows}"', {
59
- encoding: "utf8"
60
- });
61
- return output.trim().split("\n").filter(Boolean).map((line) => {
62
- const [name, created, attached, windows] = line.split("|");
63
- return {
64
- name,
65
- created: new Date(parseInt(created) * 1e3),
66
- attached: attached === "1",
67
- windows: parseInt(windows)
68
- };
69
- });
70
- } catch {
71
- return [];
72
- }
73
- }
74
- function sessionExists(name) {
75
- try {
76
- execSync(`tmux has-session -t ${name} 2>/dev/null`);
77
- return true;
78
- } catch {
79
- return false;
47
+ // src/cli/index.ts
48
+ import { Command } from "commander";
49
+ import chalk33 from "chalk";
50
+
51
+ // src/cli/commands/init.ts
52
+ import { existsSync, mkdirSync, readdirSync, cpSync } from "fs";
53
+ import { join, dirname } from "path";
54
+ import { fileURLToPath } from "url";
55
+ import chalk from "chalk";
56
+ import ora from "ora";
57
+ var __filename = fileURLToPath(import.meta.url);
58
+ var __dirname = dirname(__filename);
59
+ var PACKAGE_ROOT = join(__dirname, "..", "..");
60
+ var BUNDLED_SKILLS_DIR = join(PACKAGE_ROOT, "skills");
61
+ var BUNDLED_AGENTS_DIR = join(PACKAGE_ROOT, "agents");
62
+ function copyBundledSkills() {
63
+ if (!existsSync(BUNDLED_SKILLS_DIR)) {
64
+ return 0;
80
65
  }
81
- }
82
- function createSession(name, cwd, initialCommand, options) {
83
- const escapedCwd = cwd.replace(/"/g, '\\"');
84
- let envFlags = "";
85
- if (options?.env) {
86
- for (const [key, value] of Object.entries(options.env)) {
87
- envFlags += ` -e ${key}="${value.replace(/"/g, '\\"')}"`;
88
- }
66
+ if (!existsSync(SKILLS_DIR)) {
67
+ mkdirSync(SKILLS_DIR, { recursive: true });
89
68
  }
90
- if (initialCommand && (initialCommand.includes("`") || initialCommand.includes("\n") || initialCommand.length > 500)) {
91
- execSync(`tmux new-session -d -s ${name} -c "${escapedCwd}"${envFlags}`);
92
- execSync("sleep 0.5");
93
- const tmpFile = `/tmp/pan-cmd-${name}.sh`;
94
- const fs = __require("fs");
95
- fs.writeFileSync(tmpFile, initialCommand);
96
- fs.chmodSync(tmpFile, "755");
97
- execSync(`tmux send-keys -t ${name} "bash ${tmpFile}" Enter`);
98
- } else if (initialCommand) {
99
- const cmd = `tmux new-session -d -s ${name} -c "${escapedCwd}"${envFlags} "${initialCommand.replace(/"/g, '\\"')}"`;
100
- execSync(cmd);
101
- } else {
102
- execSync(`tmux new-session -d -s ${name} -c "${escapedCwd}"${envFlags}`);
69
+ const skills = readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
70
+ let copied = 0;
71
+ for (const skill of skills) {
72
+ const sourcePath = join(BUNDLED_SKILLS_DIR, skill.name);
73
+ const targetPath = join(SKILLS_DIR, skill.name);
74
+ cpSync(sourcePath, targetPath, { recursive: true });
75
+ copied++;
103
76
  }
77
+ return copied;
104
78
  }
105
- function killSession(name) {
106
- execSync(`tmux kill-session -t ${name}`);
107
- }
108
- function sendKeys(sessionName, keys) {
109
- const escapedKeys = keys.replace(/"/g, '\\"');
110
- execSync(`tmux send-keys -t ${sessionName} "${escapedKeys}"`);
111
- execSync(`tmux send-keys -t ${sessionName} Enter`);
112
- }
113
- function getAgentSessions() {
114
- return listSessions().filter((s) => s.name.startsWith("agent-"));
115
- }
116
- var init_tmux = __esm({
117
- "src/lib/tmux.ts"() {
118
- "use strict";
119
- init_esm_shims();
79
+ function copyBundledAgents() {
80
+ if (!existsSync(BUNDLED_AGENTS_DIR)) {
81
+ return 0;
120
82
  }
121
- });
122
-
123
- // src/lib/hooks.ts
124
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync, readdirSync as readdirSync3, unlinkSync } from "fs";
125
- import { join as join3 } from "path";
126
- function getHookDir(agentId) {
127
- return join3(AGENTS_DIR, agentId);
128
- }
129
- function getHookFile(agentId) {
130
- return join3(getHookDir(agentId), "hook.json");
131
- }
132
- function getMailDir(agentId) {
133
- return join3(getHookDir(agentId), "mail");
134
- }
135
- function initHook(agentId) {
136
- const hookDir = getHookDir(agentId);
137
- const mailDir = getMailDir(agentId);
138
- mkdirSync2(hookDir, { recursive: true });
139
- mkdirSync2(mailDir, { recursive: true });
140
- const hookFile = getHookFile(agentId);
141
- if (!existsSync3(hookFile)) {
142
- const hook = {
143
- agentId,
144
- items: []
145
- };
146
- writeFileSync(hookFile, JSON.stringify(hook, null, 2));
83
+ if (!existsSync(AGENTS_DIR)) {
84
+ mkdirSync(AGENTS_DIR, { recursive: true });
85
+ }
86
+ const agents = readdirSync(BUNDLED_AGENTS_DIR, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md"));
87
+ let copied = 0;
88
+ for (const agent of agents) {
89
+ const sourcePath = join(BUNDLED_AGENTS_DIR, agent.name);
90
+ const targetPath = join(AGENTS_DIR, agent.name);
91
+ cpSync(sourcePath, targetPath);
92
+ copied++;
147
93
  }
94
+ return copied;
148
95
  }
149
- function getHook(agentId) {
150
- const hookFile = getHookFile(agentId);
151
- if (!existsSync3(hookFile)) {
152
- return null;
96
+ async function initCommand() {
97
+ const spinner = ora("Initializing Panopticon...").start();
98
+ if (existsSync(CONFIG_FILE)) {
99
+ spinner.info("Panopticon already initialized");
100
+ console.log(chalk.dim(` Config: ${CONFIG_FILE}`));
101
+ console.log(chalk.dim(` Home: ${PANOPTICON_HOME}`));
102
+ console.log(chalk.dim(" Run `pan sync` to update skills"));
103
+ return;
153
104
  }
154
105
  try {
155
- const content = readFileSync2(hookFile, "utf-8");
156
- return JSON.parse(content);
157
- } catch {
158
- return null;
159
- }
160
- }
161
- function pushToHook(agentId, item) {
162
- initHook(agentId);
163
- const hook = getHook(agentId) || { agentId, items: [] };
164
- const newItem = {
165
- ...item,
166
- id: `hook-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
167
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
168
- };
169
- hook.items.push(newItem);
170
- writeFileSync(getHookFile(agentId), JSON.stringify(hook, null, 2));
171
- return newItem;
172
- }
173
- function checkHook(agentId) {
174
- const hook = getHook(agentId);
175
- if (!hook || hook.items.length === 0) {
176
- const mailDir = getMailDir(agentId);
177
- if (existsSync3(mailDir)) {
178
- const mails = readdirSync3(mailDir).filter((f) => f.endsWith(".json"));
179
- if (mails.length > 0) {
180
- const mailItems = mails.map((file) => {
181
- try {
182
- const content = readFileSync2(join3(mailDir, file), "utf-8");
183
- return JSON.parse(content);
184
- } catch {
185
- return null;
186
- }
187
- }).filter(Boolean);
188
- return {
189
- hasWork: mailItems.length > 0,
190
- urgentCount: mailItems.filter((i) => i.priority === "urgent").length,
191
- items: mailItems
192
- };
106
+ for (const dir of INIT_DIRS) {
107
+ if (!existsSync(dir)) {
108
+ mkdirSync(dir, { recursive: true });
193
109
  }
194
110
  }
195
- return { hasWork: false, urgentCount: 0, items: [] };
196
- }
197
- const now = /* @__PURE__ */ new Date();
198
- const activeItems = hook.items.filter((item) => {
199
- if (item.expiresAt) {
200
- return new Date(item.expiresAt) > now;
111
+ spinner.text = "Created directories...";
112
+ const config = getDefaultConfig();
113
+ saveConfig(config);
114
+ spinner.text = "Created config...";
115
+ spinner.text = "Installing bundled skills...";
116
+ const skillsCopied = copyBundledSkills();
117
+ spinner.text = "Installing bundled agents...";
118
+ const agentsCopied = copyBundledAgents();
119
+ const shell = detectShell();
120
+ const rcFile = getShellRcFile(shell);
121
+ if (rcFile && existsSync(rcFile)) {
122
+ addAlias(rcFile);
123
+ spinner.succeed("Panopticon initialized!");
124
+ console.log("");
125
+ console.log(chalk.green("\u2713") + " Created " + chalk.cyan(PANOPTICON_HOME));
126
+ console.log(chalk.green("\u2713") + " Created " + chalk.cyan(CONFIG_FILE));
127
+ if (skillsCopied > 0) {
128
+ console.log(chalk.green("\u2713") + ` Installed ${skillsCopied} bundled skills`);
129
+ }
130
+ if (agentsCopied > 0) {
131
+ console.log(chalk.green("\u2713") + ` Installed ${agentsCopied} bundled agents`);
132
+ }
133
+ console.log(chalk.green("\u2713") + " " + getAliasInstructions(shell));
134
+ } else {
135
+ spinner.succeed("Panopticon initialized!");
136
+ console.log("");
137
+ console.log(chalk.green("\u2713") + " Created " + chalk.cyan(PANOPTICON_HOME));
138
+ console.log(chalk.green("\u2713") + " Created " + chalk.cyan(CONFIG_FILE));
139
+ if (skillsCopied > 0) {
140
+ console.log(chalk.green("\u2713") + ` Installed ${skillsCopied} bundled skills`);
141
+ }
142
+ if (agentsCopied > 0) {
143
+ console.log(chalk.green("\u2713") + ` Installed ${agentsCopied} bundled agents`);
144
+ }
145
+ console.log(chalk.yellow("!") + " Could not detect shell. Add alias manually:");
146
+ console.log(chalk.dim(' alias pan="panopticon"'));
201
147
  }
202
- return true;
203
- });
204
- const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
205
- activeItems.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
206
- return {
207
- hasWork: activeItems.length > 0,
208
- urgentCount: activeItems.filter((i) => i.priority === "urgent").length,
209
- items: activeItems
210
- };
211
- }
212
- function popFromHook(agentId, itemId) {
213
- const hook = getHook(agentId);
214
- if (!hook) return false;
215
- const index = hook.items.findIndex((i) => i.id === itemId);
216
- if (index === -1) return false;
217
- hook.items.splice(index, 1);
218
- hook.lastChecked = (/* @__PURE__ */ new Date()).toISOString();
219
- writeFileSync(getHookFile(agentId), JSON.stringify(hook, null, 2));
220
- return true;
221
- }
222
- function clearHook(agentId) {
223
- const hook = getHook(agentId);
224
- if (!hook) return;
225
- hook.items = [];
226
- hook.lastChecked = (/* @__PURE__ */ new Date()).toISOString();
227
- writeFileSync(getHookFile(agentId), JSON.stringify(hook, null, 2));
148
+ console.log("");
149
+ console.log("Next steps:");
150
+ console.log(chalk.dim(" 1. Run: pan sync"));
151
+ console.log(chalk.dim(" 2. Start dashboard: pan up"));
152
+ } catch (error) {
153
+ spinner.fail("Failed to initialize");
154
+ console.error(chalk.red(error.message));
155
+ process.exit(1);
156
+ }
228
157
  }
229
- function sendMail(toAgentId, from, message, priority = "normal") {
230
- initHook(toAgentId);
231
- const mailDir = getMailDir(toAgentId);
232
- const mailItem = {
233
- id: `mail-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
234
- type: "message",
235
- priority,
236
- source: from,
237
- payload: { message },
238
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
239
- };
240
- writeFileSync(
241
- join3(mailDir, `${mailItem.id}.json`),
242
- JSON.stringify(mailItem, null, 2)
243
- );
244
- }
245
- function generateGUPPPrompt(agentId) {
246
- const { hasWork, urgentCount, items } = checkHook(agentId);
247
- if (!hasWork) return null;
248
- const lines = [
249
- "# GUPP: Work Found on Your Hook",
250
- "",
251
- '> "If there is work on your Hook, YOU MUST RUN IT."',
252
- ""
253
- ];
254
- if (urgentCount > 0) {
255
- lines.push(`\u26A0\uFE0F **${urgentCount} URGENT item(s) require immediate attention**`);
256
- lines.push("");
158
+
159
+ // src/cli/commands/sync.ts
160
+ import chalk2 from "chalk";
161
+ import ora2 from "ora";
162
+ async function syncCommand(options) {
163
+ const config = loadConfig();
164
+ const targets = config.sync?.targets;
165
+ if (!targets || !Array.isArray(targets) || targets.length === 0) {
166
+ console.log(chalk2.yellow("No sync targets configured."));
167
+ console.log(chalk2.dim("Edit ~/.panopticon/config.toml to add targets to the [sync] section."));
168
+ console.log(chalk2.dim('Example: targets = ["claude-code", "cursor"]'));
169
+ return;
257
170
  }
258
- lines.push(`## Pending Work Items (${items.length})`);
259
- lines.push("");
260
- for (const item of items) {
261
- const priorityEmoji = {
262
- urgent: "\u{1F534}",
263
- high: "\u{1F7E0}",
264
- normal: "\u{1F7E2}",
265
- low: "\u26AA"
266
- }[item.priority];
267
- lines.push(`### ${priorityEmoji} ${item.type.toUpperCase()}: ${item.id}`);
268
- lines.push(`- Source: ${item.source}`);
269
- lines.push(`- Created: ${item.createdAt}`);
270
- if (item.payload.issueId) {
271
- lines.push(`- Issue: ${item.payload.issueId}`);
272
- }
273
- if (item.payload.message) {
274
- lines.push(`- Message: ${item.payload.message}`);
171
+ const validTargets = targets;
172
+ if (options.dryRun) {
173
+ console.log(chalk2.bold("Sync Plan (dry run):\n"));
174
+ const hooksPlan = planHooksSync();
175
+ if (hooksPlan.length > 0) {
176
+ console.log(chalk2.cyan("hooks (bin scripts):"));
177
+ for (const hook of hooksPlan) {
178
+ const icon = hook.status === "new" ? chalk2.green("+") : chalk2.blue("\u21BB");
179
+ const status = hook.status === "new" ? "" : chalk2.dim("[update]");
180
+ console.log(` ${icon} ${hook.name} ${status}`);
181
+ }
182
+ console.log("");
275
183
  }
276
- if (item.payload.action) {
277
- lines.push(`- Action: ${item.payload.action}`);
184
+ for (const runtime of validTargets) {
185
+ const plan = planSync(runtime);
186
+ console.log(chalk2.cyan(`${runtime}:`));
187
+ if (plan.skills.length === 0 && plan.commands.length === 0 && plan.agents.length === 0) {
188
+ console.log(chalk2.dim(" (nothing to sync)"));
189
+ continue;
190
+ }
191
+ for (const item of plan.skills) {
192
+ const icon = item.status === "conflict" ? chalk2.yellow("!") : chalk2.green("+");
193
+ const status = item.status === "conflict" ? chalk2.yellow("[conflict]") : "";
194
+ console.log(` ${icon} skill/${item.name} ${status}`);
195
+ }
196
+ for (const item of plan.commands) {
197
+ const icon = item.status === "conflict" ? chalk2.yellow("!") : chalk2.green("+");
198
+ const status = item.status === "conflict" ? chalk2.yellow("[conflict]") : "";
199
+ console.log(` ${icon} command/${item.name} ${status}`);
200
+ }
201
+ for (const item of plan.agents) {
202
+ const icon = item.status === "conflict" ? chalk2.yellow("!") : chalk2.green("+");
203
+ const status = item.status === "conflict" ? chalk2.yellow("[conflict]") : "";
204
+ console.log(` ${icon} agent/${item.name} ${status}`);
205
+ }
206
+ console.log("");
278
207
  }
279
- lines.push("");
280
- }
281
- lines.push("---");
282
- lines.push("");
283
- lines.push("Execute these items in priority order. Use `bd hook pop <id>` after completing each item.");
284
- return lines.join("\n");
285
- }
286
- var init_hooks = __esm({
287
- "src/lib/hooks.ts"() {
288
- "use strict";
289
- init_esm_shims();
290
- init_paths();
208
+ console.log(chalk2.dim("Run without --dry-run to apply changes."));
209
+ return;
291
210
  }
292
- });
293
-
294
- // src/lib/cv.ts
295
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync4 } from "fs";
296
- import { join as join4 } from "path";
297
- function getCVFile(agentId) {
298
- return join4(AGENTS_DIR, agentId, "cv.json");
299
- }
300
- function getAgentCV(agentId) {
301
- const cvFile = getCVFile(agentId);
302
- if (existsSync4(cvFile)) {
303
- try {
304
- return JSON.parse(readFileSync3(cvFile, "utf-8"));
305
- } catch {
211
+ if (config.sync.backup_before_sync) {
212
+ const spinner2 = ora2("Creating backup...").start();
213
+ const backupDirs = validTargets.flatMap((r) => [
214
+ SYNC_TARGETS[r].skills,
215
+ SYNC_TARGETS[r].commands,
216
+ SYNC_TARGETS[r].agents
217
+ ]);
218
+ const backup2 = createBackup(backupDirs);
219
+ if (backup2.targets.length > 0) {
220
+ spinner2.succeed(`Backup created: ${backup2.timestamp}`);
221
+ } else {
222
+ spinner2.info("No existing content to backup");
223
+ }
224
+ if (options.backupOnly) {
225
+ return;
306
226
  }
307
227
  }
308
- const cv = {
309
- agentId,
310
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
311
- lastActive: (/* @__PURE__ */ new Date()).toISOString(),
312
- runtime: "claude",
313
- model: "sonnet",
314
- stats: {
315
- totalIssues: 0,
316
- successCount: 0,
317
- failureCount: 0,
318
- abandonedCount: 0,
319
- avgDuration: 0,
320
- successRate: 0
321
- },
322
- skillsUsed: [],
323
- recentWork: []
324
- };
325
- saveAgentCV(cv);
326
- return cv;
327
- }
328
- function saveAgentCV(cv) {
329
- const dir = join4(AGENTS_DIR, cv.agentId);
330
- mkdirSync3(dir, { recursive: true });
331
- writeFileSync2(getCVFile(cv.agentId), JSON.stringify(cv, null, 2));
332
- }
333
- function startWork(agentId, issueId, skills) {
334
- const cv = getAgentCV(agentId);
335
- const entry = {
336
- issueId,
337
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
338
- outcome: "in_progress",
339
- skills
340
- };
341
- cv.recentWork.unshift(entry);
342
- cv.stats.totalIssues++;
343
- cv.lastActive = (/* @__PURE__ */ new Date()).toISOString();
344
- if (skills) {
345
- for (const skill of skills) {
346
- if (!cv.skillsUsed.includes(skill)) {
347
- cv.skillsUsed.push(skill);
228
+ const spinner = ora2("Syncing...").start();
229
+ let totalCreated = 0;
230
+ let totalConflicts = 0;
231
+ for (const runtime of validTargets) {
232
+ spinner.text = `Syncing to ${runtime}...`;
233
+ const result = executeSync(runtime, { force: options.force });
234
+ totalCreated += result.created.length;
235
+ totalConflicts += result.conflicts.length;
236
+ if (result.conflicts.length > 0 && !options.force) {
237
+ console.log("");
238
+ console.log(chalk2.yellow(`Conflicts in ${runtime}:`));
239
+ for (const name of result.conflicts) {
240
+ console.log(chalk2.dim(` - ${name} (use --force to overwrite)`));
348
241
  }
349
242
  }
350
243
  }
351
- if (cv.recentWork.length > 50) {
352
- cv.recentWork = cv.recentWork.slice(0, 50);
244
+ if (totalConflicts > 0 && !options.force) {
245
+ spinner.warn(`Synced ${totalCreated} items, ${totalConflicts} conflicts`);
246
+ console.log("");
247
+ console.log(chalk2.dim("Use --force to overwrite conflicting items."));
248
+ } else {
249
+ spinner.succeed(`Synced ${totalCreated} items to ${validTargets.join(", ")}`);
353
250
  }
354
- saveAgentCV(cv);
355
- }
356
- function getAgentRankings() {
357
- const rankings = [];
358
- if (!existsSync4(AGENTS_DIR)) return rankings;
359
- const dirs = readdirSync4(AGENTS_DIR, { withFileTypes: true }).filter(
360
- (d) => d.isDirectory()
361
- );
362
- for (const dir of dirs) {
363
- const cv = getAgentCV(dir.name);
364
- if (cv.stats.totalIssues > 0) {
365
- rankings.push({
366
- agentId: dir.name,
367
- successRate: cv.stats.successRate,
368
- totalIssues: cv.stats.totalIssues,
369
- avgDuration: cv.stats.avgDuration
370
- });
251
+ const hooksSpinner = ora2("Syncing hooks...").start();
252
+ const hooksResult = syncHooks();
253
+ if (hooksResult.errors.length > 0) {
254
+ hooksSpinner.warn(`Synced ${hooksResult.synced.length} hooks, ${hooksResult.errors.length} errors`);
255
+ for (const error of hooksResult.errors) {
256
+ console.log(chalk2.red(` \u2717 ${error}`));
371
257
  }
258
+ } else if (hooksResult.synced.length > 0) {
259
+ hooksSpinner.succeed(`Synced ${hooksResult.synced.length} hooks to ~/.panopticon/bin/`);
260
+ } else {
261
+ hooksSpinner.info("No hooks to sync");
372
262
  }
373
- rankings.sort((a, b) => {
374
- if (b.successRate !== a.successRate) {
375
- return b.successRate - a.successRate;
376
- }
377
- return b.totalIssues - a.totalIssues;
378
- });
379
- return rankings;
380
263
  }
381
- function formatCV(cv) {
382
- const lines = [
383
- `# Agent CV: ${cv.agentId}`,
384
- "",
385
- `Runtime: ${cv.runtime} (${cv.model})`,
386
- `Created: ${cv.createdAt}`,
387
- `Last Active: ${cv.lastActive}`,
388
- "",
389
- "## Statistics",
390
- "",
391
- `- Total Issues: ${cv.stats.totalIssues}`,
392
- `- Success Rate: ${(cv.stats.successRate * 100).toFixed(1)}%`,
393
- `- Successes: ${cv.stats.successCount}`,
394
- `- Failures: ${cv.stats.failureCount}`,
395
- `- Abandoned: ${cv.stats.abandonedCount}`,
396
- `- Avg Duration: ${cv.stats.avgDuration} minutes`,
397
- ""
398
- ];
399
- if (cv.skillsUsed.length > 0) {
400
- lines.push("## Skills Used");
401
- lines.push("");
402
- lines.push(cv.skillsUsed.join(", "));
403
- lines.push("");
264
+
265
+ // src/cli/commands/restore.ts
266
+ import chalk3 from "chalk";
267
+ import ora3 from "ora";
268
+ import inquirer from "inquirer";
269
+ async function restoreCommand(timestamp) {
270
+ const backups = listBackups();
271
+ if (backups.length === 0) {
272
+ console.log(chalk3.yellow("No backups found."));
273
+ return;
404
274
  }
405
- if (cv.recentWork.length > 0) {
406
- lines.push("## Recent Work");
407
- lines.push("");
408
- for (const work of cv.recentWork.slice(0, 10)) {
409
- const statusIcon = {
410
- success: "\u2713",
411
- failed: "\u2717",
412
- abandoned: "\u2298",
413
- in_progress: "\u25CF"
414
- }[work.outcome];
415
- const duration = work.duration ? ` (${work.duration}m)` : "";
416
- lines.push(`${statusIcon} ${work.issueId}${duration}`);
417
- if (work.failureReason) {
418
- lines.push(` Reason: ${work.failureReason}`);
419
- }
420
- }
421
- lines.push("");
422
- }
423
- return lines.join("\n");
424
- }
425
- var init_cv = __esm({
426
- "src/lib/cv.ts"() {
427
- "use strict";
428
- init_esm_shims();
429
- init_paths();
430
- }
431
- });
432
-
433
- // src/lib/agents.ts
434
- var agents_exports = {};
435
- __export(agents_exports, {
436
- autoRecoverAgents: () => autoRecoverAgents,
437
- detectCrashedAgents: () => detectCrashedAgents,
438
- getAgentDir: () => getAgentDir,
439
- getAgentState: () => getAgentState,
440
- listRunningAgents: () => listRunningAgents,
441
- messageAgent: () => messageAgent,
442
- recoverAgent: () => recoverAgent,
443
- saveAgentState: () => saveAgentState,
444
- spawnAgent: () => spawnAgent,
445
- stopAgent: () => stopAgent
446
- });
447
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, readFileSync as readFileSync4, readdirSync as readdirSync5 } from "fs";
448
- import { join as join5 } from "path";
449
- import { execSync as execSync2 } from "child_process";
450
- function getAgentDir(agentId) {
451
- return join5(AGENTS_DIR, agentId);
452
- }
453
- function getAgentState(agentId) {
454
- const stateFile = join5(getAgentDir(agentId), "state.json");
455
- if (!existsSync5(stateFile)) return null;
456
- const content = readFileSync4(stateFile, "utf8");
457
- return JSON.parse(content);
458
- }
459
- function saveAgentState(state) {
460
- const dir = getAgentDir(state.id);
461
- mkdirSync4(dir, { recursive: true });
462
- writeFileSync3(
463
- join5(dir, "state.json"),
464
- JSON.stringify(state, null, 2)
465
- );
466
- }
467
- function spawnAgent(options) {
468
- const agentId = `agent-${options.issueId.toLowerCase()}`;
469
- if (sessionExists(agentId)) {
470
- throw new Error(`Agent ${agentId} already running. Use 'pan work tell' to message it.`);
471
- }
472
- initHook(agentId);
473
- const state = {
474
- id: agentId,
475
- issueId: options.issueId,
476
- workspace: options.workspace,
477
- runtime: options.runtime || "claude",
478
- model: options.model || "sonnet",
479
- status: "starting",
480
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
481
- // Initialize Phase 4 fields
482
- handoffCount: 0,
483
- costSoFar: 0
484
- };
485
- saveAgentState(state);
486
- let prompt = options.prompt || "";
487
- const { hasWork, items } = checkHook(agentId);
488
- if (hasWork) {
489
- const guppPrompt = generateGUPPPrompt(agentId);
490
- if (guppPrompt) {
491
- prompt = guppPrompt + "\n\n---\n\n" + prompt;
275
+ if (!timestamp) {
276
+ console.log(chalk3.bold("Available backups:\n"));
277
+ for (const backup2 of backups.slice(0, 10)) {
278
+ console.log(` ${chalk3.cyan(backup2.timestamp)} - ${backup2.targets.join(", ")}`);
492
279
  }
493
- }
494
- const promptFile = join5(getAgentDir(agentId), "initial-prompt.md");
495
- if (prompt) {
496
- writeFileSync3(promptFile, prompt);
497
- }
498
- checkAndSetupHooks();
499
- writeTaskCache(agentId, options.issueId);
500
- const claudeCmd = `claude --dangerously-skip-permissions --model ${state.model}`;
501
- createSession(agentId, options.workspace, claudeCmd, {
502
- env: {
503
- PANOPTICON_AGENT_ID: agentId
280
+ if (backups.length > 10) {
281
+ console.log(chalk3.dim(` ... and ${backups.length - 10} more`));
504
282
  }
505
- });
506
- if (prompt) {
507
- let ready = false;
508
- for (let i = 0; i < 15; i++) {
509
- execSync2("sleep 1");
510
- try {
511
- const pane = execSync2(`tmux capture-pane -t ${agentId} -p`, { encoding: "utf-8" });
512
- if (pane.includes("\u276F") || pane.includes(">")) {
513
- ready = true;
514
- break;
515
- }
516
- } catch {
283
+ console.log("");
284
+ const { selected } = await inquirer.prompt([
285
+ {
286
+ type: "list",
287
+ name: "selected",
288
+ message: "Select backup to restore:",
289
+ choices: backups.slice(0, 10).map((b) => ({
290
+ name: `${b.timestamp} (${b.targets.join(", ")})`,
291
+ value: b.timestamp
292
+ }))
517
293
  }
294
+ ]);
295
+ timestamp = selected;
296
+ }
297
+ const { confirm: confirm2 } = await inquirer.prompt([
298
+ {
299
+ type: "confirm",
300
+ name: "confirm",
301
+ message: `Restore backup ${timestamp}? This will overwrite current files.`,
302
+ default: false
518
303
  }
519
- if (ready) {
520
- execSync2(`tmux load-buffer "${promptFile}"`);
521
- execSync2(`tmux paste-buffer -t ${agentId}`);
522
- execSync2("sleep 0.5");
523
- execSync2(`tmux send-keys -t ${agentId} Enter`);
524
- } else {
525
- console.error("Claude did not become ready in time, prompt not sent");
526
- }
304
+ ]);
305
+ if (!confirm2) {
306
+ console.log(chalk3.dim("Restore cancelled."));
307
+ return;
527
308
  }
528
- state.status = "running";
529
- saveAgentState(state);
530
- startWork(agentId, options.issueId);
531
- return state;
532
- }
533
- function listRunningAgents() {
534
- const tmuxSessions = getAgentSessions();
535
- const tmuxNames = new Set(tmuxSessions.map((s) => s.name));
536
- const agents = [];
537
- if (!existsSync5(AGENTS_DIR)) return agents;
538
- const dirs = readdirSync5(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
539
- for (const dir of dirs) {
540
- const state = getAgentState(dir.name);
541
- if (state) {
542
- agents.push({
543
- ...state,
544
- tmuxActive: tmuxNames.has(state.id)
545
- });
309
+ const spinner = ora3("Restoring backup...").start();
310
+ try {
311
+ const config = loadConfig();
312
+ const targets = config.sync.targets;
313
+ const targetDirs = {};
314
+ for (const runtime of targets) {
315
+ targetDirs[`${runtime}-skills`] = SYNC_TARGETS[runtime].skills;
316
+ targetDirs[`${runtime}-commands`] = SYNC_TARGETS[runtime].commands;
317
+ targetDirs["skills"] = SYNC_TARGETS[runtime].skills;
318
+ targetDirs["commands"] = SYNC_TARGETS[runtime].commands;
546
319
  }
320
+ restoreBackup(timestamp, targetDirs);
321
+ spinner.succeed(`Restored backup: ${timestamp}`);
322
+ } catch (error) {
323
+ spinner.fail("Failed to restore");
324
+ console.error(chalk3.red(error.message));
325
+ process.exit(1);
547
326
  }
548
- return agents;
549
327
  }
550
- function stopAgent(agentId) {
551
- const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
552
- if (sessionExists(normalizedId)) {
553
- killSession(normalizedId);
328
+
329
+ // src/cli/commands/backup.ts
330
+ import chalk4 from "chalk";
331
+ async function backupListCommand(options) {
332
+ const backups = listBackups();
333
+ if (options.json) {
334
+ console.log(JSON.stringify(backups, null, 2));
335
+ return;
554
336
  }
555
- const state = getAgentState(normalizedId);
556
- if (state) {
557
- state.status = "stopped";
558
- saveAgentState(state);
337
+ if (backups.length === 0) {
338
+ console.log(chalk4.dim("No backups found."));
339
+ console.log(chalk4.dim("Backups are created automatically during sync."));
340
+ return;
341
+ }
342
+ console.log(chalk4.bold("Backups:\n"));
343
+ for (const backup2 of backups) {
344
+ console.log(` ${chalk4.cyan(backup2.timestamp)}`);
345
+ console.log(` ${chalk4.dim("Contains:")} ${backup2.targets.join(", ")}`);
559
346
  }
347
+ console.log();
348
+ console.log(chalk4.dim(`Total: ${backups.length} backups`));
349
+ console.log(chalk4.dim("Use `pan restore <timestamp>` to restore a backup."));
560
350
  }
561
- function messageAgent(agentId, message) {
562
- const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
563
- if (!sessionExists(normalizedId)) {
564
- throw new Error(`Agent ${normalizedId} not running`);
351
+ async function backupCleanCommand(options) {
352
+ const keepCount = parseInt(options.keep || "10", 10);
353
+ const removed = cleanOldBackups(keepCount);
354
+ if (removed === 0) {
355
+ console.log(chalk4.dim(`No backups removed (keeping ${keepCount}).`));
356
+ } else {
357
+ console.log(chalk4.green(`Removed ${removed} old backup(s), keeping ${keepCount}.`));
565
358
  }
566
- sendKeys(normalizedId, message);
567
- const mailDir = join5(getAgentDir(normalizedId), "mail");
568
- mkdirSync4(mailDir, { recursive: true });
569
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
570
- writeFileSync3(
571
- join5(mailDir, `${timestamp}.md`),
572
- `# Message
359
+ }
573
360
 
574
- ${message}
575
- `
576
- );
361
+ // src/cli/commands/skills.ts
362
+ import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2 } from "fs";
363
+ import { join as join2 } from "path";
364
+ import chalk5 from "chalk";
365
+ function parseSkillFrontmatter(content) {
366
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
367
+ if (!match) return {};
368
+ const frontmatter = match[1];
369
+ const name = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim();
370
+ const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim();
371
+ return { name, description };
577
372
  }
578
- function detectCrashedAgents() {
579
- const agents = listRunningAgents();
580
- return agents.filter(
581
- (agent) => agent.status === "running" && !agent.tmuxActive
582
- );
373
+ function listSkills() {
374
+ if (!existsSync2(SKILLS_DIR)) return [];
375
+ const skills = [];
376
+ const dirs = readdirSync2(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
377
+ for (const dir of dirs) {
378
+ const skillFile = join2(SKILLS_DIR, dir.name, "SKILL.md");
379
+ if (!existsSync2(skillFile)) continue;
380
+ const content = readFileSync(skillFile, "utf8");
381
+ const { name, description } = parseSkillFrontmatter(content);
382
+ skills.push({
383
+ name: name || dir.name,
384
+ description: description || "(no description)",
385
+ path: skillFile
386
+ });
387
+ }
388
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
583
389
  }
584
- function recoverAgent(agentId) {
585
- const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
586
- const state = getAgentState(normalizedId);
587
- if (!state) {
588
- return null;
390
+ async function skillsCommand(options) {
391
+ const skills = listSkills();
392
+ if (options.json) {
393
+ console.log(JSON.stringify(skills, null, 2));
394
+ return;
589
395
  }
590
- if (sessionExists(normalizedId)) {
591
- return state;
396
+ console.log(chalk5.bold(`
397
+ Panopticon Skills (${skills.length})
398
+ `));
399
+ if (skills.length === 0) {
400
+ console.log(chalk5.yellow("No skills found."));
401
+ console.log(chalk5.dim("Skills should be in ~/.panopticon/skills/<name>/SKILL.md"));
402
+ return;
592
403
  }
593
- const healthFile = join5(getAgentDir(normalizedId), "health.json");
594
- let health = { consecutiveFailures: 0, killCount: 0, recoveryCount: 0 };
595
- if (existsSync5(healthFile)) {
596
- try {
597
- health = { ...health, ...JSON.parse(readFileSync4(healthFile, "utf-8")) };
598
- } catch {
599
- }
404
+ for (const skill of skills) {
405
+ console.log(chalk5.cyan(skill.name));
406
+ console.log(chalk5.dim(` ${skill.description}`));
600
407
  }
601
- health.recoveryCount = (health.recoveryCount || 0) + 1;
602
- writeFileSync3(healthFile, JSON.stringify(health, null, 2));
603
- const recoveryPrompt = generateRecoveryPrompt(state);
604
- const claudeCmd = `claude --dangerously-skip-permissions --model ${state.model} "${recoveryPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
605
- createSession(normalizedId, state.workspace, claudeCmd);
606
- state.status = "running";
607
- state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
608
- saveAgentState(state);
609
- return state;
408
+ console.log(`
409
+ ${chalk5.dim('Run "pan sync" to sync skills to Claude Code')}`);
610
410
  }
611
- function generateRecoveryPrompt(state) {
612
- const lines = [
613
- "# Agent Recovery",
614
- "",
615
- "\u26A0\uFE0F This agent session was recovered after a crash.",
616
- "",
617
- "## Previous Context",
618
- `- Issue: ${state.issueId}`,
619
- `- Workspace: ${state.workspace}`,
620
- `- Started: ${state.startedAt}`,
621
- "",
622
- "## Recovery Steps",
623
- "1. Check beads for context: `bd show " + state.issueId + "`",
624
- "2. Review recent git commits: `git log --oneline -10`",
625
- "3. Check hook for pending work: `pan work hook check`",
626
- "4. Resume from last known state",
627
- "",
628
- "## GUPP Reminder",
629
- '> "If there is work on your Hook, YOU MUST RUN IT."',
630
- ""
631
- ];
632
- const { hasWork } = checkHook(state.id);
633
- if (hasWork) {
634
- const guppPrompt = generateGUPPPrompt(state.id);
635
- if (guppPrompt) {
636
- lines.push("---");
637
- lines.push("");
638
- lines.push(guppPrompt);
639
- }
411
+
412
+ // src/cli/commands/work/issue.ts
413
+ import chalk6 from "chalk";
414
+ import ora4 from "ora";
415
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
416
+ import { join as join6, dirname as dirname2 } from "path";
417
+
418
+ // src/lib/agents.ts
419
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, readFileSync as readFileSync4, readdirSync as readdirSync5 } from "fs";
420
+ import { join as join5 } from "path";
421
+ import { execSync as execSync2 } from "child_process";
422
+ import { homedir } from "os";
423
+
424
+ // src/lib/tmux.ts
425
+ import { execSync } from "child_process";
426
+ import { writeFileSync, chmodSync } from "fs";
427
+ function listSessions() {
428
+ try {
429
+ const output = execSync('tmux list-sessions -F "#{session_name}|#{session_created}|#{session_attached}|#{session_windows}"', {
430
+ encoding: "utf8"
431
+ });
432
+ return output.trim().split("\n").filter(Boolean).map((line) => {
433
+ const [name, created, attached, windows] = line.split("|");
434
+ return {
435
+ name,
436
+ created: new Date(parseInt(created) * 1e3),
437
+ attached: attached === "1",
438
+ windows: parseInt(windows)
439
+ };
440
+ });
441
+ } catch {
442
+ return [];
640
443
  }
641
- return lines.join("\n");
642
444
  }
643
- function autoRecoverAgents() {
644
- const crashed = detectCrashedAgents();
645
- const recovered = [];
646
- const failed = [];
647
- for (const agent of crashed) {
648
- try {
649
- const result = recoverAgent(agent.id);
650
- if (result) {
651
- recovered.push(agent.id);
652
- } else {
653
- failed.push(agent.id);
654
- }
655
- } catch (error) {
656
- failed.push(agent.id);
657
- }
445
+ function sessionExists(name) {
446
+ try {
447
+ execSync(`tmux has-session -t ${name} 2>/dev/null`);
448
+ return true;
449
+ } catch {
450
+ return false;
658
451
  }
659
- return { recovered, failed };
660
452
  }
661
- function checkAndSetupHooks() {
662
- const { homedir: homedir9 } = __require("os");
663
- const settingsPath = join5(homedir9(), ".claude", "settings.json");
664
- const hookPath = join5(homedir9(), ".panopticon", "bin", "heartbeat-hook");
665
- if (existsSync5(settingsPath)) {
666
- try {
667
- const settingsContent = readFileSync4(settingsPath, "utf-8");
668
- const settings = JSON.parse(settingsContent);
669
- const postToolUse = settings?.hooks?.PostToolUse || [];
670
- const hookConfigured = postToolUse.some(
671
- (hookConfig) => hookConfig.hooks?.some(
672
- (hook) => hook.command === hookPath || hook.command?.includes("panopticon") || hook.command?.includes("heartbeat-hook")
673
- )
674
- );
675
- if (hookConfigured) {
676
- return;
677
- }
678
- } catch {
453
+ function createSession(name, cwd, initialCommand, options) {
454
+ const escapedCwd = cwd.replace(/"/g, '\\"');
455
+ let envFlags = "";
456
+ if (options?.env) {
457
+ for (const [key, value] of Object.entries(options.env)) {
458
+ envFlags += ` -e ${key}="${value.replace(/"/g, '\\"')}"`;
679
459
  }
680
460
  }
681
- try {
682
- console.log("Configuring Panopticon heartbeat hooks...");
683
- execSync2("pan setup hooks", { stdio: "pipe" });
684
- console.log("\u2713 Heartbeat hooks configured");
685
- } catch (error) {
686
- console.warn("\u26A0 Failed to auto-configure hooks. Run `pan setup hooks` manually.");
461
+ if (initialCommand && (initialCommand.includes("`") || initialCommand.includes("\n") || initialCommand.length > 500)) {
462
+ execSync(`tmux new-session -d -s ${name} -c "${escapedCwd}"${envFlags}`);
463
+ execSync("sleep 0.5");
464
+ const tmpFile = `/tmp/pan-cmd-${name}.sh`;
465
+ writeFileSync(tmpFile, initialCommand);
466
+ chmodSync(tmpFile, "755");
467
+ execSync(`tmux send-keys -t ${name} "bash ${tmpFile}" Enter`);
468
+ } else if (initialCommand) {
469
+ const cmd = `tmux new-session -d -s ${name} -c "${escapedCwd}"${envFlags} "${initialCommand.replace(/"/g, '\\"')}"`;
470
+ execSync(cmd);
471
+ } else {
472
+ execSync(`tmux new-session -d -s ${name} -c "${escapedCwd}"${envFlags}`);
687
473
  }
688
474
  }
689
- function writeTaskCache(agentId, issueId) {
690
- const cacheDir = join5(getAgentDir(agentId));
691
- mkdirSync4(cacheDir, { recursive: true });
692
- const cacheFile = join5(cacheDir, "current-task.json");
693
- writeFileSync3(
694
- cacheFile,
695
- JSON.stringify({
696
- id: issueId,
697
- title: `Working on ${issueId}`,
698
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
699
- }, null, 2)
700
- );
475
+ function killSession(name) {
476
+ execSync(`tmux kill-session -t ${name}`);
477
+ }
478
+ function sendKeys(sessionName, keys) {
479
+ const escapedKeys = keys.replace(/"/g, '\\"');
480
+ execSync(`tmux send-keys -t ${sessionName} "${escapedKeys}"`);
481
+ execSync(`tmux send-keys -t ${sessionName} Enter`);
482
+ }
483
+ function getAgentSessions() {
484
+ return listSessions().filter((s) => s.name.startsWith("agent-"));
701
485
  }
702
- var init_agents = __esm({
703
- "src/lib/agents.ts"() {
704
- "use strict";
705
- init_esm_shims();
706
- init_paths();
707
- init_tmux();
708
- init_hooks();
709
- init_cv();
710
- }
711
- });
712
486
 
713
- // package.json
714
- var require_package = __commonJS({
715
- "package.json"(exports, module) {
716
- module.exports = {
717
- name: "panopticon-cli",
718
- version: "0.3.2",
719
- description: "Multi-agent orchestration for AI coding assistants (Claude Code, Codex, Cursor, Gemini CLI)",
720
- keywords: [
721
- "ai-agents",
722
- "orchestration",
723
- "claude-code",
724
- "codex",
725
- "cursor",
726
- "gemini",
727
- "multi-agent",
728
- "devtools",
729
- "linear"
730
- ],
731
- author: "Edward Becker <edward.becker@mindyournow.com>",
732
- license: "MIT",
733
- repository: {
734
- type: "git",
735
- url: "https://github.com/eltmon/panopticon-cli.git"
736
- },
737
- homepage: "https://github.com/eltmon/panopticon-cli#readme",
738
- bugs: {
739
- url: "https://github.com/eltmon/panopticon-cli/issues"
740
- },
741
- type: "module",
742
- bin: {
743
- pan: "./dist/cli/index.js",
744
- panopticon: "./dist/cli/index.js"
745
- },
746
- main: "./dist/index.js",
747
- types: "./dist/index.d.ts",
748
- files: [
749
- "dist",
750
- "templates",
751
- "README.md",
752
- "LICENSE"
753
- ],
754
- engines: {
755
- node: ">=18.0.0"
756
- },
757
- scripts: {
758
- dev: "tsx watch src/cli/index.ts",
759
- build: "tsup",
760
- typecheck: "tsc --noEmit",
761
- lint: "eslint src/",
762
- test: "vitest --no-file-parallelism",
763
- "test:unit": "vitest run tests/unit",
764
- "test:integration": "vitest run tests/integration",
765
- "test:e2e": "vitest run tests/e2e",
766
- "test:coverage": "vitest run --coverage",
767
- docs: "typedoc",
768
- "docs:watch": "typedoc --watch",
769
- prepublishOnly: "npm run build",
770
- postinstall: "node scripts/postinstall.mjs || true"
771
- },
772
- dependencies: {
773
- "@iarna/toml": "^2.2.5",
774
- "@linear/sdk": "^70.0.0",
775
- "@octokit/rest": "^22.0.1",
776
- "better-sqlite3": "^12.6.2",
777
- chalk: "^5.6.2",
778
- commander: "^12.1.0",
779
- conf: "^12.0.0",
780
- execa: "^8.0.1",
781
- inquirer: "^9.3.8",
782
- ora: "^8.2.0",
783
- rally: "^2.1.3",
784
- yaml: "^2.8.2"
785
- },
786
- devDependencies: {
787
- "@types/better-sqlite3": "^7.6.13",
788
- "@types/inquirer": "^9.0.9",
789
- "@types/node": "^20.10.0",
790
- "@vitest/coverage-v8": "^1.0.4",
791
- eslint: "^8.55.0",
792
- tsup: "^8.0.1",
793
- tsx: "^4.6.2",
794
- typedoc: "^0.25.0",
795
- typescript: "^5.3.2",
796
- vitest: "^1.0.4"
797
- }
487
+ // src/lib/hooks.ts
488
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
489
+ import { join as join3 } from "path";
490
+ function getHookDir(agentId) {
491
+ return join3(AGENTS_DIR, agentId);
492
+ }
493
+ function getHookFile(agentId) {
494
+ return join3(getHookDir(agentId), "hook.json");
495
+ }
496
+ function getMailDir(agentId) {
497
+ return join3(getHookDir(agentId), "mail");
498
+ }
499
+ function initHook(agentId) {
500
+ const hookDir = getHookDir(agentId);
501
+ const mailDir = getMailDir(agentId);
502
+ mkdirSync2(hookDir, { recursive: true });
503
+ mkdirSync2(mailDir, { recursive: true });
504
+ const hookFile = getHookFile(agentId);
505
+ if (!existsSync3(hookFile)) {
506
+ const hook = {
507
+ agentId,
508
+ items: []
798
509
  };
510
+ writeFileSync2(hookFile, JSON.stringify(hook, null, 2));
799
511
  }
800
- });
801
-
802
- // src/cli/index.ts
803
- init_esm_shims();
804
- import { Command } from "commander";
805
- import chalk29 from "chalk";
806
-
807
- // src/cli/commands/init.ts
808
- init_esm_shims();
809
- init_paths();
810
- import { existsSync, mkdirSync, readdirSync, cpSync } from "fs";
811
- import { join, dirname } from "path";
812
- import { fileURLToPath } from "url";
813
- import chalk from "chalk";
814
- import ora from "ora";
815
- var __filename = fileURLToPath(import.meta.url);
816
- var __dirname = dirname(__filename);
817
- var PACKAGE_ROOT = join(__dirname, "..", "..");
818
- var BUNDLED_SKILLS_DIR = join(PACKAGE_ROOT, "skills");
819
- var BUNDLED_AGENTS_DIR = join(PACKAGE_ROOT, "agents");
820
- function copyBundledSkills() {
821
- if (!existsSync(BUNDLED_SKILLS_DIR)) {
822
- return 0;
823
- }
824
- if (!existsSync(SKILLS_DIR)) {
825
- mkdirSync(SKILLS_DIR, { recursive: true });
826
- }
827
- const skills = readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
828
- let copied = 0;
829
- for (const skill of skills) {
830
- const sourcePath = join(BUNDLED_SKILLS_DIR, skill.name);
831
- const targetPath = join(SKILLS_DIR, skill.name);
832
- cpSync(sourcePath, targetPath, { recursive: true });
833
- copied++;
834
- }
835
- return copied;
836
512
  }
837
- function copyBundledAgents() {
838
- if (!existsSync(BUNDLED_AGENTS_DIR)) {
839
- return 0;
840
- }
841
- if (!existsSync(AGENTS_DIR)) {
842
- mkdirSync(AGENTS_DIR, { recursive: true });
513
+ function getHook(agentId) {
514
+ const hookFile = getHookFile(agentId);
515
+ if (!existsSync3(hookFile)) {
516
+ return null;
843
517
  }
844
- const agents = readdirSync(BUNDLED_AGENTS_DIR, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md"));
845
- let copied = 0;
846
- for (const agent of agents) {
847
- const sourcePath = join(BUNDLED_AGENTS_DIR, agent.name);
848
- const targetPath = join(AGENTS_DIR, agent.name);
849
- cpSync(sourcePath, targetPath);
850
- copied++;
518
+ try {
519
+ const content = readFileSync2(hookFile, "utf-8");
520
+ return JSON.parse(content);
521
+ } catch {
522
+ return null;
851
523
  }
852
- return copied;
853
524
  }
854
- async function initCommand() {
855
- const spinner = ora("Initializing Panopticon...").start();
856
- if (existsSync(CONFIG_FILE)) {
857
- spinner.info("Panopticon already initialized");
858
- console.log(chalk.dim(` Config: ${CONFIG_FILE}`));
859
- console.log(chalk.dim(` Home: ${PANOPTICON_HOME}`));
860
- console.log(chalk.dim(" Run `pan sync` to update skills"));
861
- return;
862
- }
863
- try {
864
- for (const dir of INIT_DIRS) {
865
- if (!existsSync(dir)) {
866
- mkdirSync(dir, { recursive: true });
525
+ function pushToHook(agentId, item) {
526
+ initHook(agentId);
527
+ const hook = getHook(agentId) || { agentId, items: [] };
528
+ const newItem = {
529
+ ...item,
530
+ id: `hook-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
531
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
532
+ };
533
+ hook.items.push(newItem);
534
+ writeFileSync2(getHookFile(agentId), JSON.stringify(hook, null, 2));
535
+ return newItem;
536
+ }
537
+ function checkHook(agentId) {
538
+ const hook = getHook(agentId);
539
+ if (!hook || hook.items.length === 0) {
540
+ const mailDir = getMailDir(agentId);
541
+ if (existsSync3(mailDir)) {
542
+ const mails = readdirSync3(mailDir).filter((f) => f.endsWith(".json"));
543
+ if (mails.length > 0) {
544
+ const mailItems = mails.map((file) => {
545
+ try {
546
+ const content = readFileSync2(join3(mailDir, file), "utf-8");
547
+ return JSON.parse(content);
548
+ } catch {
549
+ return null;
550
+ }
551
+ }).filter(Boolean);
552
+ return {
553
+ hasWork: mailItems.length > 0,
554
+ urgentCount: mailItems.filter((i) => i.priority === "urgent").length,
555
+ items: mailItems
556
+ };
867
557
  }
868
558
  }
869
- spinner.text = "Created directories...";
870
- const config = getDefaultConfig();
871
- saveConfig(config);
872
- spinner.text = "Created config...";
873
- spinner.text = "Installing bundled skills...";
874
- const skillsCopied = copyBundledSkills();
875
- spinner.text = "Installing bundled agents...";
876
- const agentsCopied = copyBundledAgents();
877
- const shell = detectShell();
878
- const rcFile = getShellRcFile(shell);
879
- if (rcFile && existsSync(rcFile)) {
880
- addAlias(rcFile);
881
- spinner.succeed("Panopticon initialized!");
882
- console.log("");
883
- console.log(chalk.green("\u2713") + " Created " + chalk.cyan(PANOPTICON_HOME));
884
- console.log(chalk.green("\u2713") + " Created " + chalk.cyan(CONFIG_FILE));
885
- if (skillsCopied > 0) {
886
- console.log(chalk.green("\u2713") + ` Installed ${skillsCopied} bundled skills`);
887
- }
888
- if (agentsCopied > 0) {
889
- console.log(chalk.green("\u2713") + ` Installed ${agentsCopied} bundled agents`);
890
- }
891
- console.log(chalk.green("\u2713") + " " + getAliasInstructions(shell));
892
- } else {
893
- spinner.succeed("Panopticon initialized!");
894
- console.log("");
895
- console.log(chalk.green("\u2713") + " Created " + chalk.cyan(PANOPTICON_HOME));
896
- console.log(chalk.green("\u2713") + " Created " + chalk.cyan(CONFIG_FILE));
897
- if (skillsCopied > 0) {
898
- console.log(chalk.green("\u2713") + ` Installed ${skillsCopied} bundled skills`);
899
- }
900
- if (agentsCopied > 0) {
901
- console.log(chalk.green("\u2713") + ` Installed ${agentsCopied} bundled agents`);
902
- }
903
- console.log(chalk.yellow("!") + " Could not detect shell. Add alias manually:");
904
- console.log(chalk.dim(' alias pan="panopticon"'));
559
+ return { hasWork: false, urgentCount: 0, items: [] };
560
+ }
561
+ const now = /* @__PURE__ */ new Date();
562
+ const activeItems = hook.items.filter((item) => {
563
+ if (item.expiresAt) {
564
+ return new Date(item.expiresAt) > now;
565
+ }
566
+ return true;
567
+ });
568
+ const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
569
+ activeItems.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
570
+ return {
571
+ hasWork: activeItems.length > 0,
572
+ urgentCount: activeItems.filter((i) => i.priority === "urgent").length,
573
+ items: activeItems
574
+ };
575
+ }
576
+ function popFromHook(agentId, itemId) {
577
+ const hook = getHook(agentId);
578
+ if (!hook) return false;
579
+ const index = hook.items.findIndex((i) => i.id === itemId);
580
+ if (index === -1) return false;
581
+ hook.items.splice(index, 1);
582
+ hook.lastChecked = (/* @__PURE__ */ new Date()).toISOString();
583
+ writeFileSync2(getHookFile(agentId), JSON.stringify(hook, null, 2));
584
+ return true;
585
+ }
586
+ function clearHook(agentId) {
587
+ const hook = getHook(agentId);
588
+ if (!hook) return;
589
+ hook.items = [];
590
+ hook.lastChecked = (/* @__PURE__ */ new Date()).toISOString();
591
+ writeFileSync2(getHookFile(agentId), JSON.stringify(hook, null, 2));
592
+ }
593
+ function sendMail(toAgentId, from, message, priority = "normal") {
594
+ initHook(toAgentId);
595
+ const mailDir = getMailDir(toAgentId);
596
+ const mailItem = {
597
+ id: `mail-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
598
+ type: "message",
599
+ priority,
600
+ source: from,
601
+ payload: { message },
602
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
603
+ };
604
+ writeFileSync2(
605
+ join3(mailDir, `${mailItem.id}.json`),
606
+ JSON.stringify(mailItem, null, 2)
607
+ );
608
+ }
609
+ function generateGUPPPrompt(agentId) {
610
+ const { hasWork, urgentCount, items } = checkHook(agentId);
611
+ if (!hasWork) return null;
612
+ const lines = [
613
+ "# GUPP: Work Found on Your Hook",
614
+ "",
615
+ '> "If there is work on your Hook, YOU MUST RUN IT."',
616
+ ""
617
+ ];
618
+ if (urgentCount > 0) {
619
+ lines.push(`\u26A0\uFE0F **${urgentCount} URGENT item(s) require immediate attention**`);
620
+ lines.push("");
621
+ }
622
+ lines.push(`## Pending Work Items (${items.length})`);
623
+ lines.push("");
624
+ for (const item of items) {
625
+ const priorityEmoji = {
626
+ urgent: "\u{1F534}",
627
+ high: "\u{1F7E0}",
628
+ normal: "\u{1F7E2}",
629
+ low: "\u26AA"
630
+ }[item.priority];
631
+ lines.push(`### ${priorityEmoji} ${item.type.toUpperCase()}: ${item.id}`);
632
+ lines.push(`- Source: ${item.source}`);
633
+ lines.push(`- Created: ${item.createdAt}`);
634
+ if (item.payload.issueId) {
635
+ lines.push(`- Issue: ${item.payload.issueId}`);
905
636
  }
906
- console.log("");
907
- console.log("Next steps:");
908
- console.log(chalk.dim(" 1. Run: pan sync"));
909
- console.log(chalk.dim(" 2. Start dashboard: pan up"));
910
- } catch (error) {
911
- spinner.fail("Failed to initialize");
912
- console.error(chalk.red(error.message));
913
- process.exit(1);
637
+ if (item.payload.message) {
638
+ lines.push(`- Message: ${item.payload.message}`);
639
+ }
640
+ if (item.payload.action) {
641
+ lines.push(`- Action: ${item.payload.action}`);
642
+ }
643
+ lines.push("");
914
644
  }
645
+ lines.push("---");
646
+ lines.push("");
647
+ lines.push("Execute these items in priority order. Use `bd hook pop <id>` after completing each item.");
648
+ return lines.join("\n");
915
649
  }
916
650
 
917
- // src/cli/commands/sync.ts
918
- init_esm_shims();
919
- import chalk2 from "chalk";
920
- import ora2 from "ora";
921
- init_paths();
922
- async function syncCommand(options) {
923
- const config = loadConfig();
924
- const targets = config.sync?.targets;
925
- if (!targets || !Array.isArray(targets) || targets.length === 0) {
926
- console.log(chalk2.yellow("No sync targets configured."));
927
- console.log(chalk2.dim("Edit ~/.panopticon/config.toml to add targets to the [sync] section."));
928
- console.log(chalk2.dim('Example: targets = ["claude-code", "cursor"]'));
929
- return;
930
- }
931
- const validTargets = targets;
932
- if (options.dryRun) {
933
- console.log(chalk2.bold("Sync Plan (dry run):\n"));
934
- const hooksPlan = planHooksSync();
935
- if (hooksPlan.length > 0) {
936
- console.log(chalk2.cyan("hooks (bin scripts):"));
937
- for (const hook of hooksPlan) {
938
- const icon = hook.status === "new" ? chalk2.green("+") : chalk2.blue("\u21BB");
939
- const status = hook.status === "new" ? "" : chalk2.dim("[update]");
940
- console.log(` ${icon} ${hook.name} ${status}`);
941
- }
942
- console.log("");
651
+ // src/lib/cv.ts
652
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, readdirSync as readdirSync4 } from "fs";
653
+ import { join as join4 } from "path";
654
+ function getCVFile(agentId) {
655
+ return join4(AGENTS_DIR, agentId, "cv.json");
656
+ }
657
+ function getAgentCV(agentId) {
658
+ const cvFile = getCVFile(agentId);
659
+ if (existsSync4(cvFile)) {
660
+ try {
661
+ return JSON.parse(readFileSync3(cvFile, "utf-8"));
662
+ } catch {
943
663
  }
944
- for (const runtime of validTargets) {
945
- const plan = planSync(runtime);
946
- console.log(chalk2.cyan(`${runtime}:`));
947
- if (plan.skills.length === 0 && plan.commands.length === 0 && plan.agents.length === 0) {
948
- console.log(chalk2.dim(" (nothing to sync)"));
949
- continue;
950
- }
951
- for (const item of plan.skills) {
952
- const icon = item.status === "conflict" ? chalk2.yellow("!") : chalk2.green("+");
953
- const status = item.status === "conflict" ? chalk2.yellow("[conflict]") : "";
954
- console.log(` ${icon} skill/${item.name} ${status}`);
955
- }
956
- for (const item of plan.commands) {
957
- const icon = item.status === "conflict" ? chalk2.yellow("!") : chalk2.green("+");
958
- const status = item.status === "conflict" ? chalk2.yellow("[conflict]") : "";
959
- console.log(` ${icon} command/${item.name} ${status}`);
960
- }
961
- for (const item of plan.agents) {
962
- const icon = item.status === "conflict" ? chalk2.yellow("!") : chalk2.green("+");
963
- const status = item.status === "conflict" ? chalk2.yellow("[conflict]") : "";
964
- console.log(` ${icon} agent/${item.name} ${status}`);
664
+ }
665
+ const cv = {
666
+ agentId,
667
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
668
+ lastActive: (/* @__PURE__ */ new Date()).toISOString(),
669
+ runtime: "claude",
670
+ model: "sonnet",
671
+ stats: {
672
+ totalIssues: 0,
673
+ successCount: 0,
674
+ failureCount: 0,
675
+ abandonedCount: 0,
676
+ avgDuration: 0,
677
+ successRate: 0
678
+ },
679
+ skillsUsed: [],
680
+ recentWork: []
681
+ };
682
+ saveAgentCV(cv);
683
+ return cv;
684
+ }
685
+ function saveAgentCV(cv) {
686
+ const dir = join4(AGENTS_DIR, cv.agentId);
687
+ mkdirSync3(dir, { recursive: true });
688
+ writeFileSync3(getCVFile(cv.agentId), JSON.stringify(cv, null, 2));
689
+ }
690
+ function startWork(agentId, issueId, skills) {
691
+ const cv = getAgentCV(agentId);
692
+ const entry = {
693
+ issueId,
694
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
695
+ outcome: "in_progress",
696
+ skills
697
+ };
698
+ cv.recentWork.unshift(entry);
699
+ cv.stats.totalIssues++;
700
+ cv.lastActive = (/* @__PURE__ */ new Date()).toISOString();
701
+ if (skills) {
702
+ for (const skill of skills) {
703
+ if (!cv.skillsUsed.includes(skill)) {
704
+ cv.skillsUsed.push(skill);
965
705
  }
966
- console.log("");
967
706
  }
968
- console.log(chalk2.dim("Run without --dry-run to apply changes."));
969
- return;
970
707
  }
971
- if (config.sync.backup_before_sync) {
972
- const spinner2 = ora2("Creating backup...").start();
973
- const backupDirs = validTargets.flatMap((r) => [
974
- SYNC_TARGETS[r].skills,
975
- SYNC_TARGETS[r].commands,
976
- SYNC_TARGETS[r].agents
977
- ]);
978
- const backup2 = createBackup(backupDirs);
979
- if (backup2.targets.length > 0) {
980
- spinner2.succeed(`Backup created: ${backup2.timestamp}`);
981
- } else {
982
- spinner2.info("No existing content to backup");
708
+ if (cv.recentWork.length > 50) {
709
+ cv.recentWork = cv.recentWork.slice(0, 50);
710
+ }
711
+ saveAgentCV(cv);
712
+ }
713
+ function getAgentRankings() {
714
+ const rankings = [];
715
+ if (!existsSync4(AGENTS_DIR)) return rankings;
716
+ const dirs = readdirSync4(AGENTS_DIR, { withFileTypes: true }).filter(
717
+ (d) => d.isDirectory()
718
+ );
719
+ for (const dir of dirs) {
720
+ const cv = getAgentCV(dir.name);
721
+ if (cv.stats.totalIssues > 0) {
722
+ rankings.push({
723
+ agentId: dir.name,
724
+ successRate: cv.stats.successRate,
725
+ totalIssues: cv.stats.totalIssues,
726
+ avgDuration: cv.stats.avgDuration
727
+ });
983
728
  }
984
- if (options.backupOnly) {
985
- return;
729
+ }
730
+ rankings.sort((a, b) => {
731
+ if (b.successRate !== a.successRate) {
732
+ return b.successRate - a.successRate;
986
733
  }
734
+ return b.totalIssues - a.totalIssues;
735
+ });
736
+ return rankings;
737
+ }
738
+ function formatCV(cv) {
739
+ const lines = [
740
+ `# Agent CV: ${cv.agentId}`,
741
+ "",
742
+ `Runtime: ${cv.runtime} (${cv.model})`,
743
+ `Created: ${cv.createdAt}`,
744
+ `Last Active: ${cv.lastActive}`,
745
+ "",
746
+ "## Statistics",
747
+ "",
748
+ `- Total Issues: ${cv.stats.totalIssues}`,
749
+ `- Success Rate: ${(cv.stats.successRate * 100).toFixed(1)}%`,
750
+ `- Successes: ${cv.stats.successCount}`,
751
+ `- Failures: ${cv.stats.failureCount}`,
752
+ `- Abandoned: ${cv.stats.abandonedCount}`,
753
+ `- Avg Duration: ${cv.stats.avgDuration} minutes`,
754
+ ""
755
+ ];
756
+ if (cv.skillsUsed.length > 0) {
757
+ lines.push("## Skills Used");
758
+ lines.push("");
759
+ lines.push(cv.skillsUsed.join(", "));
760
+ lines.push("");
987
761
  }
988
- const spinner = ora2("Syncing...").start();
989
- let totalCreated = 0;
990
- let totalConflicts = 0;
991
- for (const runtime of validTargets) {
992
- spinner.text = `Syncing to ${runtime}...`;
993
- const result = executeSync(runtime, { force: options.force });
994
- totalCreated += result.created.length;
995
- totalConflicts += result.conflicts.length;
996
- if (result.conflicts.length > 0 && !options.force) {
997
- console.log("");
998
- console.log(chalk2.yellow(`Conflicts in ${runtime}:`));
999
- for (const name of result.conflicts) {
1000
- console.log(chalk2.dim(` - ${name} (use --force to overwrite)`));
762
+ if (cv.recentWork.length > 0) {
763
+ lines.push("## Recent Work");
764
+ lines.push("");
765
+ for (const work of cv.recentWork.slice(0, 10)) {
766
+ const statusIcon = {
767
+ success: "\u2713",
768
+ failed: "\u2717",
769
+ abandoned: "\u2298",
770
+ in_progress: "\u25CF"
771
+ }[work.outcome];
772
+ const duration = work.duration ? ` (${work.duration}m)` : "";
773
+ lines.push(`${statusIcon} ${work.issueId}${duration}`);
774
+ if (work.failureReason) {
775
+ lines.push(` Reason: ${work.failureReason}`);
1001
776
  }
1002
777
  }
778
+ lines.push("");
1003
779
  }
1004
- if (totalConflicts > 0 && !options.force) {
1005
- spinner.warn(`Synced ${totalCreated} items, ${totalConflicts} conflicts`);
1006
- console.log("");
1007
- console.log(chalk2.dim("Use --force to overwrite conflicting items."));
1008
- } else {
1009
- spinner.succeed(`Synced ${totalCreated} items to ${validTargets.join(", ")}`);
780
+ return lines.join("\n");
781
+ }
782
+
783
+ // src/lib/agents.ts
784
+ function getAgentDir(agentId) {
785
+ return join5(AGENTS_DIR, agentId);
786
+ }
787
+ function getAgentState(agentId) {
788
+ const stateFile = join5(getAgentDir(agentId), "state.json");
789
+ if (!existsSync5(stateFile)) return null;
790
+ const content = readFileSync4(stateFile, "utf8");
791
+ return JSON.parse(content);
792
+ }
793
+ function saveAgentState(state) {
794
+ const dir = getAgentDir(state.id);
795
+ mkdirSync4(dir, { recursive: true });
796
+ writeFileSync4(
797
+ join5(dir, "state.json"),
798
+ JSON.stringify(state, null, 2)
799
+ );
800
+ }
801
+ function spawnAgent(options) {
802
+ const agentId = `agent-${options.issueId.toLowerCase()}`;
803
+ if (sessionExists(agentId)) {
804
+ throw new Error(`Agent ${agentId} already running. Use 'pan work tell' to message it.`);
1010
805
  }
1011
- const hooksSpinner = ora2("Syncing hooks...").start();
1012
- const hooksResult = syncHooks();
1013
- if (hooksResult.errors.length > 0) {
1014
- hooksSpinner.warn(`Synced ${hooksResult.synced.length} hooks, ${hooksResult.errors.length} errors`);
1015
- for (const error of hooksResult.errors) {
1016
- console.log(chalk2.red(` \u2717 ${error}`));
806
+ initHook(agentId);
807
+ const state = {
808
+ id: agentId,
809
+ issueId: options.issueId,
810
+ workspace: options.workspace,
811
+ runtime: options.runtime || "claude",
812
+ model: options.model || "sonnet",
813
+ status: "starting",
814
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
815
+ // Initialize Phase 4 fields
816
+ handoffCount: 0,
817
+ costSoFar: 0
818
+ };
819
+ saveAgentState(state);
820
+ let prompt = options.prompt || "";
821
+ const { hasWork, items } = checkHook(agentId);
822
+ if (hasWork) {
823
+ const guppPrompt = generateGUPPPrompt(agentId);
824
+ if (guppPrompt) {
825
+ prompt = guppPrompt + "\n\n---\n\n" + prompt;
1017
826
  }
1018
- } else if (hooksResult.synced.length > 0) {
1019
- hooksSpinner.succeed(`Synced ${hooksResult.synced.length} hooks to ~/.panopticon/bin/`);
1020
- } else {
1021
- hooksSpinner.info("No hooks to sync");
1022
827
  }
1023
- }
1024
-
1025
- // src/cli/commands/restore.ts
1026
- init_esm_shims();
1027
- import chalk3 from "chalk";
1028
- import ora3 from "ora";
1029
- import inquirer from "inquirer";
1030
- init_paths();
1031
- async function restoreCommand(timestamp) {
1032
- const backups = listBackups();
1033
- if (backups.length === 0) {
1034
- console.log(chalk3.yellow("No backups found."));
1035
- return;
828
+ const promptFile = join5(getAgentDir(agentId), "initial-prompt.md");
829
+ if (prompt) {
830
+ writeFileSync4(promptFile, prompt);
1036
831
  }
1037
- if (!timestamp) {
1038
- console.log(chalk3.bold("Available backups:\n"));
1039
- for (const backup2 of backups.slice(0, 10)) {
1040
- console.log(` ${chalk3.cyan(backup2.timestamp)} - ${backup2.targets.join(", ")}`);
1041
- }
1042
- if (backups.length > 10) {
1043
- console.log(chalk3.dim(` ... and ${backups.length - 10} more`));
832
+ checkAndSetupHooks();
833
+ writeTaskCache(agentId, options.issueId);
834
+ const claudeCmd = `claude --dangerously-skip-permissions --model ${state.model}`;
835
+ createSession(agentId, options.workspace, claudeCmd, {
836
+ env: {
837
+ PANOPTICON_AGENT_ID: agentId
1044
838
  }
1045
- console.log("");
1046
- const { selected } = await inquirer.prompt([
1047
- {
1048
- type: "list",
1049
- name: "selected",
1050
- message: "Select backup to restore:",
1051
- choices: backups.slice(0, 10).map((b) => ({
1052
- name: `${b.timestamp} (${b.targets.join(", ")})`,
1053
- value: b.timestamp
1054
- }))
839
+ });
840
+ if (prompt) {
841
+ let ready = false;
842
+ for (let i = 0; i < 15; i++) {
843
+ execSync2("sleep 1");
844
+ try {
845
+ const pane = execSync2(`tmux capture-pane -t ${agentId} -p`, { encoding: "utf-8" });
846
+ if (pane.includes("\u276F") || pane.includes(">")) {
847
+ ready = true;
848
+ break;
849
+ }
850
+ } catch {
1055
851
  }
1056
- ]);
1057
- timestamp = selected;
1058
- }
1059
- const { confirm } = await inquirer.prompt([
1060
- {
1061
- type: "confirm",
1062
- name: "confirm",
1063
- message: `Restore backup ${timestamp}? This will overwrite current files.`,
1064
- default: false
1065
852
  }
1066
- ]);
1067
- if (!confirm) {
1068
- console.log(chalk3.dim("Restore cancelled."));
1069
- return;
853
+ if (ready) {
854
+ execSync2(`tmux load-buffer "${promptFile}"`);
855
+ execSync2(`tmux paste-buffer -t ${agentId}`);
856
+ execSync2("sleep 0.5");
857
+ execSync2(`tmux send-keys -t ${agentId} Enter`);
858
+ } else {
859
+ console.error("Claude did not become ready in time, prompt not sent");
860
+ }
1070
861
  }
1071
- const spinner = ora3("Restoring backup...").start();
1072
- try {
1073
- const config = loadConfig();
1074
- const targets = config.sync.targets;
1075
- const targetDirs = {};
1076
- for (const runtime of targets) {
1077
- targetDirs[`${runtime}-skills`] = SYNC_TARGETS[runtime].skills;
1078
- targetDirs[`${runtime}-commands`] = SYNC_TARGETS[runtime].commands;
1079
- targetDirs["skills"] = SYNC_TARGETS[runtime].skills;
1080
- targetDirs["commands"] = SYNC_TARGETS[runtime].commands;
862
+ state.status = "running";
863
+ saveAgentState(state);
864
+ startWork(agentId, options.issueId);
865
+ return state;
866
+ }
867
+ function listRunningAgents() {
868
+ const tmuxSessions = getAgentSessions();
869
+ const tmuxNames = new Set(tmuxSessions.map((s) => s.name));
870
+ const agents = [];
871
+ if (!existsSync5(AGENTS_DIR)) return agents;
872
+ const dirs = readdirSync5(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
873
+ for (const dir of dirs) {
874
+ const state = getAgentState(dir.name);
875
+ if (state) {
876
+ agents.push({
877
+ ...state,
878
+ tmuxActive: tmuxNames.has(state.id)
879
+ });
1081
880
  }
1082
- restoreBackup(timestamp, targetDirs);
1083
- spinner.succeed(`Restored backup: ${timestamp}`);
1084
- } catch (error) {
1085
- spinner.fail("Failed to restore");
1086
- console.error(chalk3.red(error.message));
1087
- process.exit(1);
1088
881
  }
882
+ return agents;
1089
883
  }
1090
-
1091
- // src/cli/commands/backup.ts
1092
- init_esm_shims();
1093
- import chalk4 from "chalk";
1094
- async function backupListCommand(options) {
1095
- const backups = listBackups();
1096
- if (options.json) {
1097
- console.log(JSON.stringify(backups, null, 2));
1098
- return;
884
+ function stopAgent(agentId) {
885
+ const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
886
+ if (sessionExists(normalizedId)) {
887
+ killSession(normalizedId);
1099
888
  }
1100
- if (backups.length === 0) {
1101
- console.log(chalk4.dim("No backups found."));
1102
- console.log(chalk4.dim("Backups are created automatically during sync."));
1103
- return;
889
+ const state = getAgentState(normalizedId);
890
+ if (state) {
891
+ state.status = "stopped";
892
+ saveAgentState(state);
1104
893
  }
1105
- console.log(chalk4.bold("Backups:\n"));
1106
- for (const backup2 of backups) {
1107
- console.log(` ${chalk4.cyan(backup2.timestamp)}`);
1108
- console.log(` ${chalk4.dim("Contains:")} ${backup2.targets.join(", ")}`);
894
+ }
895
+ function messageAgent(agentId, message) {
896
+ const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
897
+ if (!sessionExists(normalizedId)) {
898
+ throw new Error(`Agent ${normalizedId} not running`);
1109
899
  }
1110
- console.log();
1111
- console.log(chalk4.dim(`Total: ${backups.length} backups`));
1112
- console.log(chalk4.dim("Use `pan restore <timestamp>` to restore a backup."));
900
+ sendKeys(normalizedId, message);
901
+ const mailDir = join5(getAgentDir(normalizedId), "mail");
902
+ mkdirSync4(mailDir, { recursive: true });
903
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
904
+ writeFileSync4(
905
+ join5(mailDir, `${timestamp}.md`),
906
+ `# Message
907
+
908
+ ${message}
909
+ `
910
+ );
1113
911
  }
1114
- async function backupCleanCommand(options) {
1115
- const keepCount = parseInt(options.keep || "10", 10);
1116
- const removed = cleanOldBackups(keepCount);
1117
- if (removed === 0) {
1118
- console.log(chalk4.dim(`No backups removed (keeping ${keepCount}).`));
1119
- } else {
1120
- console.log(chalk4.green(`Removed ${removed} old backup(s), keeping ${keepCount}.`));
912
+ function detectCrashedAgents() {
913
+ const agents = listRunningAgents();
914
+ return agents.filter(
915
+ (agent) => agent.status === "running" && !agent.tmuxActive
916
+ );
917
+ }
918
+ function recoverAgent(agentId) {
919
+ const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
920
+ const state = getAgentState(normalizedId);
921
+ if (!state) {
922
+ return null;
923
+ }
924
+ if (sessionExists(normalizedId)) {
925
+ return state;
926
+ }
927
+ const healthFile = join5(getAgentDir(normalizedId), "health.json");
928
+ let health = { consecutiveFailures: 0, killCount: 0, recoveryCount: 0 };
929
+ if (existsSync5(healthFile)) {
930
+ try {
931
+ health = { ...health, ...JSON.parse(readFileSync4(healthFile, "utf-8")) };
932
+ } catch {
933
+ }
1121
934
  }
935
+ health.recoveryCount = (health.recoveryCount || 0) + 1;
936
+ writeFileSync4(healthFile, JSON.stringify(health, null, 2));
937
+ const recoveryPrompt = generateRecoveryPrompt(state);
938
+ const claudeCmd = `claude --dangerously-skip-permissions --model ${state.model} "${recoveryPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
939
+ createSession(normalizedId, state.workspace, claudeCmd);
940
+ state.status = "running";
941
+ state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
942
+ saveAgentState(state);
943
+ return state;
1122
944
  }
1123
-
1124
- // src/cli/commands/skills.ts
1125
- init_esm_shims();
1126
- init_paths();
1127
- import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2 } from "fs";
1128
- import { join as join2 } from "path";
1129
- import chalk5 from "chalk";
1130
- function parseSkillFrontmatter(content) {
1131
- const match = content.match(/^---\n([\s\S]*?)\n---/);
1132
- if (!match) return {};
1133
- const frontmatter = match[1];
1134
- const name = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim();
1135
- const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim();
1136
- return { name, description };
945
+ function generateRecoveryPrompt(state) {
946
+ const lines = [
947
+ "# Agent Recovery",
948
+ "",
949
+ "\u26A0\uFE0F This agent session was recovered after a crash.",
950
+ "",
951
+ "## Previous Context",
952
+ `- Issue: ${state.issueId}`,
953
+ `- Workspace: ${state.workspace}`,
954
+ `- Started: ${state.startedAt}`,
955
+ "",
956
+ "## Recovery Steps",
957
+ "1. Check beads for context: `bd show " + state.issueId + "`",
958
+ "2. Review recent git commits: `git log --oneline -10`",
959
+ "3. Check hook for pending work: `pan work hook check`",
960
+ "4. Resume from last known state",
961
+ "",
962
+ "## GUPP Reminder",
963
+ '> "If there is work on your Hook, YOU MUST RUN IT."',
964
+ ""
965
+ ];
966
+ const { hasWork } = checkHook(state.id);
967
+ if (hasWork) {
968
+ const guppPrompt = generateGUPPPrompt(state.id);
969
+ if (guppPrompt) {
970
+ lines.push("---");
971
+ lines.push("");
972
+ lines.push(guppPrompt);
973
+ }
974
+ }
975
+ return lines.join("\n");
1137
976
  }
1138
- function listSkills() {
1139
- if (!existsSync2(SKILLS_DIR)) return [];
1140
- const skills = [];
1141
- const dirs = readdirSync2(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
1142
- for (const dir of dirs) {
1143
- const skillFile = join2(SKILLS_DIR, dir.name, "SKILL.md");
1144
- if (!existsSync2(skillFile)) continue;
1145
- const content = readFileSync(skillFile, "utf8");
1146
- const { name, description } = parseSkillFrontmatter(content);
1147
- skills.push({
1148
- name: name || dir.name,
1149
- description: description || "(no description)",
1150
- path: skillFile
1151
- });
977
+ function autoRecoverAgents() {
978
+ const crashed = detectCrashedAgents();
979
+ const recovered = [];
980
+ const failed = [];
981
+ for (const agent of crashed) {
982
+ try {
983
+ const result = recoverAgent(agent.id);
984
+ if (result) {
985
+ recovered.push(agent.id);
986
+ } else {
987
+ failed.push(agent.id);
988
+ }
989
+ } catch (error) {
990
+ failed.push(agent.id);
991
+ }
1152
992
  }
1153
- return skills.sort((a, b) => a.name.localeCompare(b.name));
993
+ return { recovered, failed };
1154
994
  }
1155
- async function skillsCommand(options) {
1156
- const skills = listSkills();
1157
- if (options.json) {
1158
- console.log(JSON.stringify(skills, null, 2));
1159
- return;
1160
- }
1161
- console.log(chalk5.bold(`
1162
- Panopticon Skills (${skills.length})
1163
- `));
1164
- if (skills.length === 0) {
1165
- console.log(chalk5.yellow("No skills found."));
1166
- console.log(chalk5.dim("Skills should be in ~/.panopticon/skills/<name>/SKILL.md"));
1167
- return;
995
+ function checkAndSetupHooks() {
996
+ const settingsPath = join5(homedir(), ".claude", "settings.json");
997
+ const hookPath = join5(homedir(), ".panopticon", "bin", "heartbeat-hook");
998
+ if (existsSync5(settingsPath)) {
999
+ try {
1000
+ const settingsContent = readFileSync4(settingsPath, "utf-8");
1001
+ const settings = JSON.parse(settingsContent);
1002
+ const postToolUse = settings?.hooks?.PostToolUse || [];
1003
+ const hookConfigured = postToolUse.some(
1004
+ (hookConfig) => hookConfig.hooks?.some(
1005
+ (hook) => hook.command === hookPath || hook.command?.includes("panopticon") || hook.command?.includes("heartbeat-hook")
1006
+ )
1007
+ );
1008
+ if (hookConfigured) {
1009
+ return;
1010
+ }
1011
+ } catch {
1012
+ }
1168
1013
  }
1169
- for (const skill of skills) {
1170
- console.log(chalk5.cyan(skill.name));
1171
- console.log(chalk5.dim(` ${skill.description}`));
1014
+ try {
1015
+ console.log("Configuring Panopticon heartbeat hooks...");
1016
+ execSync2("pan setup hooks", { stdio: "pipe" });
1017
+ console.log("\u2713 Heartbeat hooks configured");
1018
+ } catch (error) {
1019
+ console.warn("\u26A0 Failed to auto-configure hooks. Run `pan setup hooks` manually.");
1172
1020
  }
1173
- console.log(`
1174
- ${chalk5.dim('Run "pan sync" to sync skills to Claude Code')}`);
1175
1021
  }
1176
-
1177
- // src/cli/commands/work/index.ts
1178
- init_esm_shims();
1022
+ function writeTaskCache(agentId, issueId) {
1023
+ const cacheDir = join5(getAgentDir(agentId));
1024
+ mkdirSync4(cacheDir, { recursive: true });
1025
+ const cacheFile = join5(cacheDir, "current-task.json");
1026
+ writeFileSync4(
1027
+ cacheFile,
1028
+ JSON.stringify({
1029
+ id: issueId,
1030
+ title: `Working on ${issueId}`,
1031
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1032
+ }, null, 2)
1033
+ );
1034
+ }
1179
1035
 
1180
1036
  // src/cli/commands/work/issue.ts
1181
- init_esm_shims();
1182
- init_agents();
1183
- import chalk6 from "chalk";
1184
- import ora4 from "ora";
1185
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1186
- import { join as join6, dirname as dirname2 } from "path";
1187
1037
  function findWorkspace(issueId, labels = []) {
1188
1038
  const normalizedId = issueId.toLowerCase();
1189
1039
  const resolved = resolveProjectFromIssue(issueId, labels);
@@ -1431,8 +1281,6 @@ async function issueCommand(id, options) {
1431
1281
  }
1432
1282
 
1433
1283
  // src/cli/commands/work/status.ts
1434
- init_esm_shims();
1435
- init_agents();
1436
1284
  import chalk7 from "chalk";
1437
1285
  async function statusCommand(options) {
1438
1286
  const agents = listRunningAgents();
@@ -1462,8 +1310,6 @@ async function statusCommand(options) {
1462
1310
  }
1463
1311
 
1464
1312
  // src/cli/commands/work/tell.ts
1465
- init_esm_shims();
1466
- init_agents();
1467
1313
  import chalk8 from "chalk";
1468
1314
  async function tellCommand(id, message) {
1469
1315
  const agentId = id.startsWith("agent-") ? id : `agent-${id.toLowerCase()}`;
@@ -1478,9 +1324,6 @@ async function tellCommand(id, message) {
1478
1324
  }
1479
1325
 
1480
1326
  // src/cli/commands/work/kill.ts
1481
- init_esm_shims();
1482
- init_agents();
1483
- init_tmux();
1484
1327
  import chalk9 from "chalk";
1485
1328
  async function killCommand(id, options) {
1486
1329
  const agentId = id.startsWith("agent-") ? id : `agent-${id.toLowerCase()}`;
@@ -1502,9 +1345,6 @@ async function killCommand(id, options) {
1502
1345
  }
1503
1346
 
1504
1347
  // src/cli/commands/work/pending.ts
1505
- init_esm_shims();
1506
- init_agents();
1507
- init_paths();
1508
1348
  import chalk10 from "chalk";
1509
1349
  import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1510
1350
  import { join as join7 } from "path";
@@ -1534,17 +1374,14 @@ async function pendingCommand() {
1534
1374
  }
1535
1375
 
1536
1376
  // src/cli/commands/work/approve.ts
1537
- init_esm_shims();
1538
- init_agents();
1539
- init_paths();
1540
1377
  import chalk11 from "chalk";
1541
1378
  import ora5 from "ora";
1542
- import { existsSync as existsSync8, writeFileSync as writeFileSync4, readFileSync as readFileSync7 } from "fs";
1379
+ import { existsSync as existsSync8, writeFileSync as writeFileSync5, readFileSync as readFileSync7 } from "fs";
1543
1380
  import { join as join8 } from "path";
1544
- import { homedir } from "os";
1381
+ import { homedir as homedir2 } from "os";
1545
1382
  import { execSync as execSync3 } from "child_process";
1546
1383
  function getLinearApiKey() {
1547
- const envFile = join8(homedir(), ".panopticon.env");
1384
+ const envFile = join8(homedir2(), ".panopticon.env");
1548
1385
  if (existsSync8(envFile)) {
1549
1386
  const content = readFileSync7(envFile, "utf-8");
1550
1387
  const match = content.match(/LINEAR_API_KEY=(.+)/);
@@ -1668,7 +1505,7 @@ async function approveCommand(id, options = {}) {
1668
1505
  state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1669
1506
  saveAgentState(state);
1670
1507
  const approvedFile = join8(AGENTS_DIR, agentId, "approved");
1671
- writeFileSync4(approvedFile, JSON.stringify({
1508
+ writeFileSync5(approvedFile, JSON.stringify({
1672
1509
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1673
1510
  prMerged,
1674
1511
  linearUpdated
@@ -1689,16 +1526,15 @@ async function approveCommand(id, options = {}) {
1689
1526
  }
1690
1527
 
1691
1528
  // src/cli/commands/work/plan.ts
1692
- init_esm_shims();
1693
1529
  import chalk12 from "chalk";
1694
1530
  import ora6 from "ora";
1695
1531
  import inquirer2 from "inquirer";
1696
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
1532
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
1697
1533
  import { join as join9, dirname as dirname3 } from "path";
1698
- import { homedir as homedir2 } from "os";
1534
+ import { homedir as homedir3 } from "os";
1699
1535
  import { execSync as execSync4 } from "child_process";
1700
1536
  function getLinearApiKey2() {
1701
- const envFile = join9(homedir2(), ".panopticon.env");
1537
+ const envFile = join9(homedir3(), ".panopticon.env");
1702
1538
  if (existsSync9(envFile)) {
1703
1539
  const content = readFileSync8(envFile, "utf-8");
1704
1540
  const match = content.match(/LINEAR_API_KEY=(.+)/);
@@ -2084,7 +1920,7 @@ function copyToPRDDirectory(issue, stateContent) {
2084
1920
  mkdirSync5(prdDir, { recursive: true });
2085
1921
  const filename = `${issue.identifier.toLowerCase()}-plan.md`;
2086
1922
  const prdPath = join9(prdDir, filename);
2087
- writeFileSync5(prdPath, stateContent);
1923
+ writeFileSync6(prdPath, stateContent);
2088
1924
  return prdPath;
2089
1925
  } catch {
2090
1926
  return null;
@@ -2201,9 +2037,9 @@ async function planCommand(id, options = {}) {
2201
2037
  const planningDir = join9(outputDir, ".planning");
2202
2038
  mkdirSync5(planningDir, { recursive: true });
2203
2039
  const statePath = join9(planningDir, "STATE.md");
2204
- writeFileSync5(statePath, stateContent);
2040
+ writeFileSync6(statePath, stateContent);
2205
2041
  const workspacePath = join9(planningDir, "WORKSPACE.md");
2206
- writeFileSync5(workspacePath, workspaceContent);
2042
+ writeFileSync6(workspacePath, workspaceContent);
2207
2043
  spinnerCreate.succeed("Context files created");
2208
2044
  const spinnerBeads = ora6("Creating Beads tasks...").start();
2209
2045
  const beadsResult = createBeadsTasks(issueData, tasks);
@@ -2267,7 +2103,6 @@ async function planCommand(id, options = {}) {
2267
2103
  }
2268
2104
 
2269
2105
  // src/cli/commands/work/list.ts
2270
- init_esm_shims();
2271
2106
  import chalk13 from "chalk";
2272
2107
  import ora7 from "ora";
2273
2108
  var PRIORITY_LABELS = {
@@ -2414,12 +2249,11 @@ ${tracker.toUpperCase()} (${issues.length} issues)
2414
2249
  }
2415
2250
 
2416
2251
  // src/cli/commands/work/triage.ts
2417
- init_esm_shims();
2418
2252
  import chalk14 from "chalk";
2419
2253
  import ora8 from "ora";
2420
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
2254
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
2421
2255
  import { join as join10 } from "path";
2422
- import { homedir as homedir3 } from "os";
2256
+ import { homedir as homedir4 } from "os";
2423
2257
  function getTrackerConfig2(trackerType) {
2424
2258
  const config = loadConfig();
2425
2259
  const trackerConfig = config.trackers[trackerType];
@@ -2437,7 +2271,7 @@ function getTrackerConfig2(trackerType) {
2437
2271
  };
2438
2272
  }
2439
2273
  function getTriageStatePath() {
2440
- return join10(homedir3(), ".panopticon", "triage-state.json");
2274
+ return join10(homedir4(), ".panopticon", "triage-state.json");
2441
2275
  }
2442
2276
  function loadTriageState() {
2443
2277
  const path = getTriageStatePath();
@@ -2451,12 +2285,12 @@ function loadTriageState() {
2451
2285
  return { dismissed: [], created: {} };
2452
2286
  }
2453
2287
  function saveTriageState(state) {
2454
- const dir = join10(homedir3(), ".panopticon");
2288
+ const dir = join10(homedir4(), ".panopticon");
2455
2289
  if (!existsSync10(dir)) {
2456
2290
  mkdirSync6(dir, { recursive: true });
2457
2291
  }
2458
2292
  const path = getTriageStatePath();
2459
- writeFileSync6(path, JSON.stringify(state, null, 2));
2293
+ writeFileSync7(path, JSON.stringify(state, null, 2));
2460
2294
  }
2461
2295
  async function triageCommand(id, options = {}) {
2462
2296
  const spinner = ora8("Loading triage queue...").start();
@@ -2592,8 +2426,6 @@ ${secondaryType.toUpperCase()} Issues Pending Triage (${pending.length})
2592
2426
  }
2593
2427
 
2594
2428
  // src/cli/commands/work/hook.ts
2595
- init_esm_shims();
2596
- init_hooks();
2597
2429
  import chalk15 from "chalk";
2598
2430
  async function hookCommand(action, idOrMessage, options = {}) {
2599
2431
  const agentId = process.env.PANOPTICON_AGENT_ID || "default";
@@ -2709,8 +2541,6 @@ async function hookCommand(action, idOrMessage, options = {}) {
2709
2541
  }
2710
2542
 
2711
2543
  // src/cli/commands/work/recover.ts
2712
- init_esm_shims();
2713
- init_agents();
2714
2544
  import chalk16 from "chalk";
2715
2545
  import ora9 from "ora";
2716
2546
  async function recoverCommand(id, options = {}) {
@@ -2787,8 +2617,6 @@ async function recoverCommand(id, options = {}) {
2787
2617
  }
2788
2618
 
2789
2619
  // src/cli/commands/work/cv.ts
2790
- init_esm_shims();
2791
- init_cv();
2792
2620
  import chalk17 from "chalk";
2793
2621
  async function cvCommand(agentId, options = {}) {
2794
2622
  if (options.rankings || !agentId) {
@@ -2831,13 +2659,10 @@ async function cvCommand(agentId, options = {}) {
2831
2659
  }
2832
2660
 
2833
2661
  // src/cli/commands/work/context.ts
2834
- init_esm_shims();
2835
2662
  import chalk18 from "chalk";
2836
2663
 
2837
2664
  // src/lib/context.ts
2838
- init_esm_shims();
2839
- init_paths();
2840
- import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync7, appendFileSync, readdirSync as readdirSync8 } from "fs";
2665
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync8, appendFileSync, readdirSync as readdirSync8 } from "fs";
2841
2666
  import { join as join11 } from "path";
2842
2667
  function getStateFile(agentId) {
2843
2668
  return join11(AGENTS_DIR, agentId, "STATE.md");
@@ -2856,7 +2681,7 @@ function writeAgentState(agentId, state) {
2856
2681
  const dir = join11(AGENTS_DIR, agentId);
2857
2682
  mkdirSync7(dir, { recursive: true });
2858
2683
  const content = generateStateMd(state);
2859
- writeFileSync7(getStateFile(agentId), content);
2684
+ writeFileSync8(getStateFile(agentId), content);
2860
2685
  }
2861
2686
  function updateCheckpoint(agentId, checkpoint, resumePoint) {
2862
2687
  const state = readAgentState(agentId);
@@ -2940,7 +2765,7 @@ function appendSummary(agentId, summary) {
2940
2765
  if (existsSync11(summaryFile)) {
2941
2766
  appendFileSync(summaryFile, "\n---\n\n" + content);
2942
2767
  } else {
2943
- writeFileSync7(summaryFile, "# Work Summaries\n\n" + content);
2768
+ writeFileSync8(summaryFile, "# Work Summaries\n\n" + content);
2944
2769
  }
2945
2770
  }
2946
2771
  function generateSummaryEntry(summary) {
@@ -3218,14 +3043,10 @@ History matches for "${pattern}":
3218
3043
  }
3219
3044
 
3220
3045
  // src/cli/commands/work/health.ts
3221
- init_esm_shims();
3222
3046
  import chalk19 from "chalk";
3223
3047
 
3224
3048
  // src/lib/health.ts
3225
- init_esm_shims();
3226
- init_paths();
3227
- init_agents();
3228
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
3049
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
3229
3050
  import { join as join12 } from "path";
3230
3051
  import { execSync as execSync5 } from "child_process";
3231
3052
  var DEFAULT_PING_TIMEOUT_MS = 30 * 1e3;
@@ -3257,7 +3078,7 @@ function getAgentHealth(agentId) {
3257
3078
  function saveAgentHealth(health) {
3258
3079
  const dir = join12(AGENTS_DIR, health.agentId);
3259
3080
  mkdirSync8(dir, { recursive: true });
3260
- writeFileSync8(getHealthFile(health.agentId), JSON.stringify(health, null, 2));
3081
+ writeFileSync9(getHealthFile(health.agentId), JSON.stringify(health, null, 2));
3261
3082
  }
3262
3083
  function isAgentAlive(agentId) {
3263
3084
  try {
@@ -3469,7 +3290,6 @@ function formatHealthStatus(health) {
3469
3290
  }
3470
3291
 
3471
3292
  // src/cli/commands/work/health.ts
3472
- init_agents();
3473
3293
  async function healthCommand(action, arg, options = {}) {
3474
3294
  const config = {
3475
3295
  pingTimeoutMs: DEFAULT_PING_TIMEOUT_MS,
@@ -3640,14 +3460,12 @@ function registerWorkCommands(program2) {
3640
3460
  }
3641
3461
 
3642
3462
  // src/cli/commands/workspace.ts
3643
- init_esm_shims();
3644
3463
  import chalk20 from "chalk";
3645
3464
  import ora10 from "ora";
3646
- import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9 } from "fs";
3465
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
3647
3466
  import { join as join15, basename as basename2 } from "path";
3648
3467
 
3649
3468
  // src/lib/worktree.ts
3650
- init_esm_shims();
3651
3469
  import { execSync as execSync6 } from "child_process";
3652
3470
  import { mkdirSync as mkdirSync9 } from "fs";
3653
3471
  import { dirname as dirname4 } from "path";
@@ -3698,8 +3516,6 @@ function removeWorktree(repoPath, worktreePath) {
3698
3516
  }
3699
3517
 
3700
3518
  // src/lib/template.ts
3701
- init_esm_shims();
3702
- init_paths();
3703
3519
  import { readFileSync as readFileSync13, existsSync as existsSync14, readdirSync as readdirSync9 } from "fs";
3704
3520
  import { join as join13 } from "path";
3705
3521
  function loadTemplate(templatePath) {
@@ -3757,8 +3573,6 @@ This workspace was created by Panopticon. Use \`bd\` commands to track your work
3757
3573
  }
3758
3574
 
3759
3575
  // src/lib/skills-merge.ts
3760
- init_esm_shims();
3761
- init_paths();
3762
3576
  import {
3763
3577
  existsSync as existsSync15,
3764
3578
  readdirSync as readdirSync10,
@@ -3914,7 +3728,7 @@ async function createCommand(issueId, options) {
3914
3728
  PROJECT_NAME: projectName
3915
3729
  };
3916
3730
  const claudeMd = generateClaudeMd(projectRoot, variables);
3917
- writeFileSync9(join15(workspacePath, "CLAUDE.md"), claudeMd);
3731
+ writeFileSync10(join15(workspacePath, "CLAUDE.md"), claudeMd);
3918
3732
  let skillsResult = { added: [], skipped: [] };
3919
3733
  if (options.skills !== false) {
3920
3734
  spinner.text = "Merging skills...";
@@ -3945,7 +3759,7 @@ async function createCommand(issueId, options) {
3945
3759
  }
3946
3760
  }
3947
3761
  async function listCommand2(options) {
3948
- const { listProjects: listProjects3 } = await import("../projects-6JVKIYIH.js");
3762
+ const { listProjects: listProjects3 } = await import("../projects-54CV437J.js");
3949
3763
  const projects = listProjects3();
3950
3764
  if (projects.length > 0 && options.all) {
3951
3765
  const allWorkspaces = [];
@@ -4062,14 +3876,12 @@ async function destroyCommand(issueId, options) {
4062
3876
  }
4063
3877
 
4064
3878
  // src/cli/commands/install.ts
4065
- init_esm_shims();
4066
- init_paths();
4067
3879
  import chalk21 from "chalk";
4068
3880
  import ora11 from "ora";
4069
3881
  import { execSync as execSync8 } from "child_process";
4070
3882
  import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync14, copyFileSync, readdirSync as readdirSync11, statSync } from "fs";
4071
3883
  import { join as join16 } from "path";
4072
- import { homedir as homedir4, platform } from "os";
3884
+ import { homedir as homedir5, platform } from "os";
4073
3885
  function registerInstallCommand(program2) {
4074
3886
  program2.command("install").description("Install Panopticon prerequisites").option("--check", "Check prerequisites only").option("--minimal", "Skip Traefik and mkcert (use port-based routing)").option("--skip-mkcert", "Skip mkcert/HTTPS setup").option("--skip-docker", "Skip Docker network setup").action(installCommand);
4075
3887
  }
@@ -4165,7 +3977,7 @@ function checkPrerequisites() {
4165
3977
  message: hasBeads ? "installed" : "not found",
4166
3978
  fix: "cargo install beads-cli"
4167
3979
  });
4168
- const hasTtyd = checkCommand("ttyd") || existsSync17(join16(homedir4(), "bin", "ttyd"));
3980
+ const hasTtyd = checkCommand("ttyd") || existsSync17(join16(homedir5(), "bin", "ttyd"));
4169
3981
  results.push({
4170
3982
  name: "ttyd",
4171
3983
  passed: hasTtyd,
@@ -4245,11 +4057,11 @@ async function installCommand(options) {
4245
4057
  spinner.info("Skipping mkcert (not installed)");
4246
4058
  }
4247
4059
  }
4248
- const hasTtyd = checkCommand("ttyd") || existsSync17(join16(homedir4(), "bin", "ttyd"));
4060
+ const hasTtyd = checkCommand("ttyd") || existsSync17(join16(homedir5(), "bin", "ttyd"));
4249
4061
  if (!hasTtyd) {
4250
4062
  spinner.start("Installing ttyd (web terminal)...");
4251
4063
  try {
4252
- const binDir = join16(homedir4(), "bin");
4064
+ const binDir = join16(homedir5(), "bin");
4253
4065
  mkdirSync12(binDir, { recursive: true });
4254
4066
  const ttydPath = join16(binDir, "ttyd");
4255
4067
  const plat2 = detectPlatform();
@@ -4328,20 +4140,11 @@ async function installCommand(options) {
4328
4140
  console.log("");
4329
4141
  }
4330
4142
 
4331
- // src/cli/commands/cloister/index.ts
4332
- init_esm_shims();
4333
-
4334
4143
  // src/cli/commands/cloister/status.ts
4335
- init_esm_shims();
4336
4144
  import chalk22 from "chalk";
4337
4145
 
4338
- // src/lib/cloister/service.ts
4339
- init_esm_shims();
4340
-
4341
4146
  // src/lib/cloister/config.ts
4342
- init_esm_shims();
4343
- init_paths();
4344
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync18, mkdirSync as mkdirSync13 } from "fs";
4147
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync18, mkdirSync as mkdirSync13 } from "fs";
4345
4148
  import { parse, stringify } from "@iarna/toml";
4346
4149
  import { join as join17 } from "path";
4347
4150
  var CLOISTER_CONFIG_FILE = join17(PANOPTICON_HOME, "cloister.toml");
@@ -4470,7 +4273,7 @@ function saveCloisterConfig(config) {
4470
4273
  }
4471
4274
  try {
4472
4275
  const content = stringify(config);
4473
- writeFileSync11(CLOISTER_CONFIG_FILE, content, "utf-8");
4276
+ writeFileSync12(CLOISTER_CONFIG_FILE, content, "utf-8");
4474
4277
  } catch (error) {
4475
4278
  console.error("Failed to save Cloister config:", error);
4476
4279
  throw error;
@@ -4486,7 +4289,6 @@ function getHealthThresholdsMs() {
4486
4289
  }
4487
4290
 
4488
4291
  // src/lib/cloister/health.ts
4489
- init_esm_shims();
4490
4292
  function evaluateHealthState(timeSinceActivityMs, thresholds) {
4491
4293
  if (timeSinceActivityMs < thresholds.stale) {
4492
4294
  return "active";
@@ -4580,8 +4382,6 @@ function getHealthLabel(state) {
4580
4382
  }
4581
4383
 
4582
4384
  // src/lib/cloister/database.ts
4583
- init_esm_shims();
4584
- init_paths();
4585
4385
  import Database from "better-sqlite3";
4586
4386
  import { join as join18 } from "path";
4587
4387
  import { existsSync as existsSync19, mkdirSync as mkdirSync14 } from "fs";
@@ -4655,21 +4455,16 @@ function cleanupOldEvents(database = getHealthDatabase(), retentionDays = RETENT
4655
4455
  }
4656
4456
 
4657
4457
  // src/lib/cloister/specialists.ts
4658
- init_esm_shims();
4659
- init_paths();
4660
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync12, existsSync as existsSync21, mkdirSync as mkdirSync15, readdirSync as readdirSync13, unlinkSync as unlinkSync2 } from "fs";
4458
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync13, existsSync as existsSync21, mkdirSync as mkdirSync15, readdirSync as readdirSync13, unlinkSync as unlinkSync2 } from "fs";
4661
4459
  import { join as join21, basename as basename4 } from "path";
4662
4460
  import { execSync as execSync9 } from "child_process";
4663
4461
 
4664
4462
  // src/lib/cost-parsers/jsonl-parser.ts
4665
- init_esm_shims();
4666
4463
  import { existsSync as existsSync20, readFileSync as readFileSync16, readdirSync as readdirSync12, statSync as statSync2 } from "fs";
4667
4464
  import { join as join20, basename as basename3 } from "path";
4668
- import { homedir as homedir5 } from "os";
4465
+ import { homedir as homedir6 } from "os";
4669
4466
 
4670
4467
  // src/lib/cost.ts
4671
- init_esm_shims();
4672
- init_paths();
4673
4468
  import { join as join19 } from "path";
4674
4469
  var DEFAULT_PRICING = [
4675
4470
  // Anthropic
@@ -4710,7 +4505,7 @@ function getPricing(provider, model) {
4710
4505
  var BUDGETS_FILE = join19(COSTS_DIR, "budgets.json");
4711
4506
 
4712
4507
  // src/lib/cost-parsers/jsonl-parser.ts
4713
- var CLAUDE_PROJECTS_DIR = join20(homedir5(), ".claude", "projects");
4508
+ var CLAUDE_PROJECTS_DIR = join20(homedir6(), ".claude", "projects");
4714
4509
  function getProjectDirs() {
4715
4510
  if (!existsSync20(CLAUDE_PROJECTS_DIR)) {
4716
4511
  return [];
@@ -4735,6 +4530,19 @@ function getSessionFiles(projectDir) {
4735
4530
  }
4736
4531
  });
4737
4532
  }
4533
+ function getAllSessionFiles() {
4534
+ const files = [];
4535
+ for (const projectDir of getProjectDirs()) {
4536
+ files.push(...getSessionFiles(projectDir));
4537
+ }
4538
+ return files.sort((a, b) => {
4539
+ try {
4540
+ return statSync2(b).mtime.getTime() - statSync2(a).mtime.getTime();
4541
+ } catch {
4542
+ return 0;
4543
+ }
4544
+ });
4545
+ }
4738
4546
  function normalizeModelName(model) {
4739
4547
  if (model.includes("claude")) {
4740
4548
  let normalizedModel = model;
@@ -4887,7 +4695,7 @@ function saveRegistry(registry) {
4887
4695
  registry.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
4888
4696
  try {
4889
4697
  const content = JSON.stringify(registry, null, 2);
4890
- writeFileSync12(REGISTRY_FILE, content, "utf-8");
4698
+ writeFileSync13(REGISTRY_FILE, content, "utf-8");
4891
4699
  } catch (error) {
4892
4700
  console.error("Failed to save specialist registry:", error);
4893
4701
  throw error;
@@ -4908,6 +4716,23 @@ function getSessionId(name) {
4908
4716
  return null;
4909
4717
  }
4910
4718
  }
4719
+ function clearSessionId(name) {
4720
+ const sessionFile = getSessionFilePath(name);
4721
+ if (!existsSync21(sessionFile)) {
4722
+ return false;
4723
+ }
4724
+ try {
4725
+ unlinkSync2(sessionFile);
4726
+ return true;
4727
+ } catch (error) {
4728
+ console.error(`Failed to delete session file for ${name}:`, error);
4729
+ throw error;
4730
+ }
4731
+ }
4732
+ function getSpecialistMetadata(name) {
4733
+ const registry = loadRegistry();
4734
+ return registry.specialists.find((s) => s.name === name) || null;
4735
+ }
4911
4736
  function updateSpecialistMetadata(name, updates) {
4912
4737
  const registry = loadRegistry();
4913
4738
  const index = registry.specialists.findIndex((s) => s.name === name);
@@ -4941,6 +4766,34 @@ function recordWake(name, sessionId) {
4941
4766
  function getEnabledSpecialists() {
4942
4767
  return getAllSpecialists().filter((s) => s.enabled);
4943
4768
  }
4769
+ function findSessionFile(sessionId) {
4770
+ try {
4771
+ const allFiles = getAllSessionFiles();
4772
+ for (const file of allFiles) {
4773
+ const fileSessionId = basename4(file, ".jsonl");
4774
+ if (fileSessionId === sessionId) {
4775
+ return file;
4776
+ }
4777
+ }
4778
+ } catch {
4779
+ }
4780
+ return null;
4781
+ }
4782
+ function countContextTokens(name) {
4783
+ const sessionId = getSessionId(name);
4784
+ if (!sessionId) {
4785
+ return null;
4786
+ }
4787
+ const sessionFile = findSessionFile(sessionId);
4788
+ if (!sessionFile) {
4789
+ return null;
4790
+ }
4791
+ const sessionUsage = parseClaudeSession(sessionFile);
4792
+ if (!sessionUsage) {
4793
+ return null;
4794
+ }
4795
+ return sessionUsage.usage.inputTokens + sessionUsage.usage.outputTokens + (sessionUsage.usage.cacheReadTokens || 0) + (sessionUsage.usage.cacheWriteTokens || 0);
4796
+ }
4944
4797
  function isRunning(name) {
4945
4798
  const tmuxSession = getTmuxSessionName(name);
4946
4799
  try {
@@ -4950,6 +4803,37 @@ function isRunning(name) {
4950
4803
  return false;
4951
4804
  }
4952
4805
  }
4806
+ function getSpecialistStatus(name) {
4807
+ const metadata = getSpecialistMetadata(name) || {
4808
+ name,
4809
+ displayName: name,
4810
+ description: "",
4811
+ enabled: false,
4812
+ autoWake: false
4813
+ };
4814
+ const sessionId = getSessionId(name);
4815
+ const running = isRunning(name);
4816
+ const contextTokens = countContextTokens(name);
4817
+ let state;
4818
+ if (running) {
4819
+ state = "active";
4820
+ } else if (sessionId) {
4821
+ state = "sleeping";
4822
+ } else {
4823
+ state = "uninitialized";
4824
+ }
4825
+ return {
4826
+ ...metadata,
4827
+ sessionId: sessionId || void 0,
4828
+ contextTokens: contextTokens || void 0,
4829
+ state,
4830
+ isRunning: running,
4831
+ tmuxSession: getTmuxSessionName(name)
4832
+ };
4833
+ }
4834
+ function getAllSpecialistStatus() {
4835
+ return getAllSpecialists().map((metadata) => getSpecialistStatus(metadata.name));
4836
+ }
4953
4837
  async function initializeSpecialist(name) {
4954
4838
  if (isRunning(name)) {
4955
4839
  return {
@@ -5022,21 +4906,15 @@ async function initializeEnabledSpecialists() {
5022
4906
  }
5023
4907
  return results;
5024
4908
  }
5025
-
5026
- // src/lib/runtimes/index.ts
5027
- init_esm_shims();
5028
-
5029
- // src/lib/runtimes/types.ts
5030
- init_esm_shims();
4909
+ function checkSpecialistQueue(specialistName) {
4910
+ return checkHook(specialistName);
4911
+ }
5031
4912
 
5032
4913
  // src/lib/runtimes/claude-code.ts
5033
- init_esm_shims();
5034
- init_agents();
5035
- init_tmux();
5036
- import { existsSync as existsSync22, readFileSync as readFileSync18, statSync as statSync3, mkdirSync as mkdirSync16 } from "fs";
4914
+ import { existsSync as existsSync22, readFileSync as readFileSync18, statSync as statSync3, mkdirSync as mkdirSync16, writeFileSync as writeFileSync14 } from "fs";
5037
4915
  import { join as join22 } from "path";
5038
- import { homedir as homedir6 } from "os";
5039
- var CLAUDE_PROJECTS_DIR2 = join22(homedir6(), ".claude", "projects");
4916
+ import { homedir as homedir7 } from "os";
4917
+ var CLAUDE_PROJECTS_DIR2 = join22(homedir7(), ".claude", "projects");
5040
4918
  var ClaudeCodeRuntime = class {
5041
4919
  name = "claude-code";
5042
4920
  /**
@@ -5136,7 +5014,7 @@ var ClaudeCodeRuntime = class {
5136
5014
  * Read active heartbeat file if it exists
5137
5015
  */
5138
5016
  getActiveHeartbeat(agentId) {
5139
- const heartbeatPath = join22(homedir6(), ".panopticon", "heartbeats", `${agentId}.json`);
5017
+ const heartbeatPath = join22(homedir7(), ".panopticon", "heartbeats", `${agentId}.json`);
5140
5018
  if (!existsSync22(heartbeatPath)) {
5141
5019
  return null;
5142
5020
  }
@@ -5248,8 +5126,7 @@ var ClaudeCodeRuntime = class {
5248
5126
  const mailDir = join22(getAgentDir(agentId), "mail");
5249
5127
  mkdirSync16(mailDir, { recursive: true });
5250
5128
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5251
- const fs = __require("fs");
5252
- fs.writeFileSync(
5129
+ writeFileSync14(
5253
5130
  join22(mailDir, `${timestamp}.md`),
5254
5131
  `# Message
5255
5132
 
@@ -5268,8 +5145,7 @@ ${message}
5268
5145
  const state = getAgentState(agentId);
5269
5146
  if (state) {
5270
5147
  state.status = "stopped";
5271
- const { saveAgentState: saveAgentState2 } = (init_agents(), __toCommonJS(agents_exports));
5272
- saveAgentState2(state);
5148
+ saveAgentState(state);
5273
5149
  }
5274
5150
  }
5275
5151
  /**
@@ -5356,7 +5232,6 @@ function createClaudeCodeRuntime() {
5356
5232
  }
5357
5233
 
5358
5234
  // src/lib/runtimes/index.ts
5359
- init_agents();
5360
5235
  var RuntimeRegistry = class {
5361
5236
  runtimes = /* @__PURE__ */ new Map();
5362
5237
  /**
@@ -5417,11 +5292,7 @@ function getRuntimeForAgent(agentId) {
5417
5292
  return getGlobalRegistry().getRuntimeForAgent(agentId);
5418
5293
  }
5419
5294
 
5420
- // src/lib/cloister/service.ts
5421
- init_agents();
5422
-
5423
5295
  // src/lib/cloister/triggers.ts
5424
- init_esm_shims();
5425
5296
  import { existsSync as existsSync23 } from "fs";
5426
5297
  import { join as join23 } from "path";
5427
5298
  import { execSync as execSync10 } from "child_process";
@@ -5656,13 +5527,10 @@ function checkAllTriggers(agentId, workspace, issueId, currentModel, health, con
5656
5527
  }
5657
5528
 
5658
5529
  // src/lib/cloister/handoff.ts
5659
- init_esm_shims();
5660
- init_agents();
5661
- import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync17 } from "fs";
5530
+ import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync17 } from "fs";
5662
5531
  import { join as join25 } from "path";
5663
5532
 
5664
5533
  // src/lib/cloister/handoff-context.ts
5665
- init_esm_shims();
5666
5534
  import { existsSync as existsSync24, readFileSync as readFileSync19 } from "fs";
5667
5535
  import { join as join24 } from "path";
5668
5536
  import { execSync as execSync11 } from "child_process";
@@ -5846,7 +5714,6 @@ function buildHandoffPrompt(context, additionalInstructions) {
5846
5714
  }
5847
5715
 
5848
5716
  // src/lib/cloister/handoff.ts
5849
- init_tmux();
5850
5717
  async function performHandoff(agentId, options) {
5851
5718
  const state = getAgentState(agentId);
5852
5719
  if (!state) {
@@ -5885,7 +5752,7 @@ async function performKillAndSpawn(state, options) {
5885
5752
  const handoffDir = join25(getAgentDir(state.id), "handoffs");
5886
5753
  mkdirSync17(handoffDir, { recursive: true });
5887
5754
  const handoffFile = join25(handoffDir, `handoff-${Date.now()}.md`);
5888
- writeFileSync13(handoffFile, prompt);
5755
+ writeFileSync15(handoffFile, prompt);
5889
5756
  const newState = spawnAgent({
5890
5757
  issueId: state.issueId,
5891
5758
  workspace: state.workspace,
@@ -5957,8 +5824,6 @@ function sleep(ms) {
5957
5824
  }
5958
5825
 
5959
5826
  // src/lib/cloister/handoff-logger.ts
5960
- init_esm_shims();
5961
- init_paths();
5962
5827
  import { existsSync as existsSync26, mkdirSync as mkdirSync18, appendFileSync as appendFileSync3, readFileSync as readFileSync20 } from "fs";
5963
5828
  import { join as join26 } from "path";
5964
5829
  var HANDOFF_LOG_FILE = join26(PANOPTICON_HOME, "logs", "handoffs.jsonl");
@@ -6145,7 +6010,7 @@ var CloisterService = class {
6145
6010
  }
6146
6011
  }
6147
6012
  }
6148
- await this.checkHandoffTriggers(agentHealths);
6013
+ void this.checkHandoffTriggers(agentHealths);
6149
6014
  } catch (error) {
6150
6015
  console.error("Cloister health check failed:", error);
6151
6016
  this.emit({ type: "error", error });
@@ -6436,7 +6301,6 @@ async function statusCommand2(options) {
6436
6301
  }
6437
6302
 
6438
6303
  // src/cli/commands/cloister/start.ts
6439
- init_esm_shims();
6440
6304
  import chalk23 from "chalk";
6441
6305
  async function startCommand() {
6442
6306
  const service = getCloisterService();
@@ -6450,7 +6314,6 @@ async function startCommand() {
6450
6314
  }
6451
6315
 
6452
6316
  // src/cli/commands/cloister/stop.ts
6453
- init_esm_shims();
6454
6317
  import chalk24 from "chalk";
6455
6318
  async function stopCommand(options) {
6456
6319
  const service = getCloisterService();
@@ -6483,16 +6346,12 @@ function registerCloisterCommands(program2) {
6483
6346
  cloister.command("emergency-stop").description("Emergency stop - kill ALL agents immediately").action(() => stopCommand({ emergency: true }));
6484
6347
  }
6485
6348
 
6486
- // src/cli/commands/setup/index.ts
6487
- init_esm_shims();
6488
-
6489
6349
  // src/cli/commands/setup/hooks.ts
6490
- init_esm_shims();
6491
6350
  import chalk25 from "chalk";
6492
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync14, existsSync as existsSync27, mkdirSync as mkdirSync19, copyFileSync as copyFileSync2, chmodSync } from "fs";
6351
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync16, existsSync as existsSync27, mkdirSync as mkdirSync19, copyFileSync as copyFileSync2, chmodSync as chmodSync2 } from "fs";
6493
6352
  import { join as join27 } from "path";
6494
6353
  import { execSync as execSync12 } from "child_process";
6495
- import { homedir as homedir7 } from "os";
6354
+ import { homedir as homedir8 } from "os";
6496
6355
  function checkJqInstalled() {
6497
6356
  try {
6498
6357
  execSync12("which jq", { stdio: "pipe" });
@@ -6561,7 +6420,7 @@ async function setupHooksCommand() {
6561
6420
  } else {
6562
6421
  console.log(chalk25.green("\u2713 jq is installed"));
6563
6422
  }
6564
- const panopticonHome = join27(homedir7(), ".panopticon");
6423
+ const panopticonHome = join27(homedir8(), ".panopticon");
6565
6424
  const binDir = join27(panopticonHome, "bin");
6566
6425
  const heartbeatsDir = join27(panopticonHome, "heartbeats");
6567
6426
  if (!existsSync27(binDir)) {
@@ -6576,9 +6435,9 @@ async function setupHooksCommand() {
6576
6435
  const scriptDest = join27(binDir, "heartbeat-hook");
6577
6436
  let sourcePath = scriptSource;
6578
6437
  if (!existsSync27(sourcePath)) {
6579
- const { fileURLToPath: fileURLToPath2 } = await import("url");
6580
- const { dirname: dirname5 } = await import("path");
6581
- const __dirname3 = dirname5(fileURLToPath2(import.meta.url));
6438
+ const { fileURLToPath: fileURLToPath3 } = await import("url");
6439
+ const { dirname: dirname6 } = await import("path");
6440
+ const __dirname3 = dirname6(fileURLToPath3(import.meta.url));
6582
6441
  const installedSource = join27(__dirname3, "..", "..", "..", "scripts", "heartbeat-hook");
6583
6442
  if (existsSync27(installedSource)) {
6584
6443
  sourcePath = installedSource;
@@ -6590,9 +6449,9 @@ async function setupHooksCommand() {
6590
6449
  }
6591
6450
  }
6592
6451
  copyFileSync2(sourcePath, scriptDest);
6593
- chmodSync(scriptDest, 493);
6452
+ chmodSync2(scriptDest, 493);
6594
6453
  console.log(chalk25.green("\u2713 Installed heartbeat-hook script"));
6595
- const claudeDir = join27(homedir7(), ".claude");
6454
+ const claudeDir = join27(homedir8(), ".claude");
6596
6455
  const settingsPath = join27(claudeDir, "settings.json");
6597
6456
  let settings = {};
6598
6457
  if (existsSync27(settingsPath)) {
@@ -6630,7 +6489,7 @@ async function setupHooksCommand() {
6630
6489
  }
6631
6490
  ]
6632
6491
  });
6633
- writeFileSync14(settingsPath, JSON.stringify(settings, null, 2));
6492
+ writeFileSync16(settingsPath, JSON.stringify(settings, null, 2));
6634
6493
  console.log(chalk25.green("\u2713 Updated Claude Code settings.json"));
6635
6494
  console.log(chalk25.green.bold("\n\u2713 Setup complete!\n"));
6636
6495
  console.log(chalk25.dim("Heartbeat hooks are now active. When you run agents via"));
@@ -6644,24 +6503,341 @@ function registerSetupCommands(program2) {
6644
6503
  setup.command("hooks").description("Configure Claude Code hooks for heartbeat tracking").action(setupHooksCommand);
6645
6504
  }
6646
6505
 
6647
- // src/cli/commands/project.ts
6648
- init_esm_shims();
6506
+ // src/cli/commands/specialists/list.ts
6649
6507
  import chalk26 from "chalk";
6508
+ function listCommand3(options) {
6509
+ const specialists = getAllSpecialistStatus();
6510
+ if (options.json) {
6511
+ console.log(JSON.stringify(specialists, null, 2));
6512
+ return;
6513
+ }
6514
+ console.log(chalk26.bold("\nSpecialist Agents:\n"));
6515
+ if (specialists.length === 0) {
6516
+ console.log(chalk26.dim("No specialists configured."));
6517
+ console.log("");
6518
+ return;
6519
+ }
6520
+ for (const specialist of specialists) {
6521
+ displaySpecialist(specialist);
6522
+ }
6523
+ console.log("");
6524
+ }
6525
+ function displaySpecialist(specialist) {
6526
+ const statusIcon = getStatusIcon(specialist);
6527
+ const statusColor = getStatusColor(specialist);
6528
+ const enabledBadge = specialist.enabled ? chalk26.green("enabled") : chalk26.dim("disabled");
6529
+ console.log(`${statusIcon} ${chalk26.bold(specialist.displayName)} (${enabledBadge})`);
6530
+ console.log(` ${chalk26.dim(specialist.description)}`);
6531
+ console.log(` Status: ${statusColor(specialist.state)}`);
6532
+ if (specialist.isRunning) {
6533
+ console.log(` Running: ${chalk26.cyan(specialist.tmuxSession)}`);
6534
+ }
6535
+ if (specialist.sessionId) {
6536
+ const shortId = specialist.sessionId.substring(0, 8);
6537
+ console.log(` Session: ${chalk26.dim(shortId + "...")}`);
6538
+ }
6539
+ if (specialist.contextTokens) {
6540
+ const tokensFormatted = specialist.contextTokens.toLocaleString();
6541
+ console.log(` Context: ${chalk26.dim(tokensFormatted + " tokens")}`);
6542
+ }
6543
+ if (specialist.lastWake) {
6544
+ const lastWake = new Date(specialist.lastWake);
6545
+ const relative = getRelativeTime(lastWake);
6546
+ console.log(` Last wake: ${chalk26.dim(relative)}`);
6547
+ }
6548
+ console.log("");
6549
+ }
6550
+ function getStatusIcon(specialist) {
6551
+ if (!specialist.enabled) return chalk26.dim("\u25CB");
6552
+ if (specialist.isRunning) return chalk26.green("\u25CF");
6553
+ if (specialist.state === "sleeping") return chalk26.yellow("\u25CF");
6554
+ return chalk26.dim("\u25CB");
6555
+ }
6556
+ function getStatusColor(specialist) {
6557
+ if (specialist.isRunning) return chalk26.green;
6558
+ if (specialist.state === "sleeping") return chalk26.yellow;
6559
+ return chalk26.dim;
6560
+ }
6561
+ function getRelativeTime(date) {
6562
+ const now = /* @__PURE__ */ new Date();
6563
+ const diff = now.getTime() - date.getTime();
6564
+ const seconds = Math.floor(diff / 1e3);
6565
+ const minutes = Math.floor(seconds / 60);
6566
+ const hours = Math.floor(minutes / 60);
6567
+ const days = Math.floor(hours / 24);
6568
+ if (days > 0) return `${days}d ago`;
6569
+ if (hours > 0) return `${hours}h ago`;
6570
+ if (minutes > 0) return `${minutes}m ago`;
6571
+ return "just now";
6572
+ }
6573
+
6574
+ // src/cli/commands/specialists/wake.ts
6575
+ import chalk27 from "chalk";
6576
+ import { execSync as execSync13 } from "child_process";
6577
+ function wakeCommand(name, options) {
6578
+ const validNames = ["merge-agent", "review-agent", "test-agent"];
6579
+ if (!validNames.includes(name)) {
6580
+ console.log(chalk27.red(`
6581
+ Error: Unknown specialist '${name}'`));
6582
+ console.log(`Valid specialists: ${validNames.join(", ")}
6583
+ `);
6584
+ process.exit(1);
6585
+ }
6586
+ const specialistName = name;
6587
+ const status = getSpecialistStatus(specialistName);
6588
+ console.log(chalk27.bold(`
6589
+ Waking ${status.displayName}...
6590
+ `));
6591
+ if (status.isRunning) {
6592
+ console.log(chalk27.yellow(`Specialist is already running in tmux session: ${status.tmuxSession}`));
6593
+ if (options.task) {
6594
+ console.log(chalk27.dim("\nSending task message..."));
6595
+ try {
6596
+ const escapedTask = options.task.replace(/'/g, "'\\''");
6597
+ execSync13(`tmux send-keys -t "${status.tmuxSession}" '${escapedTask}' C-m`, { encoding: "utf-8" });
6598
+ console.log(chalk27.green("\u2713 Task message sent"));
6599
+ } catch (error) {
6600
+ console.log(chalk27.red(`Failed to send message: ${error.message}`));
6601
+ }
6602
+ } else {
6603
+ console.log(chalk27.dim("Use --task to send a message to the running specialist"));
6604
+ }
6605
+ console.log("");
6606
+ return;
6607
+ }
6608
+ if (!status.enabled) {
6609
+ console.log(chalk27.yellow(`Warning: Specialist '${specialistName}' is disabled in registry`));
6610
+ console.log(chalk27.dim("You can still wake it manually, but it won't auto-wake\n"));
6611
+ }
6612
+ const sessionId = getSessionId(specialistName);
6613
+ const tmuxSession = getTmuxSessionName(specialistName);
6614
+ const cwd = process.env.HOME || "/home/eltmon";
6615
+ try {
6616
+ let claudeCmd = "claude --dangerously-skip-permissions";
6617
+ if (sessionId) {
6618
+ claudeCmd += ` --resume ${sessionId}`;
6619
+ console.log(chalk27.dim(`Resuming session: ${sessionId.substring(0, 8)}...`));
6620
+ } else {
6621
+ console.log(chalk27.dim("Starting fresh session (no previous session found)"));
6622
+ }
6623
+ console.log(chalk27.dim(`Creating tmux session: ${tmuxSession}`));
6624
+ execSync13(
6625
+ `tmux new-session -d -s "${tmuxSession}" -c "${cwd}" "${claudeCmd}"`,
6626
+ { encoding: "utf-8" }
6627
+ );
6628
+ execSync13("sleep 2", { encoding: "utf-8" });
6629
+ if (options.task) {
6630
+ console.log(chalk27.dim("Sending task message..."));
6631
+ const escapedTask = options.task.replace(/'/g, "'\\''");
6632
+ execSync13(`tmux send-keys -t "${tmuxSession}" '${escapedTask}' C-m`, { encoding: "utf-8" });
6633
+ }
6634
+ recordWake(specialistName);
6635
+ console.log(chalk27.green(`\u2713 Specialist ${specialistName} woken up successfully`));
6636
+ console.log(chalk27.dim(` Tmux session: ${tmuxSession}`));
6637
+ console.log(chalk27.dim(` Attach with: tmux attach -t ${tmuxSession}`));
6638
+ console.log("");
6639
+ } catch (error) {
6640
+ console.log(chalk27.red(`
6641
+ Failed to wake specialist: ${error.message}
6642
+ `));
6643
+ process.exit(1);
6644
+ }
6645
+ }
6646
+
6647
+ // src/cli/commands/specialists/queue.ts
6648
+ import chalk28 from "chalk";
6649
+ function queueCommand(name, options) {
6650
+ const validNames = ["merge-agent", "review-agent", "test-agent"];
6651
+ if (!validNames.includes(name)) {
6652
+ console.log(chalk28.red(`
6653
+ Error: Unknown specialist '${name}'`));
6654
+ console.log(`Valid specialists: ${validNames.join(", ")}
6655
+ `);
6656
+ process.exit(1);
6657
+ }
6658
+ const specialistName = name;
6659
+ const metadata = getSpecialistMetadata(specialistName);
6660
+ if (!metadata) {
6661
+ console.log(chalk28.red(`
6662
+ Error: Specialist '${specialistName}' not found in registry
6663
+ `));
6664
+ process.exit(1);
6665
+ }
6666
+ const queueStatus = checkSpecialistQueue(specialistName);
6667
+ if (options.json) {
6668
+ console.log(JSON.stringify(queueStatus, null, 2));
6669
+ return;
6670
+ }
6671
+ console.log(chalk28.bold(`
6672
+ ${metadata.displayName} Queue:
6673
+ `));
6674
+ if (!queueStatus.hasWork) {
6675
+ console.log(chalk28.dim("Queue is empty - no pending work"));
6676
+ console.log("");
6677
+ return;
6678
+ }
6679
+ console.log(`Total items: ${chalk28.bold(queueStatus.items.length.toString())}`);
6680
+ if (queueStatus.urgentCount > 0) {
6681
+ console.log(`Urgent items: ${chalk28.red.bold(queueStatus.urgentCount.toString())}`);
6682
+ }
6683
+ console.log("");
6684
+ for (let i = 0; i < queueStatus.items.length; i++) {
6685
+ const item = queueStatus.items[i];
6686
+ const position = i + 1;
6687
+ const priorityColor = getPriorityColor(item.priority);
6688
+ console.log(`${position}. ${priorityColor(item.priority.toUpperCase())}`);
6689
+ console.log(` ID: ${chalk28.dim(item.id)}`);
6690
+ console.log(` Source: ${chalk28.dim(item.source)}`);
6691
+ if (item.payload) {
6692
+ const payload = item.payload;
6693
+ if (payload.issueId) {
6694
+ console.log(` Issue: ${chalk28.cyan(payload.issueId)}`);
6695
+ }
6696
+ if (payload.prUrl) {
6697
+ console.log(` PR: ${chalk28.dim(payload.prUrl)}`);
6698
+ }
6699
+ if (payload.workspace) {
6700
+ console.log(` Workspace: ${chalk28.dim(payload.workspace)}`);
6701
+ }
6702
+ if (payload.branch) {
6703
+ console.log(` Branch: ${chalk28.dim(payload.branch)}`);
6704
+ }
6705
+ if (payload.filesChanged && Array.isArray(payload.filesChanged)) {
6706
+ const fileCount = payload.filesChanged.length;
6707
+ console.log(` Files: ${chalk28.dim(fileCount + " changed")}`);
6708
+ }
6709
+ }
6710
+ const createdAt = new Date(item.createdAt);
6711
+ const age = getAge(createdAt);
6712
+ console.log(` Age: ${chalk28.dim(age)}`);
6713
+ console.log("");
6714
+ }
6715
+ }
6716
+ function getPriorityColor(priority) {
6717
+ switch (priority.toLowerCase()) {
6718
+ case "urgent":
6719
+ return chalk28.red.bold;
6720
+ case "high":
6721
+ return chalk28.yellow;
6722
+ case "normal":
6723
+ return chalk28.white;
6724
+ case "low":
6725
+ return chalk28.dim;
6726
+ default:
6727
+ return chalk28.white;
6728
+ }
6729
+ }
6730
+ function getAge(date) {
6731
+ const now = /* @__PURE__ */ new Date();
6732
+ const diff = now.getTime() - date.getTime();
6733
+ const seconds = Math.floor(diff / 1e3);
6734
+ const minutes = Math.floor(seconds / 60);
6735
+ const hours = Math.floor(minutes / 60);
6736
+ const days = Math.floor(hours / 24);
6737
+ if (days > 0) return `${days}d ago`;
6738
+ if (hours > 0) return `${hours}h ago`;
6739
+ if (minutes > 0) return `${minutes}m ago`;
6740
+ return "just now";
6741
+ }
6742
+
6743
+ // src/cli/commands/specialists/reset.ts
6744
+ import chalk29 from "chalk";
6745
+ import { execSync as execSync14 } from "child_process";
6746
+ import * as readline from "readline";
6747
+ async function resetCommand(name, options) {
6748
+ const validNames = ["merge-agent", "review-agent", "test-agent"];
6749
+ if (!validNames.includes(name)) {
6750
+ console.log(chalk29.red(`
6751
+ Error: Unknown specialist '${name}'`));
6752
+ console.log(`Valid specialists: ${validNames.join(", ")}
6753
+ `);
6754
+ process.exit(1);
6755
+ }
6756
+ const specialistName = name;
6757
+ const status = getSpecialistStatus(specialistName);
6758
+ console.log(chalk29.bold(`
6759
+ Resetting ${status.displayName}...
6760
+ `));
6761
+ console.log(chalk29.dim("Current state:"));
6762
+ console.log(` Status: ${status.state}`);
6763
+ if (status.isRunning) {
6764
+ console.log(` Running: ${chalk29.yellow("yes")} (tmux: ${status.tmuxSession})`);
6765
+ }
6766
+ if (status.sessionId) {
6767
+ console.log(` Session: ${status.sessionId.substring(0, 8)}...`);
6768
+ }
6769
+ if (status.contextTokens) {
6770
+ console.log(` Context: ${status.contextTokens.toLocaleString()} tokens`);
6771
+ }
6772
+ console.log("");
6773
+ if (!options.force) {
6774
+ const confirmed = await confirm(
6775
+ "This will clear the session and all context. Continue?"
6776
+ );
6777
+ if (!confirmed) {
6778
+ console.log(chalk29.dim("Reset cancelled\n"));
6779
+ return;
6780
+ }
6781
+ }
6782
+ if (status.isRunning) {
6783
+ console.log(chalk29.dim("Stopping tmux session..."));
6784
+ try {
6785
+ execSync14(`tmux kill-session -t "${status.tmuxSession}"`, { encoding: "utf-8", stdio: "ignore" });
6786
+ console.log(chalk29.green("\u2713 Tmux session stopped"));
6787
+ } catch (error) {
6788
+ console.log(chalk29.yellow("\u26A0 Failed to stop tmux session (may not be running)"));
6789
+ }
6790
+ }
6791
+ console.log(chalk29.dim("Clearing session file..."));
6792
+ const cleared = clearSessionId(specialistName);
6793
+ if (cleared) {
6794
+ console.log(chalk29.green("\u2713 Session file cleared"));
6795
+ } else {
6796
+ console.log(chalk29.dim("No session file to clear"));
6797
+ }
6798
+ console.log(chalk29.green(`
6799
+ \u2713 Specialist ${specialistName} has been reset`));
6800
+ console.log(chalk29.dim("Next wake will start a fresh session\n"));
6801
+ }
6802
+ function confirm(question) {
6803
+ const rl = readline.createInterface({
6804
+ input: process.stdin,
6805
+ output: process.stdout
6806
+ });
6807
+ return new Promise((resolve2) => {
6808
+ rl.question(chalk29.yellow(`${question} [y/N] `), (answer) => {
6809
+ rl.close();
6810
+ resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
6811
+ });
6812
+ });
6813
+ }
6814
+
6815
+ // src/cli/commands/specialists/index.ts
6816
+ function registerSpecialistsCommands(program2) {
6817
+ const specialists = program2.command("specialists").description("Manage specialist agents (review-agent, test-agent, merge-agent)");
6818
+ specialists.command("list").description("Show all specialists with their status").option("--json", "Output in JSON format").action(listCommand3);
6819
+ specialists.command("wake <name>").description("Wake up a specialist agent (for testing/debugging)").option("--task <description>", "Optional task description to wake with").action(wakeCommand);
6820
+ specialists.command("queue <name>").description("Show pending work in a specialist's queue").option("--json", "Output in JSON format").action(queueCommand);
6821
+ specialists.command("reset <name>").description("Reset a specialist (clear session, start fresh)").option("--force", "Skip confirmation prompt").action(resetCommand);
6822
+ }
6823
+
6824
+ // src/cli/commands/project.ts
6825
+ import chalk30 from "chalk";
6650
6826
  import { existsSync as existsSync28, readFileSync as readFileSync22 } from "fs";
6651
6827
  import { join as join28, resolve } from "path";
6652
6828
  async function projectAddCommand(projectPath, options = {}) {
6653
6829
  const fullPath = resolve(projectPath);
6654
6830
  if (!existsSync28(fullPath)) {
6655
- console.log(chalk26.red(`Path does not exist: ${fullPath}`));
6831
+ console.log(chalk30.red(`Path does not exist: ${fullPath}`));
6656
6832
  return;
6657
6833
  }
6658
6834
  const name = options.name || fullPath.split("/").pop() || "unknown";
6659
6835
  const key = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
6660
6836
  const existing = getProject(key);
6661
6837
  if (existing) {
6662
- console.log(chalk26.yellow(`Project already registered with key: ${key}`));
6663
- console.log(chalk26.dim(`Existing path: ${existing.path}`));
6664
- console.log(chalk26.dim(`To update, first run: pan project remove ${key}`));
6838
+ console.log(chalk30.yellow(`Project already registered with key: ${key}`));
6839
+ console.log(chalk30.dim(`Existing path: ${existing.path}`));
6840
+ console.log(chalk30.dim(`To update, first run: pan project remove ${key}`));
6665
6841
  return;
6666
6842
  }
6667
6843
  let linearTeam = options.linearTeam;
@@ -6681,21 +6857,21 @@ async function projectAddCommand(projectPath, options = {}) {
6681
6857
  projectConfig.linear_team = linearTeam.toUpperCase();
6682
6858
  }
6683
6859
  registerProject(key, projectConfig);
6684
- console.log(chalk26.green(`\u2713 Added project: ${name}`));
6685
- console.log(chalk26.dim(` Key: ${key}`));
6686
- console.log(chalk26.dim(` Path: ${fullPath}`));
6860
+ console.log(chalk30.green(`\u2713 Added project: ${name}`));
6861
+ console.log(chalk30.dim(` Key: ${key}`));
6862
+ console.log(chalk30.dim(` Path: ${fullPath}`));
6687
6863
  if (linearTeam) {
6688
- console.log(chalk26.dim(` Linear team: ${linearTeam}`));
6864
+ console.log(chalk30.dim(` Linear team: ${linearTeam}`));
6689
6865
  }
6690
6866
  console.log("");
6691
- console.log(chalk26.dim(`Edit ${PROJECTS_CONFIG_FILE} to add issue routing rules.`));
6867
+ console.log(chalk30.dim(`Edit ${PROJECTS_CONFIG_FILE} to add issue routing rules.`));
6692
6868
  }
6693
6869
  async function projectListCommand(options = {}) {
6694
6870
  const projects = listProjects();
6695
6871
  if (projects.length === 0) {
6696
- console.log(chalk26.dim("No projects registered."));
6697
- console.log(chalk26.dim("Add one with: pan project add <path> --linear-team <TEAM>"));
6698
- console.log(chalk26.dim(`Or edit: ${PROJECTS_CONFIG_FILE}`));
6872
+ console.log(chalk30.dim("No projects registered."));
6873
+ console.log(chalk30.dim("Add one with: pan project add <path> --linear-team <TEAM>"));
6874
+ console.log(chalk30.dim(`Or edit: ${PROJECTS_CONFIG_FILE}`));
6699
6875
  return;
6700
6876
  }
6701
6877
  if (options.json) {
@@ -6706,51 +6882,51 @@ async function projectListCommand(options = {}) {
6706
6882
  console.log(JSON.stringify(output, null, 2));
6707
6883
  return;
6708
6884
  }
6709
- console.log(chalk26.bold("\nRegistered Projects:\n"));
6885
+ console.log(chalk30.bold("\nRegistered Projects:\n"));
6710
6886
  for (const { key, config } of projects) {
6711
6887
  const exists = existsSync28(config.path);
6712
- const statusIcon = exists ? chalk26.green("\u2713") : chalk26.red("\u2717");
6713
- console.log(`${statusIcon} ${chalk26.bold(config.name)} ${chalk26.dim(`(${key})`)}`);
6714
- console.log(` ${chalk26.dim(config.path)}`);
6888
+ const statusIcon = exists ? chalk30.green("\u2713") : chalk30.red("\u2717");
6889
+ console.log(`${statusIcon} ${chalk30.bold(config.name)} ${chalk30.dim(`(${key})`)}`);
6890
+ console.log(` ${chalk30.dim(config.path)}`);
6715
6891
  if (config.linear_team) {
6716
- console.log(` ${chalk26.cyan(`Linear: ${config.linear_team}`)}`);
6892
+ console.log(` ${chalk30.cyan(`Linear: ${config.linear_team}`)}`);
6717
6893
  }
6718
6894
  if (config.issue_routing && config.issue_routing.length > 0) {
6719
- console.log(` ${chalk26.dim(`Routes: ${config.issue_routing.length} rules`)}`);
6895
+ console.log(` ${chalk30.dim(`Routes: ${config.issue_routing.length} rules`)}`);
6720
6896
  }
6721
6897
  console.log("");
6722
6898
  }
6723
- console.log(chalk26.dim(`Config: ${PROJECTS_CONFIG_FILE}`));
6899
+ console.log(chalk30.dim(`Config: ${PROJECTS_CONFIG_FILE}`));
6724
6900
  }
6725
6901
  async function projectRemoveCommand(nameOrPath) {
6726
6902
  const projects = listProjects();
6727
6903
  if (unregisterProject(nameOrPath)) {
6728
- console.log(chalk26.green(`\u2713 Removed project: ${nameOrPath}`));
6904
+ console.log(chalk30.green(`\u2713 Removed project: ${nameOrPath}`));
6729
6905
  return;
6730
6906
  }
6731
6907
  for (const { key, config } of projects) {
6732
6908
  if (config.name === nameOrPath || config.path === resolve(nameOrPath)) {
6733
6909
  unregisterProject(key);
6734
- console.log(chalk26.green(`\u2713 Removed project: ${config.name}`));
6910
+ console.log(chalk30.green(`\u2713 Removed project: ${config.name}`));
6735
6911
  return;
6736
6912
  }
6737
6913
  }
6738
- console.log(chalk26.red(`Project not found: ${nameOrPath}`));
6739
- console.log(chalk26.dim(`Use 'pan project list' to see registered projects.`));
6914
+ console.log(chalk30.red(`Project not found: ${nameOrPath}`));
6915
+ console.log(chalk30.dim(`Use 'pan project list' to see registered projects.`));
6740
6916
  }
6741
6917
  async function projectInitCommand() {
6742
6918
  if (existsSync28(PROJECTS_CONFIG_FILE)) {
6743
- console.log(chalk26.yellow(`Config already exists: ${PROJECTS_CONFIG_FILE}`));
6919
+ console.log(chalk30.yellow(`Config already exists: ${PROJECTS_CONFIG_FILE}`));
6744
6920
  return;
6745
6921
  }
6746
6922
  initializeProjectsConfig();
6747
- console.log(chalk26.green("\u2713 Projects config initialized"));
6923
+ console.log(chalk30.green("\u2713 Projects config initialized"));
6748
6924
  console.log("");
6749
- console.log(chalk26.dim(`Edit ${PROJECTS_CONFIG_FILE} to add your projects.`));
6925
+ console.log(chalk30.dim(`Edit ${PROJECTS_CONFIG_FILE} to add your projects.`));
6750
6926
  console.log("");
6751
- console.log(chalk26.bold("Quick start:"));
6927
+ console.log(chalk30.bold("Quick start:"));
6752
6928
  console.log(
6753
- chalk26.dim(
6929
+ chalk30.dim(
6754
6930
  ' pan project add /path/to/project --name "My Project" --linear-team MIN'
6755
6931
  )
6756
6932
  );
@@ -6769,13 +6945,13 @@ async function projectShowCommand(keyOrName) {
6769
6945
  }
6770
6946
  }
6771
6947
  if (!found) {
6772
- console.error(chalk26.red(`Project not found: ${keyOrName}`));
6773
- console.log(chalk26.dim(`Use 'pan project list' to see registered projects.`));
6948
+ console.error(chalk30.red(`Project not found: ${keyOrName}`));
6949
+ console.log(chalk30.dim(`Use 'pan project list' to see registered projects.`));
6774
6950
  process.exit(1);
6775
6951
  }
6776
6952
  const pathExists = existsSync28(found.path);
6777
- const pathStatus = pathExists ? chalk26.green("\u2713") : chalk26.red("\u2717");
6778
- console.log(chalk26.bold(`
6953
+ const pathStatus = pathExists ? chalk30.green("\u2713") : chalk30.red("\u2717");
6954
+ console.log(chalk30.bold(`
6779
6955
  Project: ${foundKey}
6780
6956
  `));
6781
6957
  console.log(` Name: ${found.name}`);
@@ -6784,7 +6960,7 @@ Project: ${foundKey}
6784
6960
  console.log(` Team: ${found.linear_team}`);
6785
6961
  }
6786
6962
  if (found.issue_routing && found.issue_routing.length > 0) {
6787
- console.log("\n " + chalk26.bold("Routing Rules:"));
6963
+ console.log("\n " + chalk30.bold("Routing Rules:"));
6788
6964
  for (const rule of found.issue_routing) {
6789
6965
  if (rule.labels) {
6790
6966
  console.log(` Labels: ${rule.labels.join(", ")}`);
@@ -6799,16 +6975,14 @@ Project: ${foundKey}
6799
6975
  }
6800
6976
 
6801
6977
  // src/cli/commands/doctor.ts
6802
- init_esm_shims();
6803
- init_paths();
6804
- import chalk27 from "chalk";
6805
- import { existsSync as existsSync29, readdirSync as readdirSync15 } from "fs";
6806
- import { execSync as execSync13 } from "child_process";
6807
- import { homedir as homedir8 } from "os";
6978
+ import chalk31 from "chalk";
6979
+ import { existsSync as existsSync29, readdirSync as readdirSync15, readFileSync as readFileSync23 } from "fs";
6980
+ import { execSync as execSync15 } from "child_process";
6981
+ import { homedir as homedir9 } from "os";
6808
6982
  import { join as join29 } from "path";
6809
6983
  function checkCommand2(cmd) {
6810
6984
  try {
6811
- execSync13(`which ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
6985
+ execSync15(`which ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
6812
6986
  return true;
6813
6987
  } catch {
6814
6988
  return false;
@@ -6826,8 +7000,8 @@ function countItems(path) {
6826
7000
  }
6827
7001
  }
6828
7002
  async function doctorCommand() {
6829
- console.log(chalk27.bold("\nPanopticon Doctor\n"));
6830
- console.log(chalk27.dim("Checking system health...\n"));
7003
+ console.log(chalk31.bold("\nPanopticon Doctor\n"));
7004
+ console.log(chalk31.dim("Checking system health...\n"));
6831
7005
  const checks = [];
6832
7006
  const requiredCommands = [
6833
7007
  { cmd: "git", name: "Git", fix: "Install git" },
@@ -6891,7 +7065,7 @@ async function doctorCommand() {
6891
7065
  fix: "Install Claude Code first"
6892
7066
  });
6893
7067
  }
6894
- const envFile = join29(homedir8(), ".panopticon.env");
7068
+ const envFile = join29(homedir9(), ".panopticon.env");
6895
7069
  if (existsSync29(envFile)) {
6896
7070
  checks.push({ name: "Config File", status: "ok", message: "~/.panopticon.env exists" });
6897
7071
  } else {
@@ -6905,7 +7079,7 @@ async function doctorCommand() {
6905
7079
  if (process.env.LINEAR_API_KEY) {
6906
7080
  checks.push({ name: "LINEAR_API_KEY", status: "ok", message: "Set in environment" });
6907
7081
  } else if (existsSync29(envFile)) {
6908
- const content = __require("fs").readFileSync(envFile, "utf-8");
7082
+ const content = readFileSync23(envFile, "utf-8");
6909
7083
  if (content.includes("LINEAR_API_KEY")) {
6910
7084
  checks.push({ name: "LINEAR_API_KEY", status: "ok", message: "Set in config file" });
6911
7085
  } else {
@@ -6925,7 +7099,7 @@ async function doctorCommand() {
6925
7099
  });
6926
7100
  }
6927
7101
  try {
6928
- const sessions = execSync13("tmux list-sessions 2>/dev/null || true", { encoding: "utf-8" });
7102
+ const sessions = execSync15("tmux list-sessions 2>/dev/null || true", { encoding: "utf-8" });
6929
7103
  const agentSessions = sessions.split("\n").filter((s) => s.includes("agent-")).length;
6930
7104
  checks.push({
6931
7105
  name: "Running Agents",
@@ -6940,41 +7114,46 @@ async function doctorCommand() {
6940
7114
  });
6941
7115
  }
6942
7116
  const icons = {
6943
- ok: chalk27.green("\u2713"),
6944
- warn: chalk27.yellow("\u26A0"),
6945
- error: chalk27.red("\u2717")
7117
+ ok: chalk31.green("\u2713"),
7118
+ warn: chalk31.yellow("\u26A0"),
7119
+ error: chalk31.red("\u2717")
6946
7120
  };
6947
7121
  let hasErrors = false;
6948
7122
  let hasWarnings = false;
6949
7123
  for (const check of checks) {
6950
7124
  const icon = icons[check.status];
6951
- const message = check.status === "error" ? chalk27.red(check.message) : check.status === "warn" ? chalk27.yellow(check.message) : chalk27.dim(check.message);
7125
+ const message = check.status === "error" ? chalk31.red(check.message) : check.status === "warn" ? chalk31.yellow(check.message) : chalk31.dim(check.message);
6952
7126
  console.log(`${icon} ${check.name}: ${message}`);
6953
7127
  if (check.fix && check.status !== "ok") {
6954
- console.log(chalk27.dim(` Fix: ${check.fix}`));
7128
+ console.log(chalk31.dim(` Fix: ${check.fix}`));
6955
7129
  }
6956
7130
  if (check.status === "error") hasErrors = true;
6957
7131
  if (check.status === "warn") hasWarnings = true;
6958
7132
  }
6959
7133
  console.log("");
6960
7134
  if (hasErrors) {
6961
- console.log(chalk27.red("Some required components are missing."));
6962
- console.log(chalk27.dim("Fix the errors above before using Panopticon."));
7135
+ console.log(chalk31.red("Some required components are missing."));
7136
+ console.log(chalk31.dim("Fix the errors above before using Panopticon."));
6963
7137
  } else if (hasWarnings) {
6964
- console.log(chalk27.yellow("System is functional with some optional features missing."));
7138
+ console.log(chalk31.yellow("System is functional with some optional features missing."));
6965
7139
  } else {
6966
- console.log(chalk27.green("All systems operational!"));
7140
+ console.log(chalk31.green("All systems operational!"));
6967
7141
  }
6968
7142
  console.log("");
6969
7143
  }
6970
7144
 
6971
7145
  // src/cli/commands/update.ts
6972
- init_esm_shims();
6973
- import { execSync as execSync14 } from "child_process";
6974
- import chalk28 from "chalk";
7146
+ import { execSync as execSync16 } from "child_process";
7147
+ import chalk32 from "chalk";
7148
+ import { readFileSync as readFileSync24 } from "fs";
7149
+ import { fileURLToPath as fileURLToPath2 } from "url";
7150
+ import { dirname as dirname5, join as join30 } from "path";
6975
7151
  function getCurrentVersion() {
6976
7152
  try {
6977
- const pkg = require_package();
7153
+ const __filename3 = fileURLToPath2(import.meta.url);
7154
+ const __dirname3 = dirname5(__filename3);
7155
+ const pkgPath = join30(__dirname3, "..", "..", "..", "package.json");
7156
+ const pkg = JSON.parse(readFileSync24(pkgPath, "utf-8"));
6978
7157
  return pkg.version;
6979
7158
  } catch {
6980
7159
  return "unknown";
@@ -6982,7 +7161,7 @@ function getCurrentVersion() {
6982
7161
  }
6983
7162
  async function getLatestVersion() {
6984
7163
  try {
6985
- const result = execSync14("npm view panopticon-cli version", {
7164
+ const result = execSync16("npm view panopticon-cli version", {
6986
7165
  encoding: "utf8",
6987
7166
  stdio: ["pipe", "pipe", "pipe"]
6988
7167
  });
@@ -7007,49 +7186,49 @@ function isNewer(latest, current) {
7007
7186
  return l.patch > c.patch;
7008
7187
  }
7009
7188
  async function updateCommand(options) {
7010
- console.log(chalk28.bold("Panopticon Update\n"));
7189
+ console.log(chalk32.bold("Panopticon Update\n"));
7011
7190
  const currentVersion = getCurrentVersion();
7012
- console.log(`Current version: ${chalk28.cyan(currentVersion)}`);
7191
+ console.log(`Current version: ${chalk32.cyan(currentVersion)}`);
7013
7192
  let latestVersion;
7014
7193
  try {
7015
- console.log(chalk28.dim("Checking npm for latest version..."));
7194
+ console.log(chalk32.dim("Checking npm for latest version..."));
7016
7195
  latestVersion = await getLatestVersion();
7017
- console.log(`Latest version: ${chalk28.cyan(latestVersion)}`);
7196
+ console.log(`Latest version: ${chalk32.cyan(latestVersion)}`);
7018
7197
  } catch (error) {
7019
- console.error(chalk28.red("Failed to check for updates"));
7020
- console.error(chalk28.dim("Make sure you have internet connectivity"));
7198
+ console.error(chalk32.red("Failed to check for updates"));
7199
+ console.error(chalk32.dim("Make sure you have internet connectivity"));
7021
7200
  process.exit(1);
7022
7201
  }
7023
7202
  const needsUpdate = isNewer(latestVersion, currentVersion);
7024
7203
  if (!needsUpdate) {
7025
- console.log(chalk28.green("\n\u2713 You are on the latest version"));
7204
+ console.log(chalk32.green("\n\u2713 You are on the latest version"));
7026
7205
  return;
7027
7206
  }
7028
7207
  console.log(
7029
- chalk28.yellow(`
7208
+ chalk32.yellow(`
7030
7209
  \u2191 Update available: ${currentVersion} \u2192 ${latestVersion}`)
7031
7210
  );
7032
7211
  if (options.check) {
7033
- console.log(chalk28.dim("\nRun `pan update` to install"));
7212
+ console.log(chalk32.dim("\nRun `pan update` to install"));
7034
7213
  return;
7035
7214
  }
7036
- console.log(chalk28.dim("\nUpdating Panopticon..."));
7215
+ console.log(chalk32.dim("\nUpdating Panopticon..."));
7037
7216
  try {
7038
- execSync14("npm install -g panopticon-cli@latest", {
7217
+ execSync16("npm install -g panopticon-cli@latest", {
7039
7218
  stdio: "inherit"
7040
7219
  });
7041
- console.log(chalk28.green(`
7220
+ console.log(chalk32.green(`
7042
7221
  \u2713 Updated to ${latestVersion}`));
7043
7222
  const config = loadConfig();
7044
7223
  if (config.sync.auto_sync) {
7045
- console.log(chalk28.dim("\nRunning auto-sync..."));
7224
+ console.log(chalk32.dim("\nRunning auto-sync..."));
7046
7225
  await syncCommand({});
7047
7226
  }
7048
- console.log(chalk28.dim("\nRestart any running agents to use the new version."));
7227
+ console.log(chalk32.dim("\nRestart any running agents to use the new version."));
7049
7228
  } catch (error) {
7050
- console.error(chalk28.red("\nUpdate failed"));
7229
+ console.error(chalk32.red("\nUpdate failed"));
7051
7230
  console.error(
7052
- chalk28.dim("Try running with sudo: sudo npm install -g panopticon-cli@latest")
7231
+ chalk32.dim("Try running with sudo: sudo npm install -g panopticon-cli@latest")
7053
7232
  );
7054
7233
  process.exit(1);
7055
7234
  }
@@ -7068,120 +7247,140 @@ program.command("skills").description("List and manage skills").option("--json",
7068
7247
  registerWorkCommands(program);
7069
7248
  registerWorkspaceCommands(program);
7070
7249
  registerCloisterCommands(program);
7250
+ registerSpecialistsCommands(program);
7071
7251
  registerSetupCommands(program);
7072
7252
  registerInstallCommand(program);
7073
7253
  program.command("status").description("Show running agents (shorthand for work status)").option("--json", "Output as JSON").action(statusCommand);
7074
7254
  program.command("up").description("Start dashboard (and Traefik if enabled)").option("--detach", "Run in background").option("--skip-traefik", "Skip Traefik startup").action(async (options) => {
7075
- const { spawn, execSync: execSync15 } = await import("child_process");
7076
- const { join: join30, dirname: dirname5 } = await import("path");
7077
- const { fileURLToPath: fileURLToPath2 } = await import("url");
7078
- const { readFileSync: readFileSync23, existsSync: existsSync30 } = await import("fs");
7255
+ const { spawn, execSync: execSync17 } = await import("child_process");
7256
+ const { join: join31, dirname: dirname6 } = await import("path");
7257
+ const { fileURLToPath: fileURLToPath3 } = await import("url");
7258
+ const { readFileSync: readFileSync25, existsSync: existsSync30 } = await import("fs");
7079
7259
  const { parse: parse2 } = await import("@iarna/toml");
7080
- const __dirname3 = dirname5(fileURLToPath2(import.meta.url));
7081
- const dashboardDir = join30(__dirname3, "..", "dashboard");
7082
- const configFile = join30(process.env.HOME || "", ".panopticon", "config.toml");
7260
+ const __dirname3 = dirname6(fileURLToPath3(import.meta.url));
7261
+ const dashboardDir = join31(__dirname3, "..", "dashboard");
7262
+ const configFile = join31(process.env.HOME || "", ".panopticon", "config.toml");
7083
7263
  let traefikEnabled = false;
7084
7264
  let traefikDomain = "pan.localhost";
7085
7265
  if (existsSync30(configFile)) {
7086
7266
  try {
7087
- const configContent = readFileSync23(configFile, "utf-8");
7267
+ const configContent = readFileSync25(configFile, "utf-8");
7088
7268
  const config = parse2(configContent);
7089
7269
  traefikEnabled = config.traefik?.enabled === true;
7090
7270
  traefikDomain = config.traefik?.domain || "pan.localhost";
7091
7271
  } catch (error) {
7092
- console.log(chalk29.yellow("Warning: Could not read config.toml"));
7272
+ console.log(chalk33.yellow("Warning: Could not read config.toml"));
7093
7273
  }
7094
7274
  }
7095
- console.log(chalk29.bold("Starting Panopticon...\n"));
7275
+ console.log(chalk33.bold("Starting Panopticon...\n"));
7096
7276
  if (traefikEnabled && !options.skipTraefik) {
7097
- const traefikDir = join30(process.env.HOME || "", ".panopticon", "traefik");
7277
+ const traefikDir = join31(process.env.HOME || "", ".panopticon", "traefik");
7098
7278
  if (existsSync30(traefikDir)) {
7099
7279
  try {
7100
- console.log(chalk29.dim("Starting Traefik..."));
7101
- execSync15("docker-compose up -d", {
7280
+ console.log(chalk33.dim("Starting Traefik..."));
7281
+ execSync17("docker-compose up -d", {
7102
7282
  cwd: traefikDir,
7103
7283
  stdio: "pipe"
7104
7284
  });
7105
- console.log(chalk29.green("\u2713 Traefik started"));
7106
- console.log(chalk29.dim(` Dashboard: https://traefik.${traefikDomain}:8080
7285
+ console.log(chalk33.green("\u2713 Traefik started"));
7286
+ console.log(chalk33.dim(` Dashboard: https://traefik.${traefikDomain}:8080
7107
7287
  `));
7108
7288
  } catch (error) {
7109
- console.log(chalk29.yellow("\u26A0 Failed to start Traefik (continuing anyway)"));
7110
- console.log(chalk29.dim(" Run with --skip-traefik to suppress this message\n"));
7289
+ console.log(chalk33.yellow("\u26A0 Failed to start Traefik (continuing anyway)"));
7290
+ console.log(chalk33.dim(" Run with --skip-traefik to suppress this message\n"));
7111
7291
  }
7112
7292
  }
7113
7293
  }
7114
- console.log(chalk29.dim("Starting dashboard..."));
7294
+ try {
7295
+ execSync17("npm --version", { stdio: "pipe" });
7296
+ } catch {
7297
+ console.error(chalk33.red("Error: npm not found in PATH"));
7298
+ console.error(chalk33.dim("Make sure Node.js and npm are installed and in your PATH"));
7299
+ process.exit(1);
7300
+ }
7301
+ console.log(chalk33.dim("Starting dashboard..."));
7115
7302
  if (options.detach) {
7116
7303
  const child = spawn("npm", ["run", "dev"], {
7117
7304
  cwd: dashboardDir,
7118
7305
  detached: true,
7119
- stdio: "ignore"
7306
+ stdio: "ignore",
7307
+ shell: true
7308
+ });
7309
+ let hasError = false;
7310
+ child.on("error", (err) => {
7311
+ hasError = true;
7312
+ console.error(chalk33.red("Failed to start dashboard in background:"), err.message);
7313
+ process.exit(1);
7120
7314
  });
7121
- child.unref();
7122
- console.log(chalk29.green("\u2713 Dashboard started in background"));
7315
+ setTimeout(() => {
7316
+ if (!hasError) {
7317
+ child.unref();
7318
+ }
7319
+ }, 100);
7320
+ console.log(chalk33.green("\u2713 Dashboard started in background"));
7123
7321
  if (traefikEnabled) {
7124
- console.log(` Frontend: ${chalk29.cyan(`https://${traefikDomain}`)}`);
7125
- console.log(` API: ${chalk29.cyan(`https://${traefikDomain}/api`)}`);
7322
+ console.log(` Frontend: ${chalk33.cyan(`https://${traefikDomain}`)}`);
7323
+ console.log(` API: ${chalk33.cyan(`https://${traefikDomain}/api`)}`);
7126
7324
  } else {
7127
- console.log(` Frontend: ${chalk29.cyan("http://localhost:3001")}`);
7128
- console.log(` API: ${chalk29.cyan("http://localhost:3002")}`);
7325
+ console.log(` Frontend: ${chalk33.cyan("http://localhost:3001")}`);
7326
+ console.log(` API: ${chalk33.cyan("http://localhost:3002")}`);
7129
7327
  }
7130
7328
  } else {
7131
7329
  if (traefikEnabled) {
7132
- console.log(` Frontend: ${chalk29.cyan(`https://${traefikDomain}`)}`);
7133
- console.log(` API: ${chalk29.cyan(`https://${traefikDomain}/api`)}`);
7330
+ console.log(` Frontend: ${chalk33.cyan(`https://${traefikDomain}`)}`);
7331
+ console.log(` API: ${chalk33.cyan(`https://${traefikDomain}/api`)}`);
7134
7332
  } else {
7135
- console.log(` Frontend: ${chalk29.cyan("http://localhost:3001")}`);
7136
- console.log(` API: ${chalk29.cyan("http://localhost:3002")}`);
7333
+ console.log(` Frontend: ${chalk33.cyan("http://localhost:3001")}`);
7334
+ console.log(` API: ${chalk33.cyan("http://localhost:3002")}`);
7137
7335
  }
7138
- console.log(chalk29.dim("\nPress Ctrl+C to stop\n"));
7336
+ console.log(chalk33.dim("\nPress Ctrl+C to stop\n"));
7139
7337
  const child = spawn("npm", ["run", "dev"], {
7140
7338
  cwd: dashboardDir,
7141
- stdio: "inherit"
7339
+ stdio: "inherit",
7340
+ shell: true
7142
7341
  });
7143
7342
  child.on("error", (err) => {
7144
- console.error(chalk29.red("Failed to start dashboard:"), err.message);
7343
+ console.error(chalk33.red("Failed to start dashboard:"), err.message);
7145
7344
  process.exit(1);
7146
7345
  });
7147
7346
  }
7148
7347
  });
7149
7348
  program.command("down").description("Stop dashboard (and Traefik if enabled)").option("--skip-traefik", "Skip Traefik shutdown").action(async (options) => {
7150
- const { execSync: execSync15 } = await import("child_process");
7151
- const { join: join30 } = await import("path");
7152
- const { readFileSync: readFileSync23, existsSync: existsSync30 } = await import("fs");
7349
+ const { execSync: execSync17 } = await import("child_process");
7350
+ const { join: join31 } = await import("path");
7351
+ const { readFileSync: readFileSync25, existsSync: existsSync30 } = await import("fs");
7153
7352
  const { parse: parse2 } = await import("@iarna/toml");
7154
- console.log(chalk29.bold("Stopping Panopticon...\n"));
7155
- console.log(chalk29.dim("Stopping dashboard..."));
7353
+ console.log(chalk33.bold("Stopping Panopticon...\n"));
7354
+ console.log(chalk33.dim("Stopping dashboard..."));
7156
7355
  try {
7157
- execSync15("lsof -ti:3001 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
7158
- execSync15("lsof -ti:3002 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
7159
- console.log(chalk29.green("\u2713 Dashboard stopped"));
7356
+ execSync17("lsof -ti:3001 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
7357
+ execSync17("lsof -ti:3002 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
7358
+ console.log(chalk33.green("\u2713 Dashboard stopped"));
7160
7359
  } catch {
7161
- console.log(chalk29.dim(" No dashboard processes found"));
7360
+ console.log(chalk33.dim(" No dashboard processes found"));
7162
7361
  }
7163
- const configFile = join30(process.env.HOME || "", ".panopticon", "config.toml");
7362
+ const configFile = join31(process.env.HOME || "", ".panopticon", "config.toml");
7164
7363
  let traefikEnabled = false;
7165
7364
  if (existsSync30(configFile)) {
7166
7365
  try {
7167
- const configContent = readFileSync23(configFile, "utf-8");
7366
+ const configContent = readFileSync25(configFile, "utf-8");
7168
7367
  const config = parse2(configContent);
7169
7368
  traefikEnabled = config.traefik?.enabled === true;
7170
7369
  } catch (error) {
7171
7370
  }
7172
7371
  }
7173
7372
  if (traefikEnabled && !options.skipTraefik) {
7174
- const traefikDir = join30(process.env.HOME || "", ".panopticon", "traefik");
7373
+ const traefikDir = join31(process.env.HOME || "", ".panopticon", "traefik");
7175
7374
  if (existsSync30(traefikDir)) {
7176
- console.log(chalk29.dim("Stopping Traefik..."));
7375
+ console.log(chalk33.dim("Stopping Traefik..."));
7177
7376
  try {
7178
- execSync15("docker-compose down", {
7377
+ execSync17("docker-compose down", {
7179
7378
  cwd: traefikDir,
7180
7379
  stdio: "pipe"
7181
7380
  });
7182
- console.log(chalk29.green("\u2713 Traefik stopped"));
7381
+ console.log(chalk33.green("\u2713 Traefik stopped"));
7183
7382
  } catch (error) {
7184
- console.log(chalk29.yellow("\u26A0 Failed to stop Traefik"));
7383
+ console.log(chalk33.yellow("\u26A0 Failed to stop Traefik"));
7185
7384
  }
7186
7385
  }
7187
7386
  }