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/README.md +239 -1
- package/dist/{chunk-PSJRCUOA.js → chunk-BH6BR26M.js} +3 -7
- package/dist/{chunk-PSJRCUOA.js.map → chunk-BH6BR26M.js.map} +1 -1
- package/dist/{chunk-B2JBBOJN.js → chunk-C6A7S65K.js} +3 -24
- package/dist/chunk-C6A7S65K.js.map +1 -0
- package/dist/chunk-P5TQ5C3J.js +103 -0
- package/dist/chunk-P5TQ5C3J.js.map +1 -0
- package/dist/cli/index.js +1499 -1300
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +3 -9
- package/dist/index.js.map +1 -1
- package/dist/{projects-6JVKIYIH.js → projects-54CV437J.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-B2JBBOJN.js.map +0 -1
- package/dist/chunk-SG7O6I7R.js +0 -155
- package/dist/chunk-SG7O6I7R.js.map +0 -1
- /package/dist/{projects-6JVKIYIH.js.map → projects-54CV437J.js.map} +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
restoreBackup,
|
|
17
17
|
saveConfig,
|
|
18
18
|
syncHooks
|
|
19
|
-
} from "../chunk-
|
|
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-
|
|
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
|
-
|
|
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/
|
|
55
|
-
import {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
106
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
runtime
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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 (
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
"
|
|
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 (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return
|
|
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
|
-
|
|
591
|
-
|
|
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
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
602
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
//
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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/
|
|
918
|
-
|
|
919
|
-
import
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
if (
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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 (
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
985
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
889
|
+
const state = getAgentState(normalizedId);
|
|
890
|
+
if (state) {
|
|
891
|
+
state.status = "stopped";
|
|
892
|
+
saveAgentState(state);
|
|
1104
893
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
const
|
|
1142
|
-
for (const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
|
993
|
+
return { recovered, failed };
|
|
1154
994
|
}
|
|
1155
|
-
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
-
|
|
1170
|
-
console.log(
|
|
1171
|
-
|
|
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
|
-
|
|
1178
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
1534
|
+
import { homedir as homedir3 } from "os";
|
|
1699
1535
|
import { execSync as execSync4 } from "child_process";
|
|
1700
1536
|
function getLinearApiKey2() {
|
|
1701
|
-
const envFile = join9(
|
|
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
|
-
|
|
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
|
-
|
|
2040
|
+
writeFileSync6(statePath, stateContent);
|
|
2205
2041
|
const workspacePath = join9(planningDir, "WORKSPACE.md");
|
|
2206
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
5027
|
-
|
|
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
|
-
|
|
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
|
|
5039
|
-
var CLAUDE_PROJECTS_DIR2 = join22(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
6580
|
-
const { dirname:
|
|
6581
|
-
const __dirname3 =
|
|
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
|
-
|
|
6452
|
+
chmodSync2(scriptDest, 493);
|
|
6594
6453
|
console.log(chalk25.green("\u2713 Installed heartbeat-hook script"));
|
|
6595
|
-
const claudeDir = join27(
|
|
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
|
-
|
|
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/
|
|
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(
|
|
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(
|
|
6663
|
-
console.log(
|
|
6664
|
-
console.log(
|
|
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(
|
|
6685
|
-
console.log(
|
|
6686
|
-
console.log(
|
|
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(
|
|
6864
|
+
console.log(chalk30.dim(` Linear team: ${linearTeam}`));
|
|
6689
6865
|
}
|
|
6690
6866
|
console.log("");
|
|
6691
|
-
console.log(
|
|
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(
|
|
6697
|
-
console.log(
|
|
6698
|
-
console.log(
|
|
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(
|
|
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 ?
|
|
6713
|
-
console.log(`${statusIcon} ${
|
|
6714
|
-
console.log(` ${
|
|
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(` ${
|
|
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(` ${
|
|
6895
|
+
console.log(` ${chalk30.dim(`Routes: ${config.issue_routing.length} rules`)}`);
|
|
6720
6896
|
}
|
|
6721
6897
|
console.log("");
|
|
6722
6898
|
}
|
|
6723
|
-
console.log(
|
|
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(
|
|
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(
|
|
6910
|
+
console.log(chalk30.green(`\u2713 Removed project: ${config.name}`));
|
|
6735
6911
|
return;
|
|
6736
6912
|
}
|
|
6737
6913
|
}
|
|
6738
|
-
console.log(
|
|
6739
|
-
console.log(
|
|
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(
|
|
6919
|
+
console.log(chalk30.yellow(`Config already exists: ${PROJECTS_CONFIG_FILE}`));
|
|
6744
6920
|
return;
|
|
6745
6921
|
}
|
|
6746
6922
|
initializeProjectsConfig();
|
|
6747
|
-
console.log(
|
|
6923
|
+
console.log(chalk30.green("\u2713 Projects config initialized"));
|
|
6748
6924
|
console.log("");
|
|
6749
|
-
console.log(
|
|
6925
|
+
console.log(chalk30.dim(`Edit ${PROJECTS_CONFIG_FILE} to add your projects.`));
|
|
6750
6926
|
console.log("");
|
|
6751
|
-
console.log(
|
|
6927
|
+
console.log(chalk30.bold("Quick start:"));
|
|
6752
6928
|
console.log(
|
|
6753
|
-
|
|
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(
|
|
6773
|
-
console.log(
|
|
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 ?
|
|
6778
|
-
console.log(
|
|
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 " +
|
|
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
|
-
|
|
6803
|
-
|
|
6804
|
-
import
|
|
6805
|
-
import {
|
|
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
|
-
|
|
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(
|
|
6830
|
-
console.log(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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:
|
|
6944
|
-
warn:
|
|
6945
|
-
error:
|
|
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" ?
|
|
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(
|
|
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(
|
|
6962
|
-
console.log(
|
|
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(
|
|
7138
|
+
console.log(chalk31.yellow("System is functional with some optional features missing."));
|
|
6965
7139
|
} else {
|
|
6966
|
-
console.log(
|
|
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
|
-
|
|
6973
|
-
import
|
|
6974
|
-
import
|
|
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
|
|
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 =
|
|
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(
|
|
7189
|
+
console.log(chalk32.bold("Panopticon Update\n"));
|
|
7011
7190
|
const currentVersion = getCurrentVersion();
|
|
7012
|
-
console.log(`Current version: ${
|
|
7191
|
+
console.log(`Current version: ${chalk32.cyan(currentVersion)}`);
|
|
7013
7192
|
let latestVersion;
|
|
7014
7193
|
try {
|
|
7015
|
-
console.log(
|
|
7194
|
+
console.log(chalk32.dim("Checking npm for latest version..."));
|
|
7016
7195
|
latestVersion = await getLatestVersion();
|
|
7017
|
-
console.log(`Latest version: ${
|
|
7196
|
+
console.log(`Latest version: ${chalk32.cyan(latestVersion)}`);
|
|
7018
7197
|
} catch (error) {
|
|
7019
|
-
console.error(
|
|
7020
|
-
console.error(
|
|
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(
|
|
7204
|
+
console.log(chalk32.green("\n\u2713 You are on the latest version"));
|
|
7026
7205
|
return;
|
|
7027
7206
|
}
|
|
7028
7207
|
console.log(
|
|
7029
|
-
|
|
7208
|
+
chalk32.yellow(`
|
|
7030
7209
|
\u2191 Update available: ${currentVersion} \u2192 ${latestVersion}`)
|
|
7031
7210
|
);
|
|
7032
7211
|
if (options.check) {
|
|
7033
|
-
console.log(
|
|
7212
|
+
console.log(chalk32.dim("\nRun `pan update` to install"));
|
|
7034
7213
|
return;
|
|
7035
7214
|
}
|
|
7036
|
-
console.log(
|
|
7215
|
+
console.log(chalk32.dim("\nUpdating Panopticon..."));
|
|
7037
7216
|
try {
|
|
7038
|
-
|
|
7217
|
+
execSync16("npm install -g panopticon-cli@latest", {
|
|
7039
7218
|
stdio: "inherit"
|
|
7040
7219
|
});
|
|
7041
|
-
console.log(
|
|
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(
|
|
7224
|
+
console.log(chalk32.dim("\nRunning auto-sync..."));
|
|
7046
7225
|
await syncCommand({});
|
|
7047
7226
|
}
|
|
7048
|
-
console.log(
|
|
7227
|
+
console.log(chalk32.dim("\nRestart any running agents to use the new version."));
|
|
7049
7228
|
} catch (error) {
|
|
7050
|
-
console.error(
|
|
7229
|
+
console.error(chalk32.red("\nUpdate failed"));
|
|
7051
7230
|
console.error(
|
|
7052
|
-
|
|
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:
|
|
7076
|
-
const { join:
|
|
7077
|
-
const { fileURLToPath:
|
|
7078
|
-
const { readFileSync:
|
|
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 =
|
|
7081
|
-
const dashboardDir =
|
|
7082
|
-
const configFile =
|
|
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 =
|
|
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(
|
|
7272
|
+
console.log(chalk33.yellow("Warning: Could not read config.toml"));
|
|
7093
7273
|
}
|
|
7094
7274
|
}
|
|
7095
|
-
console.log(
|
|
7275
|
+
console.log(chalk33.bold("Starting Panopticon...\n"));
|
|
7096
7276
|
if (traefikEnabled && !options.skipTraefik) {
|
|
7097
|
-
const traefikDir =
|
|
7277
|
+
const traefikDir = join31(process.env.HOME || "", ".panopticon", "traefik");
|
|
7098
7278
|
if (existsSync30(traefikDir)) {
|
|
7099
7279
|
try {
|
|
7100
|
-
console.log(
|
|
7101
|
-
|
|
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(
|
|
7106
|
-
console.log(
|
|
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(
|
|
7110
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
7122
|
-
|
|
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: ${
|
|
7125
|
-
console.log(` 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: ${
|
|
7128
|
-
console.log(` API: ${
|
|
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: ${
|
|
7133
|
-
console.log(` 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: ${
|
|
7136
|
-
console.log(` API: ${
|
|
7333
|
+
console.log(` Frontend: ${chalk33.cyan("http://localhost:3001")}`);
|
|
7334
|
+
console.log(` API: ${chalk33.cyan("http://localhost:3002")}`);
|
|
7137
7335
|
}
|
|
7138
|
-
console.log(
|
|
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(
|
|
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:
|
|
7151
|
-
const { join:
|
|
7152
|
-
const { readFileSync:
|
|
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(
|
|
7155
|
-
console.log(
|
|
7353
|
+
console.log(chalk33.bold("Stopping Panopticon...\n"));
|
|
7354
|
+
console.log(chalk33.dim("Stopping dashboard..."));
|
|
7156
7355
|
try {
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
console.log(
|
|
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(
|
|
7360
|
+
console.log(chalk33.dim(" No dashboard processes found"));
|
|
7162
7361
|
}
|
|
7163
|
-
const configFile =
|
|
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 =
|
|
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 =
|
|
7373
|
+
const traefikDir = join31(process.env.HOME || "", ".panopticon", "traefik");
|
|
7175
7374
|
if (existsSync30(traefikDir)) {
|
|
7176
|
-
console.log(
|
|
7375
|
+
console.log(chalk33.dim("Stopping Traefik..."));
|
|
7177
7376
|
try {
|
|
7178
|
-
|
|
7377
|
+
execSync17("docker-compose down", {
|
|
7179
7378
|
cwd: traefikDir,
|
|
7180
7379
|
stdio: "pipe"
|
|
7181
7380
|
});
|
|
7182
|
-
console.log(
|
|
7381
|
+
console.log(chalk33.green("\u2713 Traefik stopped"));
|
|
7183
7382
|
} catch (error) {
|
|
7184
|
-
console.log(
|
|
7383
|
+
console.log(chalk33.yellow("\u26A0 Failed to stop Traefik"));
|
|
7185
7384
|
}
|
|
7186
7385
|
}
|
|
7187
7386
|
}
|