forge-cw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/console/assets/addon-fit-BlxrFPDK.js +1 -0
- package/console/assets/addon-web-links-BWjmmSgS.js +12 -0
- package/console/assets/index-Digctq9W.css +32 -0
- package/console/assets/index-ItVUTuQf.js +31 -0
- package/console/assets/xterm-DlVFs1Kw.js +9 -0
- package/console/index.html +13 -0
- package/cw/.bundle.json +4 -0
- package/cw/agents/presentation-curator.md +119 -0
- package/cw/agents/weather-agent.md +67 -0
- package/cw/cw +6188 -0
- package/cw/cw-shell-integration.sh +132 -0
- package/cw/hooks/HOOKS-README.md +471 -0
- package/cw/hooks/config/hooks-config.json +24 -0
- package/cw/hooks/scripts/auto-compact.py +133 -0
- package/cw/hooks/scripts/hooks.py +472 -0
- package/cw/hooks/scripts/review-autoclose.py +171 -0
- package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.mp3 +0 -0
- package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.wav +0 -0
- package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.mp3 +0 -0
- package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.wav +0 -0
- package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.mp3 +0 -0
- package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.wav +0 -0
- package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.mp3 +0 -0
- package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.wav +0 -0
- package/cw/hooks/sounds/agent_stop/agent_stop.mp3 +0 -0
- package/cw/hooks/sounds/agent_stop/agent_stop.wav +0 -0
- package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.mp3 +0 -0
- package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.wav +0 -0
- package/cw/hooks/sounds/configchange/configchange.mp3 +0 -0
- package/cw/hooks/sounds/configchange/configchange.wav +0 -0
- package/cw/hooks/sounds/notification/notification.mp3 +0 -0
- package/cw/hooks/sounds/notification/notification.wav +0 -0
- package/cw/hooks/sounds/permissionrequest/permissionrequest.mp3 +0 -0
- package/cw/hooks/sounds/permissionrequest/permissionrequest.wav +0 -0
- package/cw/hooks/sounds/posttooluse/posttooluse.mp3 +0 -0
- package/cw/hooks/sounds/posttooluse/posttooluse.wav +0 -0
- package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.mp3 +0 -0
- package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.wav +0 -0
- package/cw/hooks/sounds/precompact/precompact.mp3 +0 -0
- package/cw/hooks/sounds/precompact/precompact.wav +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.mp3 +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.wav +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse.mp3 +0 -0
- package/cw/hooks/sounds/pretooluse/pretooluse.wav +0 -0
- package/cw/hooks/sounds/sessionend/sessionend.mp3 +0 -0
- package/cw/hooks/sounds/sessionend/sessionend.wav +0 -0
- package/cw/hooks/sounds/sessionstart/sessionstart.mp3 +0 -0
- package/cw/hooks/sounds/sessionstart/sessionstart.wav +0 -0
- package/cw/hooks/sounds/setup/Setup.mp3 +0 -0
- package/cw/hooks/sounds/setup/Setup.wav +0 -0
- package/cw/hooks/sounds/stop/stop.mp3 +0 -0
- package/cw/hooks/sounds/stop/stop.wav +0 -0
- package/cw/hooks/sounds/subagentstart/subagentstart.mp3 +0 -0
- package/cw/hooks/sounds/subagentstart/subagentstart.wav +0 -0
- package/cw/hooks/sounds/subagentstop/subagentstop.mp3 +0 -0
- package/cw/hooks/sounds/subagentstop/subagentstop.wav +0 -0
- package/cw/hooks/sounds/taskcompleted/taskcompleted.mp3 +0 -0
- package/cw/hooks/sounds/taskcompleted/taskcompleted.wav +0 -0
- package/cw/hooks/sounds/teammateidle/teammateidle.mp3 +0 -0
- package/cw/hooks/sounds/teammateidle/teammateidle.wav +0 -0
- package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.mp3 +0 -0
- package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.wav +0 -0
- package/cw/hooks/sounds/worktreecreate/worktreecreate.mp3 +0 -0
- package/cw/hooks/sounds/worktreecreate/worktreecreate.wav +0 -0
- package/cw/hooks/sounds/worktreeremove/worktreeremove.mp3 +0 -0
- package/cw/hooks/sounds/worktreeremove/worktreeremove.wav +0 -0
- package/cw/install.sh +243 -0
- package/cw/lib/context/github.sh +51 -0
- package/cw/lib/context/linear.sh +49 -0
- package/cw/lib/context/notion.sh +38 -0
- package/cw/lib/dashboard/activity-hook.py +134 -0
- package/cw/lib/dashboard/index.html +1515 -0
- package/cw/lib/dashboard/server.py +379 -0
- package/cw/lib/dashboard/start.sh +15 -0
- package/cw/lib/harnesses/claude.sh +92 -0
- package/cw/lib/harnesses/codex.sh +245 -0
- package/cw/lib/harnesses/opencode.sh +120 -0
- package/cw/lib/harnesses/pi.sh +66 -0
- package/cw/lib/iterm2.sh +140 -0
- package/cw/templates/CLAUDE.template.md +48 -0
- package/dist/index.js +4376 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../core/dist/db.js
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
function quietSqliteWarning() {
|
|
16
|
+
const emitWarning = process.emitWarning;
|
|
17
|
+
process.emitWarning = function(warning, ...rest) {
|
|
18
|
+
const text = typeof warning === "string" ? warning : warning.message;
|
|
19
|
+
if (text.startsWith("SQLite is an experimental feature"))
|
|
20
|
+
return;
|
|
21
|
+
return emitWarning.call(process, warning, ...rest);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
var loadSqlite, ForgeDB;
|
|
25
|
+
var init_db = __esm({
|
|
26
|
+
"../core/dist/db.js"() {
|
|
27
|
+
"use strict";
|
|
28
|
+
loadSqlite = () => createRequire(import.meta.url)("node:sqlite");
|
|
29
|
+
ForgeDB = class {
|
|
30
|
+
db;
|
|
31
|
+
// node:sqlite is built into Node, so the published package has no native module to compile for it
|
|
32
|
+
constructor(dbPath) {
|
|
33
|
+
this.db = new (loadSqlite()).DatabaseSync(dbPath);
|
|
34
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
35
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
36
|
+
this.migrate();
|
|
37
|
+
}
|
|
38
|
+
migrate() {
|
|
39
|
+
this.db.exec(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
name TEXT NOT NULL UNIQUE,
|
|
43
|
+
path TEXT NOT NULL,
|
|
44
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
45
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS modules (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
name TEXT NOT NULL UNIQUE,
|
|
51
|
+
version TEXT NOT NULL,
|
|
52
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
53
|
+
installed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS action_logs (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
project_id TEXT,
|
|
59
|
+
module_id TEXT NOT NULL,
|
|
60
|
+
action_id TEXT NOT NULL,
|
|
61
|
+
command TEXT NOT NULL,
|
|
62
|
+
exit_code INTEGER,
|
|
63
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
64
|
+
finished_at TEXT,
|
|
65
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS module_settings (
|
|
69
|
+
module_id TEXT NOT NULL,
|
|
70
|
+
key TEXT NOT NULL,
|
|
71
|
+
value TEXT NOT NULL,
|
|
72
|
+
PRIMARY KEY (module_id, key)
|
|
73
|
+
);
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
listTables() {
|
|
77
|
+
const rows = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
78
|
+
return rows.map((r) => r.name);
|
|
79
|
+
}
|
|
80
|
+
addProject(input) {
|
|
81
|
+
const id = randomUUID();
|
|
82
|
+
this.db.prepare("INSERT INTO projects (id, name, path) VALUES (?, ?, ?)").run(id, input.name, input.path);
|
|
83
|
+
return id;
|
|
84
|
+
}
|
|
85
|
+
listProjects() {
|
|
86
|
+
const rows = this.db.prepare("SELECT * FROM projects ORDER BY created_at DESC").all();
|
|
87
|
+
return rows.map((r) => ({
|
|
88
|
+
id: r.id,
|
|
89
|
+
name: r.name,
|
|
90
|
+
path: r.path,
|
|
91
|
+
createdAt: r.created_at,
|
|
92
|
+
updatedAt: r.updated_at
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
getProject(id) {
|
|
96
|
+
const row = this.db.prepare("SELECT * FROM projects WHERE id = ?").get(id);
|
|
97
|
+
if (!row)
|
|
98
|
+
return void 0;
|
|
99
|
+
return {
|
|
100
|
+
id: row.id,
|
|
101
|
+
name: row.name,
|
|
102
|
+
path: row.path,
|
|
103
|
+
createdAt: row.created_at,
|
|
104
|
+
updatedAt: row.updated_at
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
removeProject(id) {
|
|
108
|
+
this.db.prepare("DELETE FROM projects WHERE id = ?").run(id);
|
|
109
|
+
}
|
|
110
|
+
addModule(input) {
|
|
111
|
+
const id = randomUUID();
|
|
112
|
+
this.db.prepare("INSERT INTO modules (id, name, version) VALUES (?, ?, ?)").run(id, input.name, input.version);
|
|
113
|
+
return id;
|
|
114
|
+
}
|
|
115
|
+
listModules() {
|
|
116
|
+
const rows = this.db.prepare("SELECT * FROM modules ORDER BY installed_at DESC").all();
|
|
117
|
+
return rows.map((r) => ({
|
|
118
|
+
id: r.id,
|
|
119
|
+
name: r.name,
|
|
120
|
+
version: r.version,
|
|
121
|
+
enabled: r.enabled === 1,
|
|
122
|
+
installedAt: r.installed_at
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
removeModule(name) {
|
|
126
|
+
this.db.prepare("DELETE FROM modules WHERE name = ?").run(name);
|
|
127
|
+
}
|
|
128
|
+
logAction(input) {
|
|
129
|
+
const id = randomUUID();
|
|
130
|
+
this.db.prepare("INSERT INTO action_logs (id, project_id, module_id, action_id, command) VALUES (?, ?, ?, ?, ?)").run(id, input.projectId, input.moduleId, input.actionId, input.command);
|
|
131
|
+
return id;
|
|
132
|
+
}
|
|
133
|
+
getActionLog(id) {
|
|
134
|
+
const row = this.db.prepare("SELECT * FROM action_logs WHERE id = ?").get(id);
|
|
135
|
+
if (!row)
|
|
136
|
+
return void 0;
|
|
137
|
+
return {
|
|
138
|
+
id: row.id,
|
|
139
|
+
projectId: row.project_id,
|
|
140
|
+
moduleId: row.module_id,
|
|
141
|
+
actionId: row.action_id,
|
|
142
|
+
command: row.command,
|
|
143
|
+
exitCode: row.exit_code,
|
|
144
|
+
startedAt: row.started_at,
|
|
145
|
+
finishedAt: row.finished_at
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
completeAction(id, exitCode) {
|
|
149
|
+
this.db.prepare("UPDATE action_logs SET exit_code = ?, finished_at = datetime('now') WHERE id = ?").run(exitCode, id);
|
|
150
|
+
}
|
|
151
|
+
getModuleSettings(moduleId) {
|
|
152
|
+
const rows = this.db.prepare("SELECT key, value FROM module_settings WHERE module_id = ?").all(moduleId);
|
|
153
|
+
const result2 = {};
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
result2[row.key] = row.value;
|
|
156
|
+
}
|
|
157
|
+
return result2;
|
|
158
|
+
}
|
|
159
|
+
setModuleSetting(moduleId, key, value) {
|
|
160
|
+
this.db.prepare("INSERT INTO module_settings (module_id, key, value) VALUES (?, ?, ?) ON CONFLICT(module_id, key) DO UPDATE SET value = excluded.value").run(moduleId, key, value);
|
|
161
|
+
}
|
|
162
|
+
listActionLogs(opts) {
|
|
163
|
+
const { moduleId, limit = 50 } = opts;
|
|
164
|
+
let sql = "SELECT * FROM action_logs";
|
|
165
|
+
const params = [];
|
|
166
|
+
if (moduleId) {
|
|
167
|
+
sql += " WHERE module_id = ?";
|
|
168
|
+
params.push(moduleId);
|
|
169
|
+
}
|
|
170
|
+
sql += " ORDER BY started_at DESC, rowid DESC LIMIT ?";
|
|
171
|
+
params.push(limit);
|
|
172
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
173
|
+
return rows.map((r) => ({
|
|
174
|
+
id: r.id,
|
|
175
|
+
projectId: r.project_id,
|
|
176
|
+
moduleId: r.module_id,
|
|
177
|
+
actionId: r.action_id,
|
|
178
|
+
command: r.command,
|
|
179
|
+
exitCode: r.exit_code,
|
|
180
|
+
startedAt: r.started_at,
|
|
181
|
+
finishedAt: r.finished_at
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
close() {
|
|
185
|
+
this.db.close();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ../core/dist/modules.js
|
|
192
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
193
|
+
import { join } from "node:path";
|
|
194
|
+
var ModuleLoader;
|
|
195
|
+
var init_modules = __esm({
|
|
196
|
+
"../core/dist/modules.js"() {
|
|
197
|
+
"use strict";
|
|
198
|
+
ModuleLoader = class {
|
|
199
|
+
modulesDir;
|
|
200
|
+
loaded = /* @__PURE__ */ new Map();
|
|
201
|
+
constructor(modulesDir) {
|
|
202
|
+
this.modulesDir = modulesDir;
|
|
203
|
+
}
|
|
204
|
+
discover() {
|
|
205
|
+
this.loaded.clear();
|
|
206
|
+
if (!existsSync(this.modulesDir))
|
|
207
|
+
return [];
|
|
208
|
+
const entries = readdirSync(this.modulesDir, { withFileTypes: true });
|
|
209
|
+
const manifests = [];
|
|
210
|
+
for (const entry of entries) {
|
|
211
|
+
if (!entry.isDirectory())
|
|
212
|
+
continue;
|
|
213
|
+
const manifest = this.load(entry.name);
|
|
214
|
+
if (manifest)
|
|
215
|
+
manifests.push(manifest);
|
|
216
|
+
}
|
|
217
|
+
return manifests;
|
|
218
|
+
}
|
|
219
|
+
load(dirName) {
|
|
220
|
+
const manifestPath = join(this.modulesDir, dirName, "forge-module.json");
|
|
221
|
+
if (!existsSync(manifestPath))
|
|
222
|
+
return void 0;
|
|
223
|
+
try {
|
|
224
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
225
|
+
const manifest = JSON.parse(raw);
|
|
226
|
+
this.loaded.set(dirName, manifest);
|
|
227
|
+
return manifest;
|
|
228
|
+
} catch {
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
getAction(moduleDirName, actionId) {
|
|
233
|
+
const manifest = this.loaded.get(moduleDirName);
|
|
234
|
+
if (!manifest)
|
|
235
|
+
return void 0;
|
|
236
|
+
return manifest.actions.find((a) => a.id === actionId);
|
|
237
|
+
}
|
|
238
|
+
getLoaded() {
|
|
239
|
+
return this.loaded;
|
|
240
|
+
}
|
|
241
|
+
getLoadedCount() {
|
|
242
|
+
return this.loaded.size;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ../core/dist/runner.js
|
|
249
|
+
import { spawn } from "node:child_process";
|
|
250
|
+
var ActionRunner;
|
|
251
|
+
var init_runner = __esm({
|
|
252
|
+
"../core/dist/runner.js"() {
|
|
253
|
+
"use strict";
|
|
254
|
+
ActionRunner = class {
|
|
255
|
+
async exec(command, options) {
|
|
256
|
+
return new Promise((resolve5) => {
|
|
257
|
+
let output = "";
|
|
258
|
+
let timedOut = false;
|
|
259
|
+
let timeoutId;
|
|
260
|
+
const proc = spawn("sh", ["-c", command], {
|
|
261
|
+
cwd: options.cwd,
|
|
262
|
+
env: { ...process.env, ...options.env },
|
|
263
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
264
|
+
});
|
|
265
|
+
const handleData = (data) => {
|
|
266
|
+
const str2 = data.toString();
|
|
267
|
+
output += str2;
|
|
268
|
+
options.onData?.(str2);
|
|
269
|
+
};
|
|
270
|
+
proc.stdout.on("data", handleData);
|
|
271
|
+
proc.stderr.on("data", handleData);
|
|
272
|
+
if (options.signal) {
|
|
273
|
+
options.signal.addEventListener("abort", () => {
|
|
274
|
+
proc.kill("SIGTERM");
|
|
275
|
+
}, { once: true });
|
|
276
|
+
}
|
|
277
|
+
if (options.timeout) {
|
|
278
|
+
timeoutId = setTimeout(() => {
|
|
279
|
+
timedOut = true;
|
|
280
|
+
proc.kill("SIGTERM");
|
|
281
|
+
}, options.timeout);
|
|
282
|
+
}
|
|
283
|
+
proc.on("close", (code) => {
|
|
284
|
+
if (timeoutId)
|
|
285
|
+
clearTimeout(timeoutId);
|
|
286
|
+
resolve5({
|
|
287
|
+
exitCode: code ?? 1,
|
|
288
|
+
output,
|
|
289
|
+
timedOut
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
proc.on("error", () => {
|
|
293
|
+
if (timeoutId)
|
|
294
|
+
clearTimeout(timeoutId);
|
|
295
|
+
resolve5({ exitCode: 1, output, timedOut });
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ../core/dist/auth.js
|
|
304
|
+
function bearerAuth(token) {
|
|
305
|
+
return async (c, next) => {
|
|
306
|
+
if (c.req.path === "/api/health") {
|
|
307
|
+
return next();
|
|
308
|
+
}
|
|
309
|
+
const authHeader = c.req.header("Authorization");
|
|
310
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
311
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
312
|
+
}
|
|
313
|
+
const provided = authHeader.slice(7);
|
|
314
|
+
if (provided !== token) {
|
|
315
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
316
|
+
}
|
|
317
|
+
return next();
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
var init_auth = __esm({
|
|
321
|
+
"../core/dist/auth.js"() {
|
|
322
|
+
"use strict";
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ../core/dist/origin-guard.js
|
|
327
|
+
function isLoopbackHost(host) {
|
|
328
|
+
if (!host)
|
|
329
|
+
return false;
|
|
330
|
+
if (LOOPBACK_HOSTNAMES.has(host))
|
|
331
|
+
return true;
|
|
332
|
+
try {
|
|
333
|
+
return LOOPBACK_HOSTNAMES.has(new URL(`http://${host}`).hostname);
|
|
334
|
+
} catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function isLocalRequest(host, origin) {
|
|
339
|
+
if (!isLoopbackHost(host))
|
|
340
|
+
return false;
|
|
341
|
+
if (origin === void 0)
|
|
342
|
+
return true;
|
|
343
|
+
try {
|
|
344
|
+
return LOOPBACK_HOSTNAMES.has(new URL(origin).hostname);
|
|
345
|
+
} catch {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function resolveListenOptions(isTeam, envHost = process.env.FORGE_HOST) {
|
|
350
|
+
const host = envHost ?? (isTeam ? void 0 : "127.0.0.1");
|
|
351
|
+
return { host, localOnly: !isTeam && isLoopbackHost(host) };
|
|
352
|
+
}
|
|
353
|
+
function localOriginGuard() {
|
|
354
|
+
return async (c, next) => {
|
|
355
|
+
if (!isLocalRequest(new URL(c.req.url).host, c.req.header("Origin"))) {
|
|
356
|
+
return c.json({ error: "Forbidden" }, 403);
|
|
357
|
+
}
|
|
358
|
+
return next();
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
var LOOPBACK_HOSTNAMES;
|
|
362
|
+
var init_origin_guard = __esm({
|
|
363
|
+
"../core/dist/origin-guard.js"() {
|
|
364
|
+
"use strict";
|
|
365
|
+
LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ../core/dist/cw-reader.js
|
|
370
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
371
|
+
import { join as join2 } from "node:path";
|
|
372
|
+
function* walkSessionDirs(baseDir, rel = "", depth = 0) {
|
|
373
|
+
if (depth >= MAX_SESSION_DEPTH)
|
|
374
|
+
return;
|
|
375
|
+
let entries;
|
|
376
|
+
try {
|
|
377
|
+
entries = readdirSync2(join2(baseDir, rel), { withFileTypes: true });
|
|
378
|
+
} catch {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
for (const entry of entries) {
|
|
382
|
+
if (!entry.isDirectory())
|
|
383
|
+
continue;
|
|
384
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
385
|
+
const sessionPath = join2(baseDir, childRel, "session.json");
|
|
386
|
+
if (existsSync2(sessionPath))
|
|
387
|
+
yield { relPath: childRel, absPath: sessionPath };
|
|
388
|
+
else
|
|
389
|
+
yield* walkSessionDirs(baseDir, childRel, depth + 1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
var withHarnessDefaults, CWReader, MAX_SESSION_DEPTH;
|
|
393
|
+
var init_cw_reader = __esm({
|
|
394
|
+
"../core/dist/cw-reader.js"() {
|
|
395
|
+
"use strict";
|
|
396
|
+
withHarnessDefaults = (session) => ({ ...session, harness: session.harness ?? "claude", provider: session.provider ?? "native" });
|
|
397
|
+
CWReader = class {
|
|
398
|
+
cwDir;
|
|
399
|
+
constructor(cwDir) {
|
|
400
|
+
this.cwDir = cwDir ?? join2(process.env.HOME ?? "", ".cw");
|
|
401
|
+
}
|
|
402
|
+
get cwHome() {
|
|
403
|
+
return this.cwDir;
|
|
404
|
+
}
|
|
405
|
+
getProjects() {
|
|
406
|
+
const path = join2(this.cwDir, "projects.json");
|
|
407
|
+
if (!existsSync2(path))
|
|
408
|
+
return {};
|
|
409
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
410
|
+
}
|
|
411
|
+
getSpaces(projectFilter) {
|
|
412
|
+
const sessionsDir = join2(this.cwDir, "sessions");
|
|
413
|
+
if (!existsSync2(sessionsDir))
|
|
414
|
+
return [];
|
|
415
|
+
const sessions = [];
|
|
416
|
+
const projects = readdirSync2(sessionsDir, { withFileTypes: true });
|
|
417
|
+
for (const proj of projects) {
|
|
418
|
+
if (!proj.isDirectory())
|
|
419
|
+
continue;
|
|
420
|
+
if (projectFilter && proj.name !== projectFilter)
|
|
421
|
+
continue;
|
|
422
|
+
const projDir = join2(sessionsDir, proj.name);
|
|
423
|
+
for (const { relPath, absPath } of walkSessionDirs(projDir)) {
|
|
424
|
+
try {
|
|
425
|
+
const data = JSON.parse(readFileSync2(absPath, "utf-8"));
|
|
426
|
+
data.sessionDir = relPath;
|
|
427
|
+
sessions.push(withHarnessDefaults(data));
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return sessions.sort((a, b) => new Date(b.last_opened).getTime() - new Date(a.last_opened).getTime());
|
|
433
|
+
}
|
|
434
|
+
getSession(project, sessionDir) {
|
|
435
|
+
const path = join2(this.cwDir, "sessions", project, sessionDir, "session.json");
|
|
436
|
+
if (!existsSync2(path))
|
|
437
|
+
return null;
|
|
438
|
+
try {
|
|
439
|
+
const data = JSON.parse(readFileSync2(path, "utf-8"));
|
|
440
|
+
data.sessionDir = sessionDir;
|
|
441
|
+
return withHarnessDefaults(data);
|
|
442
|
+
} catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
getNotes(project, sessionDir) {
|
|
447
|
+
const baseDir = join2(this.cwDir, "sessions", project, sessionDir);
|
|
448
|
+
const sessionPath = join2(baseDir, "session.json");
|
|
449
|
+
if (existsSync2(sessionPath)) {
|
|
450
|
+
try {
|
|
451
|
+
const session = JSON.parse(readFileSync2(sessionPath, "utf-8"));
|
|
452
|
+
if (existsSync2(session.notes))
|
|
453
|
+
return readFileSync2(session.notes, "utf-8");
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const name of ["TASK_NOTES.md", "REVIEW_NOTES.md"]) {
|
|
458
|
+
const p = join2(baseDir, name);
|
|
459
|
+
if (existsSync2(p))
|
|
460
|
+
return readFileSync2(p, "utf-8");
|
|
461
|
+
}
|
|
462
|
+
return "";
|
|
463
|
+
}
|
|
464
|
+
getAccounts() {
|
|
465
|
+
const dir = join2(this.cwDir, "accounts");
|
|
466
|
+
if (!existsSync2(dir))
|
|
467
|
+
return [];
|
|
468
|
+
return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
469
|
+
}
|
|
470
|
+
getMCPs(project) {
|
|
471
|
+
const tools = this.getTools(project);
|
|
472
|
+
return {
|
|
473
|
+
global: Object.fromEntries(tools.mcps.filter((m) => m.source === "global").map((m) => [m.name, {}])),
|
|
474
|
+
project: tools.mcps.filter((m) => m.source === "project").map((m) => m.name),
|
|
475
|
+
cw: tools.mcps.filter((m) => m.source === "cw").map((m) => m.name),
|
|
476
|
+
plugins: tools.plugins.map((p) => p.name)
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/** Rich tool/MCP/plugin info for the dashboard */
|
|
480
|
+
getTools(project) {
|
|
481
|
+
const home = process.env.HOME ?? "";
|
|
482
|
+
const mcps = [];
|
|
483
|
+
const plugins = [];
|
|
484
|
+
const claudeSettings = join2(home, ".claude", "settings.json");
|
|
485
|
+
let enabledPlugins = {};
|
|
486
|
+
if (existsSync2(claudeSettings)) {
|
|
487
|
+
try {
|
|
488
|
+
const data = JSON.parse(readFileSync2(claudeSettings, "utf-8"));
|
|
489
|
+
if (data.mcpServers) {
|
|
490
|
+
for (const [name, cfg] of Object.entries(data.mcpServers)) {
|
|
491
|
+
const type = cfg.url ? "url" : cfg.type === "sse" ? "sse" : "command";
|
|
492
|
+
mcps.push({ name, type, source: "global", url: cfg.url });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (data.enabledPlugins)
|
|
496
|
+
enabledPlugins = data.enabledPlugins;
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (project) {
|
|
501
|
+
const projects = this.getProjects();
|
|
502
|
+
const projPath = projects[project]?.path ?? "";
|
|
503
|
+
if (projPath) {
|
|
504
|
+
if (existsSync2(claudeSettings)) {
|
|
505
|
+
try {
|
|
506
|
+
const globalData = JSON.parse(readFileSync2(claudeSettings, "utf-8"));
|
|
507
|
+
const projSettings = globalData.projects?.[projPath];
|
|
508
|
+
if (projSettings?.mcpServers) {
|
|
509
|
+
for (const [name, cfg] of Object.entries(projSettings.mcpServers)) {
|
|
510
|
+
if (!mcps.some((m) => m.name === name)) {
|
|
511
|
+
const type = cfg.url ? "url" : cfg.type === "sse" ? "sse" : "command";
|
|
512
|
+
mcps.push({ name, type, source: "project", url: cfg.url });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (const configPath of [
|
|
520
|
+
join2(projPath, ".mcp.json"),
|
|
521
|
+
join2(projPath, ".claude", "settings.json")
|
|
522
|
+
]) {
|
|
523
|
+
if (existsSync2(configPath)) {
|
|
524
|
+
try {
|
|
525
|
+
const data = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
526
|
+
const servers = data.mcpServers ?? data;
|
|
527
|
+
if (servers && typeof servers === "object") {
|
|
528
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
529
|
+
if (name === "mcpServers")
|
|
530
|
+
continue;
|
|
531
|
+
if (!mcps.some((m) => m.name === name)) {
|
|
532
|
+
const type = cfg.url ? "url" : cfg.type === "sse" ? "sse" : "command";
|
|
533
|
+
mcps.push({ name, type, source: "project", url: cfg.url });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const mcpsDir = join2(this.cwDir, "mcps");
|
|
544
|
+
if (existsSync2(mcpsDir)) {
|
|
545
|
+
const files = readdirSync2(mcpsDir).filter((f) => f.endsWith(".json"));
|
|
546
|
+
for (const f of files) {
|
|
547
|
+
const name = f.replace(".json", "");
|
|
548
|
+
if (!mcps.some((m) => m.name === name)) {
|
|
549
|
+
mcps.push({ name, type: "cw", source: "cw" });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const pluginsFile = join2(home, ".claude", "plugins", "installed_plugins.json");
|
|
554
|
+
if (existsSync2(pluginsFile)) {
|
|
555
|
+
try {
|
|
556
|
+
const data = JSON.parse(readFileSync2(pluginsFile, "utf-8"));
|
|
557
|
+
if (data.plugins) {
|
|
558
|
+
for (const key of Object.keys(data.plugins)) {
|
|
559
|
+
const name = key.split("@")[0];
|
|
560
|
+
const marketplace = key.includes("@") ? key.split("@").slice(1).join("@") : "unknown";
|
|
561
|
+
const enabled = enabledPlugins[key] ?? false;
|
|
562
|
+
let hasMcp = false;
|
|
563
|
+
let mcpName;
|
|
564
|
+
let mcpType;
|
|
565
|
+
const entries = data.plugins[key];
|
|
566
|
+
if (entries?.[0]?.installPath) {
|
|
567
|
+
const pluginMcpFile = join2(entries[0].installPath, ".mcp.json");
|
|
568
|
+
if (existsSync2(pluginMcpFile)) {
|
|
569
|
+
try {
|
|
570
|
+
const raw = JSON.parse(readFileSync2(pluginMcpFile, "utf-8"));
|
|
571
|
+
const mcpData = raw.mcpServers ?? raw;
|
|
572
|
+
const mcpKeys = Object.keys(mcpData).filter((k) => k !== "mcpServers");
|
|
573
|
+
if (mcpKeys.length > 0) {
|
|
574
|
+
hasMcp = true;
|
|
575
|
+
mcpName = mcpKeys[0];
|
|
576
|
+
const cfg = mcpData[mcpKeys[0]];
|
|
577
|
+
mcpType = cfg.url ? "url" : cfg.type === "sse" ? "sse" : "command";
|
|
578
|
+
}
|
|
579
|
+
} catch {
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
plugins.push({ name, enabled, hasMcp, mcpName, mcpType, marketplace });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return { mcps, plugins };
|
|
590
|
+
}
|
|
591
|
+
parseFrontmatter(content) {
|
|
592
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
593
|
+
if (!match)
|
|
594
|
+
return { frontmatter: {}, body: content };
|
|
595
|
+
const rawBlock = match[1];
|
|
596
|
+
const body = match[2];
|
|
597
|
+
const frontmatter = {};
|
|
598
|
+
let inMetadata = false;
|
|
599
|
+
const metadataObj = {};
|
|
600
|
+
for (const line of rawBlock.split("\n")) {
|
|
601
|
+
if (line.match(/^metadata:\s*$/)) {
|
|
602
|
+
inMetadata = true;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (inMetadata) {
|
|
606
|
+
const nested = line.match(/^\s+(\w[\w-]*):\s*(.*)$/);
|
|
607
|
+
if (nested) {
|
|
608
|
+
metadataObj[nested[1]] = nested[2].replace(/^['"]|['"]$/g, "");
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
inMetadata = false;
|
|
612
|
+
}
|
|
613
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
614
|
+
if (kv) {
|
|
615
|
+
frontmatter[kv[1]] = kv[2].replace(/^['"]|['"]$/g, "");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (Object.keys(metadataObj).length > 0)
|
|
619
|
+
frontmatter["metadata"] = metadataObj;
|
|
620
|
+
return { frontmatter, body };
|
|
621
|
+
}
|
|
622
|
+
// directory whose skills/ holds a scope's skills; an unregistered project ref is used as a path
|
|
623
|
+
getSkillConfigDir(scope, scopeRef) {
|
|
624
|
+
if (scope === "global")
|
|
625
|
+
return join2(process.env.HOME ?? "", ".claude");
|
|
626
|
+
if (scope === "account")
|
|
627
|
+
return join2(this.cwDir, "accounts", scopeRef);
|
|
628
|
+
return join2(this.getProjects()[scopeRef]?.path ?? scopeRef, ".claude");
|
|
629
|
+
}
|
|
630
|
+
getSkillDir(scope, scopeRef, name) {
|
|
631
|
+
return join2(this.getSkillConfigDir(scope, scopeRef), "skills", name);
|
|
632
|
+
}
|
|
633
|
+
getSkills(account, project) {
|
|
634
|
+
const results = [];
|
|
635
|
+
const scanDir = (dir, scope, scopeRef) => {
|
|
636
|
+
if (!existsSync2(dir))
|
|
637
|
+
return;
|
|
638
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
639
|
+
for (const entry of entries) {
|
|
640
|
+
if (!entry.isDirectory())
|
|
641
|
+
continue;
|
|
642
|
+
if (entry.name.startsWith("acct--"))
|
|
643
|
+
continue;
|
|
644
|
+
const fullPath = join2(dir, entry.name);
|
|
645
|
+
const skillMd = join2(fullPath, "SKILL.md");
|
|
646
|
+
if (!existsSync2(skillMd))
|
|
647
|
+
continue;
|
|
648
|
+
try {
|
|
649
|
+
const content = readFileSync2(skillMd, "utf-8");
|
|
650
|
+
const { frontmatter } = this.parseFrontmatter(content);
|
|
651
|
+
const refsDir = join2(fullPath, "references");
|
|
652
|
+
const hasReferences = existsSync2(refsDir) && readdirSync2(refsDir).some((f) => f.endsWith(".md"));
|
|
653
|
+
results.push({
|
|
654
|
+
name: String(frontmatter["name"] ?? entry.name),
|
|
655
|
+
dirName: entry.name,
|
|
656
|
+
scope,
|
|
657
|
+
scopeRef,
|
|
658
|
+
description: String(frontmatter["description"] ?? ""),
|
|
659
|
+
domain: frontmatter["domain"] ? String(frontmatter["domain"]) : void 0,
|
|
660
|
+
triggers: frontmatter["triggers"] ? String(frontmatter["triggers"]) : void 0,
|
|
661
|
+
hasReferences
|
|
662
|
+
});
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
scanDir(join2(this.getSkillConfigDir("global", "global"), "skills"), "global", "global");
|
|
668
|
+
if (account)
|
|
669
|
+
scanDir(join2(this.getSkillConfigDir("account", account), "skills"), "account", account);
|
|
670
|
+
if (project)
|
|
671
|
+
scanDir(join2(this.getSkillConfigDir("project", project), "skills"), "project", project);
|
|
672
|
+
return results;
|
|
673
|
+
}
|
|
674
|
+
getSkill(scope, scopeRef, name) {
|
|
675
|
+
const dir = this.getSkillDir(scope, scopeRef, name);
|
|
676
|
+
const skillMd = join2(dir, "SKILL.md");
|
|
677
|
+
if (!existsSync2(skillMd))
|
|
678
|
+
return null;
|
|
679
|
+
try {
|
|
680
|
+
const content = readFileSync2(skillMd, "utf-8");
|
|
681
|
+
const { frontmatter, body } = this.parseFrontmatter(content);
|
|
682
|
+
const references = [];
|
|
683
|
+
const refsDir = join2(dir, "references");
|
|
684
|
+
if (existsSync2(refsDir)) {
|
|
685
|
+
for (const f of readdirSync2(refsDir)) {
|
|
686
|
+
if (!f.endsWith(".md"))
|
|
687
|
+
continue;
|
|
688
|
+
try {
|
|
689
|
+
references.push({ name: f, content: readFileSync2(join2(refsDir, f), "utf-8") });
|
|
690
|
+
} catch {
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return { name: String(frontmatter["name"] ?? name), scope, scopeRef, frontmatter, body, references };
|
|
695
|
+
} catch {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
detectStack(project) {
|
|
700
|
+
const projects = this.getProjects();
|
|
701
|
+
const projPath = projects[project]?.path ?? "";
|
|
702
|
+
const has = (file) => existsSync2(join2(projPath, file));
|
|
703
|
+
let framework = null;
|
|
704
|
+
if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs"))
|
|
705
|
+
framework = "nextjs";
|
|
706
|
+
else if (has("vite.config.ts") || has("vite.config.js"))
|
|
707
|
+
framework = "vite";
|
|
708
|
+
else if (has("astro.config.mjs"))
|
|
709
|
+
framework = "astro";
|
|
710
|
+
else if (has("nuxt.config.ts"))
|
|
711
|
+
framework = "nuxt";
|
|
712
|
+
let testRunner = null;
|
|
713
|
+
if (has("vitest.config.ts") || has("vitest.config.js"))
|
|
714
|
+
testRunner = "vitest";
|
|
715
|
+
else if (has("jest.config.js") || has("jest.config.ts"))
|
|
716
|
+
testRunner = "jest";
|
|
717
|
+
else if (has("pytest.ini") || has("pyproject.toml"))
|
|
718
|
+
testRunner = "pytest";
|
|
719
|
+
return {
|
|
720
|
+
hasPackageJson: has("package.json"),
|
|
721
|
+
hasTests: testRunner !== null || has("tests") || has("__tests__"),
|
|
722
|
+
hasTailwind: has("tailwind.config.js") || has("tailwind.config.ts"),
|
|
723
|
+
hasShadcn: has("components.json"),
|
|
724
|
+
hasTokens: has("tokens") || has("src/tokens"),
|
|
725
|
+
hasFigmaConfig: has(".figma.json") || has("figma.config.ts"),
|
|
726
|
+
hasPlaywright: has("playwright.config.ts"),
|
|
727
|
+
hasDockerfile: has("Dockerfile"),
|
|
728
|
+
framework,
|
|
729
|
+
testRunner
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
MAX_SESSION_DEPTH = 5;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// ../core/dist/cw-types.js
|
|
738
|
+
var ACCOUNT_NAME_RE, HARNESS_NAME_RE, PROVIDER_NAME_RE, MODEL_NAME_RE;
|
|
739
|
+
var init_cw_types = __esm({
|
|
740
|
+
"../core/dist/cw-types.js"() {
|
|
741
|
+
"use strict";
|
|
742
|
+
ACCOUNT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
743
|
+
HARNESS_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
744
|
+
PROVIDER_NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,63}$/;
|
|
745
|
+
MODEL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,127}$/;
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// ../core/dist/cw-doctor.js
|
|
750
|
+
import { execFile } from "node:child_process";
|
|
751
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
752
|
+
import { join as join3 } from "node:path";
|
|
753
|
+
function envWithoutHarness(env = process.env) {
|
|
754
|
+
const out = {};
|
|
755
|
+
for (const [key, value] of Object.entries(env)) {
|
|
756
|
+
if (value !== void 0 && key !== "CW_HARNESS")
|
|
757
|
+
out[key] = value;
|
|
758
|
+
}
|
|
759
|
+
return out;
|
|
760
|
+
}
|
|
761
|
+
function runDoctor(cwBin) {
|
|
762
|
+
return new Promise((resolve5) => {
|
|
763
|
+
execFile(cwBin, ["doctor", "--json"], { timeout: TIMEOUT_MS, env: envWithoutHarness(), maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
|
|
764
|
+
if (err)
|
|
765
|
+
return resolve5({ available: false, reason: err.message });
|
|
766
|
+
let parsed;
|
|
767
|
+
try {
|
|
768
|
+
parsed = JSON.parse(stdout);
|
|
769
|
+
} catch {
|
|
770
|
+
return resolve5({ available: false, reason: "cw doctor --json did not print JSON" });
|
|
771
|
+
}
|
|
772
|
+
if (parsed.schema !== 1)
|
|
773
|
+
return resolve5({ available: false, reason: `unsupported doctor schema ${String(parsed.schema)}` });
|
|
774
|
+
resolve5({ available: true, doctor: parsed });
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
function createDoctorClient(cwBin) {
|
|
779
|
+
let inFlight = null;
|
|
780
|
+
let cached = null;
|
|
781
|
+
return {
|
|
782
|
+
get(fresh = false) {
|
|
783
|
+
if (inFlight)
|
|
784
|
+
return inFlight;
|
|
785
|
+
if (!fresh && cached && Date.now() - cached.at < CACHE_MS)
|
|
786
|
+
return Promise.resolve(cached.result);
|
|
787
|
+
inFlight = runDoctor(cwBin).then((result2) => {
|
|
788
|
+
cached = { at: Date.now(), result: result2 };
|
|
789
|
+
inFlight = null;
|
|
790
|
+
return result2;
|
|
791
|
+
});
|
|
792
|
+
return inFlight;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function readContextTokens(cwHome, env = process.env) {
|
|
797
|
+
const file = join3(cwHome, "tokens.env");
|
|
798
|
+
const text = existsSync3(file) ? readFileSync3(file, "utf-8") : "";
|
|
799
|
+
const has = (name) => Boolean(env[name]?.trim()) || new RegExp(`^\\s*(export\\s+)?${name}\\s*=\\s*\\S`, "m").test(text);
|
|
800
|
+
return { linear: has("LINEAR_API_KEY"), notion: has("NOTION_TOKEN") };
|
|
801
|
+
}
|
|
802
|
+
var CACHE_MS, TIMEOUT_MS;
|
|
803
|
+
var init_cw_doctor = __esm({
|
|
804
|
+
"../core/dist/cw-doctor.js"() {
|
|
805
|
+
"use strict";
|
|
806
|
+
CACHE_MS = 2e3;
|
|
807
|
+
TIMEOUT_MS = 15e3;
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// ../core/dist/task-review.js
|
|
812
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
813
|
+
import { lstatSync } from "node:fs";
|
|
814
|
+
import { join as join4 } from "node:path";
|
|
815
|
+
function parseGitHubRemote(url) {
|
|
816
|
+
const match = GITHUB_REMOTE_RE.exec(url.trim());
|
|
817
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
818
|
+
}
|
|
819
|
+
function summarizeChecks(rollup) {
|
|
820
|
+
if (!Array.isArray(rollup) || rollup.length === 0)
|
|
821
|
+
return "none";
|
|
822
|
+
const entries = rollup;
|
|
823
|
+
const isStatusContext = (e) => e.__typename === "StatusContext" || e.state !== void 0 && e.status === void 0;
|
|
824
|
+
if (entries.some((e) => isStatusContext(e) ? FAILED_STATES.has(e.state ?? "") : FAILED_CONCLUSIONS.has(e.conclusion ?? "")))
|
|
825
|
+
return "failing";
|
|
826
|
+
if (entries.some((e) => isStatusContext(e) ? PENDING_STATES.has(e.state ?? "") : e.status !== "COMPLETED"))
|
|
827
|
+
return "pending";
|
|
828
|
+
return "passing";
|
|
829
|
+
}
|
|
830
|
+
function parseShortstat(text) {
|
|
831
|
+
const num2 = (re) => Number(re.exec(text)?.[1] ?? 0);
|
|
832
|
+
return {
|
|
833
|
+
files: num2(/(\d+) files? changed/),
|
|
834
|
+
insertions: num2(/(\d+) insertions?\(\+\)/),
|
|
835
|
+
deletions: num2(/(\d+) deletions?\(-\)/)
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function githubLink(input) {
|
|
839
|
+
const { repo, kind, pr } = input;
|
|
840
|
+
if (!repo)
|
|
841
|
+
return null;
|
|
842
|
+
const repoUrl = `https://github.com/${repo.owner}/${repo.repo}`;
|
|
843
|
+
if (pr.status === "found") {
|
|
844
|
+
return { url: kind === "review" ? pr.url : `${pr.url}/files`, label: "View PR on GitHub" };
|
|
845
|
+
}
|
|
846
|
+
if (kind === "review") {
|
|
847
|
+
return input.prNumber ? { url: `${repoUrl}/pull/${encodeURIComponent(input.prNumber)}`, label: "View PR on GitHub" } : null;
|
|
848
|
+
}
|
|
849
|
+
if (input.upstream && input.branch) {
|
|
850
|
+
const branch = encodeURIComponent(input.branch);
|
|
851
|
+
if (input.base) {
|
|
852
|
+
const baseName = encodeURIComponent(input.base.replace(/^origin\//, ""));
|
|
853
|
+
return { url: `${repoUrl}/compare/${baseName}...${branch}`, label: "View on GitHub" };
|
|
854
|
+
}
|
|
855
|
+
return { url: `${repoUrl}/tree/${branch}`, label: "View on GitHub" };
|
|
856
|
+
}
|
|
857
|
+
if ((input.commits ?? 0) > 0 || input.uncommitted > 0)
|
|
858
|
+
return { url: null, reason: "Push the branch first" };
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
function closeWarningsFor(input) {
|
|
862
|
+
if (input.workspace !== "ready")
|
|
863
|
+
return [];
|
|
864
|
+
if (input.gitFailed)
|
|
865
|
+
return ["state-unknown"];
|
|
866
|
+
const warnings = [];
|
|
867
|
+
if (input.uncommitted > 0)
|
|
868
|
+
warnings.push("uncommitted");
|
|
869
|
+
const unpushed = input.unpushed !== null ? input.unpushed > 0 : (input.commits ?? 0) > 0;
|
|
870
|
+
if (unpushed)
|
|
871
|
+
warnings.push("unpushed");
|
|
872
|
+
if (input.pr.status === "found" && input.pr.state === "OPEN")
|
|
873
|
+
warnings.push("pr-open");
|
|
874
|
+
if (input.base === null && input.unpushed === null)
|
|
875
|
+
warnings.push("state-unknown");
|
|
876
|
+
return warnings;
|
|
877
|
+
}
|
|
878
|
+
function createRunner(env, timeoutMs = COMMAND_TIMEOUT_MS) {
|
|
879
|
+
return (bin, args, cwd) => new Promise((resolve5) => {
|
|
880
|
+
execFile2(bin, args, { cwd, env, timeout: timeoutMs, maxBuffer: 5 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
881
|
+
if (!err)
|
|
882
|
+
return resolve5({ code: 0, stdout: String(stdout), stderr: String(stderr) });
|
|
883
|
+
const { code } = err;
|
|
884
|
+
if (code === "ENOENT")
|
|
885
|
+
return resolve5({ code: 127, stdout: "", stderr: "ENOENT" });
|
|
886
|
+
resolve5({ code: typeof code === "number" ? code : 1, stdout: String(stdout), stderr: String(stderr) || err.message });
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
function isCwLink(worktree, porcelainLine) {
|
|
891
|
+
if (!porcelainLine.startsWith("?? "))
|
|
892
|
+
return false;
|
|
893
|
+
const path = porcelainLine.slice(3);
|
|
894
|
+
if (!CW_LINKED_FILES.has(path))
|
|
895
|
+
return false;
|
|
896
|
+
try {
|
|
897
|
+
return lstatSync(join4(worktree, path)).isSymbolicLink();
|
|
898
|
+
} catch {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
async function readGitSnapshot(run, worktree) {
|
|
903
|
+
const head = await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], worktree);
|
|
904
|
+
if (head.code !== 0)
|
|
905
|
+
throw new GitReadError(head.stderr.trim() || "git rev-parse failed");
|
|
906
|
+
const status = await run("git", ["status", "--porcelain", "--untracked-files=all"], worktree);
|
|
907
|
+
if (status.code !== 0)
|
|
908
|
+
throw new GitReadError(status.stderr.trim() || "git status failed");
|
|
909
|
+
const branchName = head.stdout.trim();
|
|
910
|
+
const branch = branchName === "HEAD" ? null : branchName;
|
|
911
|
+
let upstream = null;
|
|
912
|
+
let unpushed = null;
|
|
913
|
+
if (branch) {
|
|
914
|
+
const remoteRef = `refs/remotes/origin/${branch}`;
|
|
915
|
+
const verify = await run("git", ["rev-parse", "--verify", "--quiet", remoteRef], worktree);
|
|
916
|
+
if (verify.code === 0) {
|
|
917
|
+
upstream = `origin/${branch}`;
|
|
918
|
+
const count = await run("git", ["rev-list", "--count", `${remoteRef}..HEAD`], worktree);
|
|
919
|
+
unpushed = count.code === 0 ? Number(count.stdout.trim()) : null;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const remote = await run("git", ["remote", "get-url", "origin"], worktree);
|
|
923
|
+
return {
|
|
924
|
+
branch,
|
|
925
|
+
uncommitted: lines(status.stdout).filter((line) => !isCwLink(worktree, line)).length,
|
|
926
|
+
upstream,
|
|
927
|
+
unpushed,
|
|
928
|
+
repo: remote.code === 0 ? parseGitHubRemote(remote.stdout) : null
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async function resolveBase(run, cwd, candidates) {
|
|
932
|
+
for (const candidate of candidates) {
|
|
933
|
+
if (!candidate)
|
|
934
|
+
continue;
|
|
935
|
+
let ref = candidate;
|
|
936
|
+
if (candidate === "origin/HEAD") {
|
|
937
|
+
const head = await run("git", ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], cwd);
|
|
938
|
+
if (head.code !== 0)
|
|
939
|
+
continue;
|
|
940
|
+
ref = head.stdout.trim().replace(/^refs\/remotes\//, "");
|
|
941
|
+
}
|
|
942
|
+
const verify = await run("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], cwd);
|
|
943
|
+
if (verify.code === 0)
|
|
944
|
+
return ref;
|
|
945
|
+
}
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
async function readBaseCounts(run, cwd, base) {
|
|
949
|
+
if (!base)
|
|
950
|
+
return { commits: null, diff: null };
|
|
951
|
+
const count = await run("git", ["rev-list", "--count", `${base}..HEAD`], cwd);
|
|
952
|
+
const stat = await run("git", ["diff", "--shortstat", base], cwd);
|
|
953
|
+
return {
|
|
954
|
+
commits: count.code === 0 ? Number(count.stdout.trim()) : null,
|
|
955
|
+
diff: stat.code === 0 ? parseShortstat(stat.stdout) : null
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function toPullRequestInfo(raw) {
|
|
959
|
+
return {
|
|
960
|
+
status: "found",
|
|
961
|
+
number: raw.number,
|
|
962
|
+
url: raw.url,
|
|
963
|
+
state: PR_STATES.has(raw.state) ? raw.state : "CLOSED",
|
|
964
|
+
isDraft: Boolean(raw.isDraft),
|
|
965
|
+
baseRefName: raw.baseRefName,
|
|
966
|
+
headRefOid: typeof raw.headRefOid === "string" ? raw.headRefOid : "",
|
|
967
|
+
checks: summarizeChecks(raw.statusCheckRollup),
|
|
968
|
+
review: REVIEW_DECISIONS.has(raw.reviewDecision) ? raw.reviewDecision : null
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function ghUnavailable(result2) {
|
|
972
|
+
if (result2.code === 127 && result2.stderr === "ENOENT")
|
|
973
|
+
return { status: "unavailable", reason: "gh is not installed" };
|
|
974
|
+
const firstLine = result2.stderr.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
|
|
975
|
+
return { status: "unavailable", reason: firstLine ?? "gh failed" };
|
|
976
|
+
}
|
|
977
|
+
async function readPullRequestForBranch(run, cwd, branch) {
|
|
978
|
+
const result2 = await run("gh", ["pr", "list", "--head", branch, "--state", "all", "--limit", "1", "--json", PR_FIELDS], cwd);
|
|
979
|
+
if (result2.code !== 0)
|
|
980
|
+
return ghUnavailable(result2);
|
|
981
|
+
try {
|
|
982
|
+
const list = JSON.parse(result2.stdout);
|
|
983
|
+
if (!Array.isArray(list))
|
|
984
|
+
return UNEXPECTED;
|
|
985
|
+
return list.length === 0 ? { status: "none" } : toPullRequestInfo(list[0]);
|
|
986
|
+
} catch {
|
|
987
|
+
return UNEXPECTED;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function readPullRequestByNumber(run, cwd, number) {
|
|
991
|
+
const result2 = await run("gh", ["pr", "view", number, "--json", PR_FIELDS], cwd);
|
|
992
|
+
if (result2.code !== 0)
|
|
993
|
+
return ghUnavailable(result2);
|
|
994
|
+
try {
|
|
995
|
+
const raw = JSON.parse(result2.stdout);
|
|
996
|
+
return typeof raw?.number === "number" ? toPullRequestInfo(raw) : UNEXPECTED;
|
|
997
|
+
} catch {
|
|
998
|
+
return UNEXPECTED;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
function idleState(workspace) {
|
|
1002
|
+
return {
|
|
1003
|
+
workspace,
|
|
1004
|
+
branch: null,
|
|
1005
|
+
base: null,
|
|
1006
|
+
uncommitted: 0,
|
|
1007
|
+
commits: null,
|
|
1008
|
+
upstream: null,
|
|
1009
|
+
unpushed: null,
|
|
1010
|
+
diff: null,
|
|
1011
|
+
pr: { status: "none" },
|
|
1012
|
+
github: null,
|
|
1013
|
+
closeWarnings: []
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
async function buildReviewSessionState(session, projectPath, deps) {
|
|
1017
|
+
if (!projectPath || !deps.exists(projectPath))
|
|
1018
|
+
return idleState("none");
|
|
1019
|
+
const remote = await deps.run("git", ["remote", "get-url", "origin"], projectPath);
|
|
1020
|
+
const repo = remote.code === 0 ? parseGitHubRemote(remote.stdout) : null;
|
|
1021
|
+
const prNumber = session.pr && /^\d+$/.test(session.pr) ? session.pr : void 0;
|
|
1022
|
+
const pr = !prNumber ? { status: "none" } : !repo ? NOT_GITHUB : await deps.limitGh(() => readPullRequestByNumber(deps.run, projectPath, prNumber));
|
|
1023
|
+
const github = githubLink({ repo, kind: "review", prNumber, pr, branch: null, base: null, upstream: null, commits: null, uncommitted: 0 });
|
|
1024
|
+
return { ...idleState("none"), pr, github };
|
|
1025
|
+
}
|
|
1026
|
+
async function isAncestor(run, cwd, ancestor, descendant) {
|
|
1027
|
+
return (await run("git", ["merge-base", "--is-ancestor", ancestor, descendant], cwd)).code === 0;
|
|
1028
|
+
}
|
|
1029
|
+
async function buildTaskReviewState(session, projectPath, deps) {
|
|
1030
|
+
if (session.type === "review")
|
|
1031
|
+
return buildReviewSessionState(session, projectPath, deps);
|
|
1032
|
+
if (session.type !== "task")
|
|
1033
|
+
return idleState("none");
|
|
1034
|
+
const worktree = session.worktree;
|
|
1035
|
+
if (!worktree || !deps.exists(worktree))
|
|
1036
|
+
return idleState("missing");
|
|
1037
|
+
let snapshot;
|
|
1038
|
+
try {
|
|
1039
|
+
snapshot = await readGitSnapshot(deps.run, worktree);
|
|
1040
|
+
} catch {
|
|
1041
|
+
return { ...idleState("ready"), pr: { status: "unavailable", reason: "Could not read git state" }, closeWarnings: ["state-unknown"] };
|
|
1042
|
+
}
|
|
1043
|
+
const { branch, repo, uncommitted, upstream } = snapshot;
|
|
1044
|
+
let { unpushed } = snapshot;
|
|
1045
|
+
let pr = !repo ? NOT_GITHUB : branch ? await deps.limitGh(() => readPullRequestForBranch(deps.run, worktree, branch)) : { status: "none" };
|
|
1046
|
+
if (pr.status === "found") {
|
|
1047
|
+
const valid = OBJECT_ID_RE.test(pr.headRefOid);
|
|
1048
|
+
const headContained = valid && await isAncestor(deps.run, worktree, "HEAD", pr.headRefOid);
|
|
1049
|
+
const headBehind = valid && !headContained && await isAncestor(deps.run, worktree, pr.headRefOid, "HEAD");
|
|
1050
|
+
if (!headContained && !headBehind)
|
|
1051
|
+
pr = { status: "none" };
|
|
1052
|
+
else if (headContained)
|
|
1053
|
+
unpushed = 0;
|
|
1054
|
+
}
|
|
1055
|
+
const base = await resolveBase(deps.run, worktree, [
|
|
1056
|
+
pr.status === "found" ? `origin/${pr.baseRefName}` : null,
|
|
1057
|
+
session.base_branch,
|
|
1058
|
+
"origin/HEAD",
|
|
1059
|
+
"origin/main"
|
|
1060
|
+
]);
|
|
1061
|
+
const { commits, diff } = await readBaseCounts(deps.run, worktree, base);
|
|
1062
|
+
return {
|
|
1063
|
+
workspace: "ready",
|
|
1064
|
+
branch,
|
|
1065
|
+
base,
|
|
1066
|
+
uncommitted,
|
|
1067
|
+
commits,
|
|
1068
|
+
upstream,
|
|
1069
|
+
unpushed,
|
|
1070
|
+
diff,
|
|
1071
|
+
pr,
|
|
1072
|
+
github: githubLink({ repo, kind: "task", pr, branch, base, upstream, commits, uncommitted }),
|
|
1073
|
+
closeWarnings: closeWarningsFor({ workspace: "ready", uncommitted, commits, base, unpushed, pr, gitFailed: false })
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function createLimiter(max) {
|
|
1077
|
+
let active = 0;
|
|
1078
|
+
const waiting = [];
|
|
1079
|
+
return async (fn) => {
|
|
1080
|
+
if (active >= max)
|
|
1081
|
+
await new Promise((resolve5) => waiting.push(resolve5));
|
|
1082
|
+
else
|
|
1083
|
+
active++;
|
|
1084
|
+
try {
|
|
1085
|
+
return await fn();
|
|
1086
|
+
} finally {
|
|
1087
|
+
const next = waiting.shift();
|
|
1088
|
+
if (next)
|
|
1089
|
+
next();
|
|
1090
|
+
else
|
|
1091
|
+
active--;
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
var GITHUB_REMOTE_RE, FAILED_CONCLUSIONS, FAILED_STATES, PENDING_STATES, COMMAND_TIMEOUT_MS, runCommand, GitReadError, lines, CW_LINKED_FILES, PR_FIELDS, PR_STATES, REVIEW_DECISIONS, UNEXPECTED, NOT_GITHUB, OBJECT_ID_RE;
|
|
1096
|
+
var init_task_review = __esm({
|
|
1097
|
+
"../core/dist/task-review.js"() {
|
|
1098
|
+
"use strict";
|
|
1099
|
+
GITHUB_REMOTE_RE = /^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/github\.com\/)([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/;
|
|
1100
|
+
FAILED_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"]);
|
|
1101
|
+
FAILED_STATES = /* @__PURE__ */ new Set(["FAILURE", "ERROR"]);
|
|
1102
|
+
PENDING_STATES = /* @__PURE__ */ new Set(["PENDING", "EXPECTED"]);
|
|
1103
|
+
COMMAND_TIMEOUT_MS = 1e4;
|
|
1104
|
+
runCommand = createRunner();
|
|
1105
|
+
GitReadError = class extends Error {
|
|
1106
|
+
};
|
|
1107
|
+
lines = (text) => text.split("\n").filter((line) => line.length > 0);
|
|
1108
|
+
CW_LINKED_FILES = /* @__PURE__ */ new Set([".claude", ".env", "TASK_NOTES.md", "SHARED_CONTEXT.md"]);
|
|
1109
|
+
PR_FIELDS = "number,state,url,isDraft,baseRefName,headRefOid,reviewDecision,statusCheckRollup";
|
|
1110
|
+
PR_STATES = /* @__PURE__ */ new Set(["OPEN", "MERGED", "CLOSED"]);
|
|
1111
|
+
REVIEW_DECISIONS = /* @__PURE__ */ new Set(["APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED"]);
|
|
1112
|
+
UNEXPECTED = { status: "unavailable", reason: "gh returned unexpected output" };
|
|
1113
|
+
NOT_GITHUB = { status: "unavailable", reason: "origin is not a GitHub repository" };
|
|
1114
|
+
OBJECT_ID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// ../core/dist/usage-window.js
|
|
1119
|
+
function severityOf(percent) {
|
|
1120
|
+
if (percent >= CRITICAL_AT)
|
|
1121
|
+
return "critical";
|
|
1122
|
+
if (percent >= WARNING_AT)
|
|
1123
|
+
return "warning";
|
|
1124
|
+
return "normal";
|
|
1125
|
+
}
|
|
1126
|
+
function windowLabel(minutes) {
|
|
1127
|
+
if (!minutes || minutes <= 0)
|
|
1128
|
+
return "";
|
|
1129
|
+
if (minutes % MINS_PER_DAY === 0)
|
|
1130
|
+
return `${minutes / MINS_PER_DAY} d`;
|
|
1131
|
+
if (minutes % MINS_PER_HOUR === 0)
|
|
1132
|
+
return `${minutes / MINS_PER_HOUR} h`;
|
|
1133
|
+
return `${minutes} min`;
|
|
1134
|
+
}
|
|
1135
|
+
var WARNING_AT, CRITICAL_AT, MINS_PER_HOUR, MINS_PER_DAY, severityFrom, obj, num, str;
|
|
1136
|
+
var init_usage_window = __esm({
|
|
1137
|
+
"../core/dist/usage-window.js"() {
|
|
1138
|
+
"use strict";
|
|
1139
|
+
WARNING_AT = 70;
|
|
1140
|
+
CRITICAL_AT = 90;
|
|
1141
|
+
MINS_PER_HOUR = 60;
|
|
1142
|
+
MINS_PER_DAY = 1440;
|
|
1143
|
+
severityFrom = (value, percent) => value === "normal" || value === "warning" || value === "critical" ? value : severityOf(percent);
|
|
1144
|
+
obj = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1145
|
+
num = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1146
|
+
str = (value) => typeof value === "string" && value ? value : null;
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// ../core/dist/usage-claude.js
|
|
1151
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
1152
|
+
import { createHash } from "node:crypto";
|
|
1153
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1154
|
+
import { homedir } from "node:os";
|
|
1155
|
+
import { join as join5 } from "node:path";
|
|
1156
|
+
import { promisify } from "node:util";
|
|
1157
|
+
function mapClaudeUsage(payload) {
|
|
1158
|
+
const limits = Array.isArray(payload.limits) ? payload.limits : null;
|
|
1159
|
+
if (limits) {
|
|
1160
|
+
return limits.flatMap((entry) => {
|
|
1161
|
+
const limit = obj(entry);
|
|
1162
|
+
const minutes = limit ? CLAUDE_WINDOW_MINS[String(limit.kind)] : void 0;
|
|
1163
|
+
const percent = num(limit?.percent);
|
|
1164
|
+
if (!minutes || percent === null)
|
|
1165
|
+
return [];
|
|
1166
|
+
return [{
|
|
1167
|
+
label: windowLabel(minutes),
|
|
1168
|
+
percent,
|
|
1169
|
+
resetsAt: str(limit?.resets_at),
|
|
1170
|
+
severity: severityFrom(limit?.severity, percent),
|
|
1171
|
+
scope: str(obj(obj(limit?.scope)?.model)?.display_name)
|
|
1172
|
+
}];
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
return [["five_hour", SESSION_MINS], ["seven_day", WEEKLY_MINS]].flatMap(([key, minutes]) => {
|
|
1176
|
+
const window = obj(payload[key]);
|
|
1177
|
+
const percent = num(window?.utilization);
|
|
1178
|
+
if (percent === null)
|
|
1179
|
+
return [];
|
|
1180
|
+
return [{ label: windowLabel(minutes), percent, resetsAt: str(window?.resets_at), severity: severityFrom(null, percent), scope: null }];
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
function claudeKeychainService(configDir) {
|
|
1184
|
+
const dir = configDir.replace(/\/+$/, "");
|
|
1185
|
+
if (dir === join5(homedir(), ".claude"))
|
|
1186
|
+
return KEYCHAIN_SERVICE;
|
|
1187
|
+
return `${KEYCHAIN_SERVICE}-${createHash("sha256").update(dir).digest("hex").slice(0, 8)}`;
|
|
1188
|
+
}
|
|
1189
|
+
async function readFromKeychain(service) {
|
|
1190
|
+
try {
|
|
1191
|
+
const { stdout } = await execFileAsync("security", ["find-generic-password", "-s", service, "-w"], { encoding: "utf-8" });
|
|
1192
|
+
return String(stdout);
|
|
1193
|
+
} catch {
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function parseCredentials(raw) {
|
|
1198
|
+
if (!raw)
|
|
1199
|
+
return null;
|
|
1200
|
+
try {
|
|
1201
|
+
const oauth = JSON.parse(raw).claudeAiOauth;
|
|
1202
|
+
const accessToken = typeof oauth?.accessToken === "string" ? oauth.accessToken : null;
|
|
1203
|
+
if (!accessToken)
|
|
1204
|
+
return null;
|
|
1205
|
+
return { accessToken, expiresAt: typeof oauth?.expiresAt === "number" ? oauth.expiresAt : null };
|
|
1206
|
+
} catch {
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async function readCredentials(configDir, deps) {
|
|
1211
|
+
const readKeychain = deps.readKeychain ?? (process.platform === "darwin" ? readFromKeychain : () => null);
|
|
1212
|
+
const fromKeychain = parseCredentials(await readKeychain(claudeKeychainService(configDir)));
|
|
1213
|
+
if (fromKeychain)
|
|
1214
|
+
return fromKeychain;
|
|
1215
|
+
try {
|
|
1216
|
+
return parseCredentials(readFileSync4(join5(configDir, ".credentials.json"), "utf-8"));
|
|
1217
|
+
} catch {
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function probeClaudeUsage(configDir, deps = {}) {
|
|
1222
|
+
const now = deps.now ?? Date.now;
|
|
1223
|
+
const credentials = await readCredentials(configDir, deps);
|
|
1224
|
+
if (!credentials)
|
|
1225
|
+
return { state: "not_connected", windows: [], detail: null };
|
|
1226
|
+
if (credentials.expiresAt !== null && credentials.expiresAt <= now()) {
|
|
1227
|
+
return { state: "expired", windows: [], detail: "The stored token has expired" };
|
|
1228
|
+
}
|
|
1229
|
+
const call = deps.fetchImpl ?? fetch;
|
|
1230
|
+
try {
|
|
1231
|
+
const response = await call(USAGE_URL, {
|
|
1232
|
+
headers: { authorization: `Bearer ${credentials.accessToken}`, "anthropic-beta": OAUTH_BETA },
|
|
1233
|
+
signal: AbortSignal.timeout(TIMEOUT_MS2)
|
|
1234
|
+
});
|
|
1235
|
+
if (response.status === 401 || response.status === 403)
|
|
1236
|
+
return { state: "expired", windows: [], detail: "Claude rejected the stored token" };
|
|
1237
|
+
const body = await response.text();
|
|
1238
|
+
if (!response.ok)
|
|
1239
|
+
return { state: "error", windows: [], detail: `Claude answered ${response.status}` };
|
|
1240
|
+
return { state: "ok", windows: mapClaudeUsage(JSON.parse(body)), detail: null };
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
return { state: "error", windows: [], detail: err instanceof Error ? err.message : String(err) };
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
var execFileAsync, USAGE_URL, OAUTH_BETA, TIMEOUT_MS2, KEYCHAIN_SERVICE, SESSION_MINS, WEEKLY_MINS, CLAUDE_WINDOW_MINS;
|
|
1246
|
+
var init_usage_claude = __esm({
|
|
1247
|
+
"../core/dist/usage-claude.js"() {
|
|
1248
|
+
"use strict";
|
|
1249
|
+
init_usage_window();
|
|
1250
|
+
execFileAsync = promisify(execFile3);
|
|
1251
|
+
USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
1252
|
+
OAUTH_BETA = "oauth-2025-04-20";
|
|
1253
|
+
TIMEOUT_MS2 = 1e4;
|
|
1254
|
+
KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
1255
|
+
SESSION_MINS = 300;
|
|
1256
|
+
WEEKLY_MINS = 10080;
|
|
1257
|
+
CLAUDE_WINDOW_MINS = { session: SESSION_MINS, weekly_all: WEEKLY_MINS, weekly_scoped: WEEKLY_MINS };
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// ../core/dist/usage-codex.js
|
|
1262
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1263
|
+
function mapCodexUsage(payload) {
|
|
1264
|
+
const limits = obj(payload.rateLimits);
|
|
1265
|
+
return ["primary", "secondary"].flatMap((key) => {
|
|
1266
|
+
const window = obj(limits?.[key]);
|
|
1267
|
+
const percent = num(window?.usedPercent);
|
|
1268
|
+
if (percent === null)
|
|
1269
|
+
return [];
|
|
1270
|
+
const resetsAt = num(window?.resetsAt);
|
|
1271
|
+
return [{
|
|
1272
|
+
label: windowLabel(num(window?.windowDurationMins)),
|
|
1273
|
+
percent,
|
|
1274
|
+
resetsAt: resetsAt === null ? null : new Date(resetsAt * 1e3).toISOString(),
|
|
1275
|
+
severity: severityOf(percent),
|
|
1276
|
+
scope: null
|
|
1277
|
+
}];
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
function callCodexAppServer(bin, codexHome, timeoutMs = TIMEOUT_MS3) {
|
|
1281
|
+
return new Promise((resolve5, reject) => {
|
|
1282
|
+
const child = spawn2(bin, ["app-server"], { env: { ...CHILD_ENV, CODEX_HOME: codexHome }, stdio: ["pipe", "pipe", "ignore"] });
|
|
1283
|
+
let buffer = "";
|
|
1284
|
+
let settled = false;
|
|
1285
|
+
const stop = () => {
|
|
1286
|
+
settled = true;
|
|
1287
|
+
clearTimeout(timer);
|
|
1288
|
+
child.stdin.end();
|
|
1289
|
+
child.kill();
|
|
1290
|
+
setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
|
|
1291
|
+
};
|
|
1292
|
+
const fail = (err) => {
|
|
1293
|
+
if (!settled) {
|
|
1294
|
+
stop();
|
|
1295
|
+
reject(err);
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
const done = (value) => {
|
|
1299
|
+
if (!settled) {
|
|
1300
|
+
stop();
|
|
1301
|
+
resolve5(value);
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
const timer = setTimeout(() => fail(new Error(`codex app-server did not answer in ${timeoutMs / 1e3} s`)), timeoutMs);
|
|
1305
|
+
child.on("error", fail);
|
|
1306
|
+
child.on("exit", () => fail(new Error("codex app-server exited before answering")));
|
|
1307
|
+
child.stdout.setEncoding("utf-8");
|
|
1308
|
+
child.stdout.on("data", (chunk) => {
|
|
1309
|
+
buffer += chunk;
|
|
1310
|
+
const lines2 = buffer.split("\n");
|
|
1311
|
+
buffer = lines2.pop() ?? "";
|
|
1312
|
+
for (const line of lines2) {
|
|
1313
|
+
if (!line.trim())
|
|
1314
|
+
continue;
|
|
1315
|
+
let message;
|
|
1316
|
+
try {
|
|
1317
|
+
message = JSON.parse(line);
|
|
1318
|
+
} catch {
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
if (message.id !== READ_ID)
|
|
1322
|
+
continue;
|
|
1323
|
+
if (message.error)
|
|
1324
|
+
return fail(new Error(String(message.error.message ?? "codex app-server returned an error")));
|
|
1325
|
+
return done(obj(message.result) ?? {});
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
const send = (message) => child.stdin.write(`${JSON.stringify(message)}
|
|
1329
|
+
`);
|
|
1330
|
+
send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { clientInfo: { name: "forge", title: "Forge", version: "0.1.0" } } });
|
|
1331
|
+
send({ jsonrpc: "2.0", method: "initialized", params: {} });
|
|
1332
|
+
send({ jsonrpc: "2.0", id: READ_ID, method: "account/rateLimits/read", params: {} });
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
async function probeCodexUsage(bin, codexHome, deps = {}) {
|
|
1336
|
+
const call = deps.callAppServer ?? callCodexAppServer;
|
|
1337
|
+
try {
|
|
1338
|
+
const windows = mapCodexUsage(await call(bin, codexHome));
|
|
1339
|
+
if (windows.length === 0)
|
|
1340
|
+
return { state: "not_connected", windows: [], detail: null };
|
|
1341
|
+
return { state: "ok", windows, detail: null };
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
return { state: "error", windows: [], detail: err instanceof Error ? err.message : String(err) };
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
var TIMEOUT_MS3, KILL_GRACE_MS, READ_ID, CHILD_ENV;
|
|
1347
|
+
var init_usage_codex = __esm({
|
|
1348
|
+
"../core/dist/usage-codex.js"() {
|
|
1349
|
+
"use strict";
|
|
1350
|
+
init_cw_doctor();
|
|
1351
|
+
init_usage_window();
|
|
1352
|
+
TIMEOUT_MS3 = 2e4;
|
|
1353
|
+
KILL_GRACE_MS = 2e3;
|
|
1354
|
+
READ_ID = 2;
|
|
1355
|
+
CHILD_ENV = envWithoutHarness();
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
// ../core/dist/usage.js
|
|
1360
|
+
function createUsageClient(deps = {}) {
|
|
1361
|
+
const probes = { ...PROBES, ...deps.probes };
|
|
1362
|
+
const limit = createLimiter(MAX_CONCURRENT);
|
|
1363
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1364
|
+
const lastGood = /* @__PURE__ */ new Map();
|
|
1365
|
+
const settle = async (key, account, cell, probe, doctor) => {
|
|
1366
|
+
const result2 = await limit(() => probe(cell, doctor));
|
|
1367
|
+
const base = { account, harness: cell.harness, detail: result2.detail };
|
|
1368
|
+
if (result2.state === "ok") {
|
|
1369
|
+
lastGood.set(key, result2.windows);
|
|
1370
|
+
return { ...base, state: "ok", windows: result2.windows, stale: false };
|
|
1371
|
+
}
|
|
1372
|
+
const previous = lastGood.get(key);
|
|
1373
|
+
if (result2.state === "error" && previous && previous.length > 0) {
|
|
1374
|
+
return { ...base, state: "ok", windows: previous, stale: true };
|
|
1375
|
+
}
|
|
1376
|
+
lastGood.delete(key);
|
|
1377
|
+
return { ...base, state: result2.state, windows: [], stale: false };
|
|
1378
|
+
};
|
|
1379
|
+
return {
|
|
1380
|
+
get(doctor, fresh = false) {
|
|
1381
|
+
const now = Date.now();
|
|
1382
|
+
const live = /* @__PURE__ */ new Set();
|
|
1383
|
+
const pending = [];
|
|
1384
|
+
for (const account of doctor.accounts) {
|
|
1385
|
+
for (const cell of account.harnesses) {
|
|
1386
|
+
const probe = probes[cell.harness];
|
|
1387
|
+
if (!probe || cell.status !== "connected")
|
|
1388
|
+
continue;
|
|
1389
|
+
const key = `${account.name}/${cell.harness}`;
|
|
1390
|
+
live.add(key);
|
|
1391
|
+
const cached = cache.get(key);
|
|
1392
|
+
if (!fresh && cached && now - cached.at < TTL_MS) {
|
|
1393
|
+
pending.push(cached.value);
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
const value = settle(key, account.name, cell, probe, doctor);
|
|
1397
|
+
cache.set(key, { at: now, value });
|
|
1398
|
+
pending.push(value);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
for (const key of cache.keys()) {
|
|
1402
|
+
if (live.has(key))
|
|
1403
|
+
continue;
|
|
1404
|
+
cache.delete(key);
|
|
1405
|
+
lastGood.delete(key);
|
|
1406
|
+
}
|
|
1407
|
+
return Promise.all(pending);
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
var PROBES, TTL_MS, MAX_CONCURRENT;
|
|
1412
|
+
var init_usage = __esm({
|
|
1413
|
+
"../core/dist/usage.js"() {
|
|
1414
|
+
"use strict";
|
|
1415
|
+
init_task_review();
|
|
1416
|
+
init_usage_claude();
|
|
1417
|
+
init_usage_codex();
|
|
1418
|
+
PROBES = {
|
|
1419
|
+
claude: (cell) => probeClaudeUsage(cell.config_dir),
|
|
1420
|
+
codex: (cell, doctor) => {
|
|
1421
|
+
const bin = doctor.harnesses.find((h) => h.name === "codex")?.path;
|
|
1422
|
+
return bin ? probeCodexUsage(bin, cell.config_dir) : Promise.resolve({ state: "error", windows: [], detail: "codex is not on PATH" });
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
TTL_MS = 5e4;
|
|
1426
|
+
MAX_CONCURRENT = 4;
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
// ../core/dist/harness-capabilities.js
|
|
1431
|
+
function supports(harness, cap) {
|
|
1432
|
+
return (HARNESS_CAPABILITIES[harness ?? "claude"] ?? []).includes(cap);
|
|
1433
|
+
}
|
|
1434
|
+
var HARNESS_CAPABILITIES;
|
|
1435
|
+
var init_harness_capabilities = __esm({
|
|
1436
|
+
"../core/dist/harness-capabilities.js"() {
|
|
1437
|
+
"use strict";
|
|
1438
|
+
HARNESS_CAPABILITIES = {
|
|
1439
|
+
claude: ["skip_permissions", "agent_teams", "slash_commands", "mcp", "model_flag", "resume_by_name", "continue_last"],
|
|
1440
|
+
codex: ["headless_login", "api_key_login", "custom_provider", "model_flag", "continue_last"],
|
|
1441
|
+
pi: ["model_flag"],
|
|
1442
|
+
opencode: ["custom_provider", "model_flag"]
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
// ../core/dist/login-manager.js
|
|
1448
|
+
import * as pty from "node-pty";
|
|
1449
|
+
var OUTPUT_LINES, MAX_RUNNING_MS, KEEP_EXITED_MS, ANSI_RE, URL_RE, CODE_RE, spawnInPty, LoginManager;
|
|
1450
|
+
var init_login_manager = __esm({
|
|
1451
|
+
"../core/dist/login-manager.js"() {
|
|
1452
|
+
"use strict";
|
|
1453
|
+
init_cw_doctor();
|
|
1454
|
+
OUTPUT_LINES = 50;
|
|
1455
|
+
MAX_RUNNING_MS = 15 * 60 * 1e3;
|
|
1456
|
+
KEEP_EXITED_MS = 5 * 60 * 1e3;
|
|
1457
|
+
ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*\x07/g;
|
|
1458
|
+
URL_RE = /^CW_LOGIN_URL=(https?:\/\/\S+)$/;
|
|
1459
|
+
CODE_RE = /^CW_LOGIN_CODE=([A-Za-z0-9-]{4,64})$/;
|
|
1460
|
+
spawnInPty = (file, args, options) => pty.spawn(file, args, { name: "xterm-256color", cols: 120, rows: 40, cwd: options.cwd, env: options.env });
|
|
1461
|
+
LoginManager = class {
|
|
1462
|
+
cwBin;
|
|
1463
|
+
spawn;
|
|
1464
|
+
entries = /* @__PURE__ */ new Map();
|
|
1465
|
+
constructor(cwBin, spawn7 = spawnInPty) {
|
|
1466
|
+
this.cwBin = cwBin;
|
|
1467
|
+
this.spawn = spawn7;
|
|
1468
|
+
}
|
|
1469
|
+
start(account, harness) {
|
|
1470
|
+
const key = `${account}::${harness}`;
|
|
1471
|
+
const existing = this.entries.get(key);
|
|
1472
|
+
if (existing?.state.status === "running")
|
|
1473
|
+
return existing.state;
|
|
1474
|
+
if (existing)
|
|
1475
|
+
this.drop(key);
|
|
1476
|
+
const state = {
|
|
1477
|
+
account,
|
|
1478
|
+
harness,
|
|
1479
|
+
status: "running",
|
|
1480
|
+
url: null,
|
|
1481
|
+
code: null,
|
|
1482
|
+
exitCode: null,
|
|
1483
|
+
output: [],
|
|
1484
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1485
|
+
};
|
|
1486
|
+
const proc = this.spawn(this.cwBin, ["account", "login", account, "--harness", harness, "--no-browser"], {
|
|
1487
|
+
cwd: process.env.HOME ?? process.cwd(),
|
|
1488
|
+
env: envWithoutHarness()
|
|
1489
|
+
});
|
|
1490
|
+
const entry = { state, proc, partial: "", timer: null, subscriptions: [] };
|
|
1491
|
+
this.entries.set(key, entry);
|
|
1492
|
+
entry.subscriptions.push(proc.onData((data) => this.ingest(entry, data)));
|
|
1493
|
+
entry.subscriptions.push(proc.onExit(({ exitCode }) => this.finish(key, entry, exitCode)));
|
|
1494
|
+
entry.timer = setTimeout(() => this.stop(account, harness), MAX_RUNNING_MS);
|
|
1495
|
+
return state;
|
|
1496
|
+
}
|
|
1497
|
+
get(account, harness) {
|
|
1498
|
+
return this.entries.get(`${account}::${harness}`)?.state;
|
|
1499
|
+
}
|
|
1500
|
+
stop(account, harness) {
|
|
1501
|
+
const key = `${account}::${harness}`;
|
|
1502
|
+
const entry = this.entries.get(key);
|
|
1503
|
+
if (!entry || entry.state.status !== "running")
|
|
1504
|
+
return;
|
|
1505
|
+
try {
|
|
1506
|
+
entry.proc.kill();
|
|
1507
|
+
} catch {
|
|
1508
|
+
}
|
|
1509
|
+
this.finish(key, entry, null);
|
|
1510
|
+
}
|
|
1511
|
+
dispose() {
|
|
1512
|
+
for (const [key, entry] of this.entries) {
|
|
1513
|
+
for (const sub of entry.subscriptions)
|
|
1514
|
+
sub.dispose();
|
|
1515
|
+
if (entry.state.status === "running") {
|
|
1516
|
+
try {
|
|
1517
|
+
entry.proc.kill();
|
|
1518
|
+
} catch {
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
this.drop(key);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
ingest(entry, data) {
|
|
1525
|
+
const lines2 = (entry.partial + data).split("\n");
|
|
1526
|
+
entry.partial = lines2.pop() ?? "";
|
|
1527
|
+
for (const raw of lines2)
|
|
1528
|
+
this.addLine(entry.state, raw);
|
|
1529
|
+
}
|
|
1530
|
+
addLine(state, raw) {
|
|
1531
|
+
const line = raw.replace(ANSI_RE, "").replace(/\r/g, "").trim();
|
|
1532
|
+
if (!line)
|
|
1533
|
+
return;
|
|
1534
|
+
state.output.push(line);
|
|
1535
|
+
if (state.output.length > OUTPUT_LINES)
|
|
1536
|
+
state.output.splice(0, state.output.length - OUTPUT_LINES);
|
|
1537
|
+
state.url ??= URL_RE.exec(line)?.[1] ?? null;
|
|
1538
|
+
state.code ??= CODE_RE.exec(line)?.[1] ?? null;
|
|
1539
|
+
}
|
|
1540
|
+
finish(key, entry, exitCode) {
|
|
1541
|
+
if (entry.state.status === "exited")
|
|
1542
|
+
return;
|
|
1543
|
+
if (entry.partial)
|
|
1544
|
+
this.addLine(entry.state, entry.partial);
|
|
1545
|
+
entry.partial = "";
|
|
1546
|
+
entry.state.status = "exited";
|
|
1547
|
+
entry.state.exitCode = exitCode;
|
|
1548
|
+
if (entry.timer)
|
|
1549
|
+
clearTimeout(entry.timer);
|
|
1550
|
+
entry.timer = setTimeout(() => {
|
|
1551
|
+
if (this.entries.get(key) === entry)
|
|
1552
|
+
this.entries.delete(key);
|
|
1553
|
+
}, KEEP_EXITED_MS);
|
|
1554
|
+
}
|
|
1555
|
+
drop(key) {
|
|
1556
|
+
const entry = this.entries.get(key);
|
|
1557
|
+
if (entry?.timer)
|
|
1558
|
+
clearTimeout(entry.timer);
|
|
1559
|
+
this.entries.delete(key);
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
// ../core/dist/api-key-login.js
|
|
1566
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
1567
|
+
function importApiKey(cwBin, account, harness, apiKey) {
|
|
1568
|
+
return new Promise((resolve5) => {
|
|
1569
|
+
const redact = (text) => text.split(apiKey).join("***").trim();
|
|
1570
|
+
const child = spawn4(cwBin, ["account", "login", account, "--harness", harness, "--with-api-key", "-"], {
|
|
1571
|
+
env: envWithoutHarness(),
|
|
1572
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1573
|
+
});
|
|
1574
|
+
let output = "";
|
|
1575
|
+
const collect = (chunk) => {
|
|
1576
|
+
output += chunk.toString();
|
|
1577
|
+
};
|
|
1578
|
+
child.stdout.on("data", collect);
|
|
1579
|
+
child.stderr.on("data", collect);
|
|
1580
|
+
child.stdin.on("error", () => {
|
|
1581
|
+
});
|
|
1582
|
+
const timer = setTimeout(() => child.kill(), TIMEOUT_MS4);
|
|
1583
|
+
child.on("error", (err) => {
|
|
1584
|
+
clearTimeout(timer);
|
|
1585
|
+
resolve5({ ok: false, error: redact(err.message) });
|
|
1586
|
+
});
|
|
1587
|
+
child.on("close", (code) => {
|
|
1588
|
+
clearTimeout(timer);
|
|
1589
|
+
resolve5(code === 0 ? { ok: true } : { ok: false, error: redact(output) || `cw exited with ${String(code)}` });
|
|
1590
|
+
});
|
|
1591
|
+
child.stdin.end(`${apiKey}
|
|
1592
|
+
`);
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
var TIMEOUT_MS4;
|
|
1596
|
+
var init_api_key_login = __esm({
|
|
1597
|
+
"../core/dist/api-key-login.js"() {
|
|
1598
|
+
"use strict";
|
|
1599
|
+
init_cw_doctor();
|
|
1600
|
+
TIMEOUT_MS4 = 6e4;
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
// ../core/dist/editors.js
|
|
1605
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
1606
|
+
import { accessSync, constants, existsSync as existsSync4 } from "node:fs";
|
|
1607
|
+
import { delimiter, join as join6 } from "node:path";
|
|
1608
|
+
function detectEditors(probe) {
|
|
1609
|
+
const found = [];
|
|
1610
|
+
for (const def of EDITORS) {
|
|
1611
|
+
const cliPath = probe.onPath(def.cli);
|
|
1612
|
+
if (cliPath) {
|
|
1613
|
+
found.push({ id: def.id, label: def.label, bin: cliPath, args: [] });
|
|
1614
|
+
} else if (probe.platform === "darwin" && probe.appExists(def.app)) {
|
|
1615
|
+
found.push({ id: def.id, label: def.label, bin: "open", args: ["-a", def.app.replace(/\.app$/, "")] });
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return found;
|
|
1619
|
+
}
|
|
1620
|
+
function openInEditor(editor, dir) {
|
|
1621
|
+
return new Promise((resolve5, reject) => {
|
|
1622
|
+
const child = spawn5(editor.bin, [...editor.args, dir], { detached: true, stdio: "ignore" });
|
|
1623
|
+
child.once("error", reject);
|
|
1624
|
+
child.once("spawn", () => {
|
|
1625
|
+
child.unref();
|
|
1626
|
+
resolve5();
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
var EDITORS, systemProbe;
|
|
1631
|
+
var init_editors = __esm({
|
|
1632
|
+
"../core/dist/editors.js"() {
|
|
1633
|
+
"use strict";
|
|
1634
|
+
EDITORS = [
|
|
1635
|
+
{ id: "vscode", label: "VS Code", cli: "code", app: "Visual Studio Code.app" },
|
|
1636
|
+
{ id: "cursor", label: "Cursor", cli: "cursor", app: "Cursor.app" },
|
|
1637
|
+
{ id: "windsurf", label: "Windsurf", cli: "windsurf", app: "Windsurf.app" },
|
|
1638
|
+
{ id: "zed", label: "Zed", cli: "zed", app: "Zed.app" }
|
|
1639
|
+
];
|
|
1640
|
+
systemProbe = {
|
|
1641
|
+
onPath: (cli) => {
|
|
1642
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
1643
|
+
if (!dir)
|
|
1644
|
+
continue;
|
|
1645
|
+
const candidate = join6(dir, cli);
|
|
1646
|
+
try {
|
|
1647
|
+
accessSync(candidate, constants.X_OK);
|
|
1648
|
+
return candidate;
|
|
1649
|
+
} catch {
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return null;
|
|
1653
|
+
},
|
|
1654
|
+
appExists: (app) => existsSync4(join6("/Applications", app)),
|
|
1655
|
+
platform: process.platform
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
// ../core/dist/output.js
|
|
1661
|
+
function tailOutput(text, max = 20) {
|
|
1662
|
+
return stripAnsi(text).split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).slice(-max).join("\n");
|
|
1663
|
+
}
|
|
1664
|
+
var ANSI_RE2, stripAnsi;
|
|
1665
|
+
var init_output = __esm({
|
|
1666
|
+
"../core/dist/output.js"() {
|
|
1667
|
+
"use strict";
|
|
1668
|
+
ANSI_RE2 = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
1669
|
+
stripAnsi = (text) => text.replace(ANSI_RE2, "");
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
// ../core/dist/cw-routes.js
|
|
1674
|
+
import { Hono } from "hono";
|
|
1675
|
+
import { execFileSync, execFile as execFile4 } from "node:child_process";
|
|
1676
|
+
import { promisify as promisify2 } from "node:util";
|
|
1677
|
+
import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync5, mkdirSync, rmSync, readdirSync as readdirSync3, statSync } from "node:fs";
|
|
1678
|
+
import { join as join7, resolve, dirname, basename, isAbsolute } from "node:path";
|
|
1679
|
+
import { homedir as homedir2 } from "node:os";
|
|
1680
|
+
function resolveCwBin(cwHome) {
|
|
1681
|
+
const candidate = join7(cwHome, "bin", "cw");
|
|
1682
|
+
return existsSync5(candidate) ? candidate : "cw";
|
|
1683
|
+
}
|
|
1684
|
+
function cwDoneTimeoutMessage(ms) {
|
|
1685
|
+
return `cw --done timed out after ${ms % 1e3 === 0 ? `${ms / 1e3} s` : `${ms} ms`}`;
|
|
1686
|
+
}
|
|
1687
|
+
async function runCw(bin, args, timeoutMs) {
|
|
1688
|
+
const pending = execFileAsync2(bin, args, { env: envWithoutHarness(), timeout: timeoutMs, maxBuffer: 5 * 1024 * 1024 });
|
|
1689
|
+
pending.child.stdin?.end();
|
|
1690
|
+
try {
|
|
1691
|
+
const { stdout, stderr } = await pending;
|
|
1692
|
+
return { code: 0, stdout: String(stdout), stderr: String(stderr), timedOut: false };
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
const e = err;
|
|
1695
|
+
if (e.killed && e.signal === "SIGTERM" && typeof e.code !== "string") {
|
|
1696
|
+
return { code: 124, stdout: e.stdout ?? "", stderr: e.stderr ?? "", timedOut: true };
|
|
1697
|
+
}
|
|
1698
|
+
return { code: typeof e.code === "number" ? e.code : 1, stdout: e.stdout ?? "", stderr: e.stderr || e.message, timedOut: false };
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function cwRoutes(reader, options = {}) {
|
|
1702
|
+
const app = new Hono();
|
|
1703
|
+
const cwBin = resolveCwBin(reader.cwHome);
|
|
1704
|
+
const logins = options.loginManager ?? new LoginManager(cwBin);
|
|
1705
|
+
const run = options.runner ?? runCommand;
|
|
1706
|
+
const localOnly = options.localOnly ?? true;
|
|
1707
|
+
const editors = options.editors ?? detectEditors(systemProbe);
|
|
1708
|
+
const cwDoneTimeoutMs = options.cwDoneTimeoutMs ?? CW_DONE_TIMEOUT_MS;
|
|
1709
|
+
const limitGh = createLimiter(4);
|
|
1710
|
+
const REVIEW_TTL_MS = 3e4;
|
|
1711
|
+
const reviewCache = /* @__PURE__ */ new Map();
|
|
1712
|
+
const knownAccount = (name) => ACCOUNT_NAME_RE.test(name) && reader.getAccounts().includes(name);
|
|
1713
|
+
const doctor = createDoctorClient(cwBin);
|
|
1714
|
+
app.get("/harnesses", async (c) => {
|
|
1715
|
+
const result2 = await doctor.get(c.req.query("fresh") === "1");
|
|
1716
|
+
if (!result2.available)
|
|
1717
|
+
return c.json(result2);
|
|
1718
|
+
const capabilities = Object.fromEntries(result2.doctor.harnesses.map((h) => [h.name, [...HARNESS_CAPABILITIES[h.name] ?? []]]));
|
|
1719
|
+
return c.json({ ...result2, capabilities, contextTokens: readContextTokens(reader.cwHome) });
|
|
1720
|
+
});
|
|
1721
|
+
const usage = createUsageClient(options.usage);
|
|
1722
|
+
app.get("/usage", async (c) => {
|
|
1723
|
+
const result2 = await doctor.get();
|
|
1724
|
+
if (!result2.available)
|
|
1725
|
+
return c.json(result2);
|
|
1726
|
+
return c.json({ available: true, usage: await usage.get(result2.doctor, c.req.query("fresh") === "1") });
|
|
1727
|
+
});
|
|
1728
|
+
app.get("/projects", (c) => {
|
|
1729
|
+
return c.json(reader.getProjects());
|
|
1730
|
+
});
|
|
1731
|
+
app.get("/spaces", (c) => {
|
|
1732
|
+
const project = c.req.query("project");
|
|
1733
|
+
return c.json(reader.getSpaces(project));
|
|
1734
|
+
});
|
|
1735
|
+
app.get("/session/:project/:sessionDir", (c) => {
|
|
1736
|
+
const { project, sessionDir } = c.req.param();
|
|
1737
|
+
const session = reader.getSession(project, sessionDir);
|
|
1738
|
+
if (!session)
|
|
1739
|
+
return c.json({ error: "Session not found" }, 404);
|
|
1740
|
+
return c.json(session);
|
|
1741
|
+
});
|
|
1742
|
+
app.get("/notes/:project/:sessionDir", (c) => {
|
|
1743
|
+
const { project, sessionDir } = c.req.param();
|
|
1744
|
+
const content = reader.getNotes(project, sessionDir);
|
|
1745
|
+
return c.json({ content });
|
|
1746
|
+
});
|
|
1747
|
+
app.get("/accounts", (c) => {
|
|
1748
|
+
return c.json(reader.getAccounts());
|
|
1749
|
+
});
|
|
1750
|
+
app.post("/accounts", async (c) => {
|
|
1751
|
+
const body = await c.req.json();
|
|
1752
|
+
const harness = body.harness || void 0;
|
|
1753
|
+
const provider = body.provider || void 0;
|
|
1754
|
+
const model = body.model || void 0;
|
|
1755
|
+
if (!body.name || !body.name.trim()) {
|
|
1756
|
+
return c.json({ ok: false, error: "Account name is required" }, 400);
|
|
1757
|
+
}
|
|
1758
|
+
const trimmed = body.name.trim();
|
|
1759
|
+
if (!ACCOUNT_NAME_RE.test(trimmed)) {
|
|
1760
|
+
return c.json({ ok: false, error: "Name must start with a letter or number and contain only letters, numbers, hyphens, and underscores (max 64 chars)" }, 400);
|
|
1761
|
+
}
|
|
1762
|
+
if (harness && !HARNESS_NAME_RE.test(harness)) {
|
|
1763
|
+
return c.json({ ok: false, error: "Invalid harness" }, 400);
|
|
1764
|
+
}
|
|
1765
|
+
if (provider && !PROVIDER_NAME_RE.test(provider)) {
|
|
1766
|
+
return c.json({ ok: false, error: "Invalid provider" }, 400);
|
|
1767
|
+
}
|
|
1768
|
+
if (provider && !supports(harness, "custom_provider")) {
|
|
1769
|
+
return c.json({ ok: false, error: `${harness ?? "claude"} cannot use a provider` }, 400);
|
|
1770
|
+
}
|
|
1771
|
+
if (model && !MODEL_NAME_RE.test(model)) {
|
|
1772
|
+
return c.json({ ok: false, error: "Invalid model" }, 400);
|
|
1773
|
+
}
|
|
1774
|
+
if (reader.getAccounts().includes(trimmed)) {
|
|
1775
|
+
return c.json({ ok: false, error: `Account "${trimmed}" already exists` }, 409);
|
|
1776
|
+
}
|
|
1777
|
+
const args = ["account", "add", trimmed];
|
|
1778
|
+
if (harness)
|
|
1779
|
+
args.push("--harness", harness);
|
|
1780
|
+
if (provider)
|
|
1781
|
+
args.push("--provider", provider);
|
|
1782
|
+
if (model)
|
|
1783
|
+
args.push("--model", model);
|
|
1784
|
+
try {
|
|
1785
|
+
await execFileAsync2(cwBin, args, { encoding: "utf-8", timeout: 1e4, env: envWithoutHarness() });
|
|
1786
|
+
return c.json({ ok: true, name: trimmed });
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
const stderr = err.stderr?.trim().split("\n")[0];
|
|
1789
|
+
const message = stderr || (err instanceof Error ? err.message : "Unknown error");
|
|
1790
|
+
return c.json({ ok: false, error: `Failed to create account: ${message}` }, 500);
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
app.delete("/accounts/:name", async (c) => {
|
|
1794
|
+
const name = c.req.param("name");
|
|
1795
|
+
if (!ACCOUNT_NAME_RE.test(name)) {
|
|
1796
|
+
return c.json({ ok: false, error: "Invalid account name" }, 400);
|
|
1797
|
+
}
|
|
1798
|
+
const accountDir = join7(reader.cwHome, "accounts", name);
|
|
1799
|
+
if (!existsSync5(accountDir)) {
|
|
1800
|
+
return c.json({ ok: false, error: `Account "${name}" not found` }, 404);
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
const { rmSync: rmSync3 } = await import("node:fs");
|
|
1804
|
+
rmSync3(accountDir, { recursive: true, force: true });
|
|
1805
|
+
return c.json({ ok: true });
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1808
|
+
return c.json({ ok: false, error: `Failed to remove account: ${message}` }, 500);
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
app.post("/accounts/:name/login", async (c) => {
|
|
1812
|
+
const name = c.req.param("name");
|
|
1813
|
+
const { harness } = await c.req.json();
|
|
1814
|
+
if (!knownAccount(name))
|
|
1815
|
+
return c.json({ ok: false, error: "Unknown account" }, 404);
|
|
1816
|
+
if (!harness || !HARNESS_NAME_RE.test(harness))
|
|
1817
|
+
return c.json({ ok: false, error: "Invalid harness" }, 400);
|
|
1818
|
+
if (!supports(harness, "headless_login")) {
|
|
1819
|
+
return c.json({ ok: false, error: `${harness} has no headless login` }, 400);
|
|
1820
|
+
}
|
|
1821
|
+
return c.json({ ok: true, login: logins.start(name, harness) });
|
|
1822
|
+
});
|
|
1823
|
+
app.get("/accounts/:name/login/:harness", (c) => {
|
|
1824
|
+
const login = logins.get(c.req.param("name"), c.req.param("harness"));
|
|
1825
|
+
return login ? c.json({ ok: true, login }) : c.json({ ok: false, error: "No login in progress" }, 404);
|
|
1826
|
+
});
|
|
1827
|
+
app.delete("/accounts/:name/login/:harness", (c) => {
|
|
1828
|
+
logins.stop(c.req.param("name"), c.req.param("harness"));
|
|
1829
|
+
return c.json({ ok: true });
|
|
1830
|
+
});
|
|
1831
|
+
app.post("/accounts/:name/api-key", async (c) => {
|
|
1832
|
+
const name = c.req.param("name");
|
|
1833
|
+
const { harness, apiKey } = await c.req.json();
|
|
1834
|
+
if (!knownAccount(name))
|
|
1835
|
+
return c.json({ ok: false, error: "Unknown account" }, 404);
|
|
1836
|
+
if (!harness || !HARNESS_NAME_RE.test(harness))
|
|
1837
|
+
return c.json({ ok: false, error: "Invalid harness" }, 400);
|
|
1838
|
+
if (!supports(harness, "api_key_login")) {
|
|
1839
|
+
return c.json({ ok: false, error: `${harness} has no API key login` }, 400);
|
|
1840
|
+
}
|
|
1841
|
+
if (!apiKey || /[\r\n]/.test(apiKey) || apiKey.length < 8 || apiKey.length > 4096) {
|
|
1842
|
+
return c.json({ ok: false, error: "Invalid API key" }, 400);
|
|
1843
|
+
}
|
|
1844
|
+
const result2 = await importApiKey(cwBin, name, harness, apiKey);
|
|
1845
|
+
return result2.ok ? c.json({ ok: true }) : c.json({ ok: false, error: result2.error }, 500);
|
|
1846
|
+
});
|
|
1847
|
+
app.get("/detect/:project", (c) => {
|
|
1848
|
+
const project = c.req.param("project");
|
|
1849
|
+
return c.json(reader.detectStack(project));
|
|
1850
|
+
});
|
|
1851
|
+
app.get("/mcps", (c) => {
|
|
1852
|
+
const project = c.req.query("project");
|
|
1853
|
+
return c.json(reader.getMCPs(project));
|
|
1854
|
+
});
|
|
1855
|
+
app.get("/tools", (c) => {
|
|
1856
|
+
const project = c.req.query("project");
|
|
1857
|
+
return c.json(reader.getTools(project));
|
|
1858
|
+
});
|
|
1859
|
+
const loadGitSession = (project, sessionDir) => {
|
|
1860
|
+
const session = reader.getSession(project, sessionDir);
|
|
1861
|
+
if (!session)
|
|
1862
|
+
return { error: true };
|
|
1863
|
+
if (!session.worktree)
|
|
1864
|
+
return { empty: true };
|
|
1865
|
+
return { session };
|
|
1866
|
+
};
|
|
1867
|
+
app.get("/git/status/:project/:sessionDir", async (c) => {
|
|
1868
|
+
const result2 = loadGitSession(c.req.param("project"), c.req.param("sessionDir"));
|
|
1869
|
+
if ("error" in result2)
|
|
1870
|
+
return c.json({ error: "Session not found" }, 404);
|
|
1871
|
+
if ("empty" in result2)
|
|
1872
|
+
return c.json({ output: "" });
|
|
1873
|
+
const status = await run("git", ["status", "--short"], result2.session.worktree);
|
|
1874
|
+
return c.json({ output: status.code === 0 ? status.stdout : "" });
|
|
1875
|
+
});
|
|
1876
|
+
app.get("/git/log/:project/:sessionDir", async (c) => {
|
|
1877
|
+
const result2 = loadGitSession(c.req.param("project"), c.req.param("sessionDir"));
|
|
1878
|
+
if ("error" in result2)
|
|
1879
|
+
return c.json({ error: "Session not found" }, 404);
|
|
1880
|
+
if ("empty" in result2)
|
|
1881
|
+
return c.json({ output: "" });
|
|
1882
|
+
const log = await run("git", ["log", "--oneline", "-20"], result2.session.worktree);
|
|
1883
|
+
return c.json({ output: log.code === 0 ? log.stdout : "" });
|
|
1884
|
+
});
|
|
1885
|
+
app.get("/git/branch/:project/:sessionDir", async (c) => {
|
|
1886
|
+
const result2 = loadGitSession(c.req.param("project"), c.req.param("sessionDir"));
|
|
1887
|
+
if ("error" in result2)
|
|
1888
|
+
return c.json({ error: "Session not found" }, 404);
|
|
1889
|
+
if ("empty" in result2)
|
|
1890
|
+
return c.json({ branch: "" });
|
|
1891
|
+
const head = await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], result2.session.worktree);
|
|
1892
|
+
return c.json({ branch: head.code === 0 ? head.stdout.trim() : "" });
|
|
1893
|
+
});
|
|
1894
|
+
app.get("/git/diff/:project/:sessionDir", async (c) => {
|
|
1895
|
+
const result2 = loadGitSession(c.req.param("project"), c.req.param("sessionDir"));
|
|
1896
|
+
if ("error" in result2)
|
|
1897
|
+
return c.json({ error: "Session not found" }, 404);
|
|
1898
|
+
if ("empty" in result2)
|
|
1899
|
+
return c.json({ output: "" });
|
|
1900
|
+
const worktree = result2.session.worktree;
|
|
1901
|
+
const base = await resolveBase(run, worktree, [result2.session.base_branch, "origin/HEAD", "origin/main"]);
|
|
1902
|
+
if (!base)
|
|
1903
|
+
return c.json({ output: "" });
|
|
1904
|
+
const diff = await run("git", ["diff", "--stat", base], worktree);
|
|
1905
|
+
return c.json({ output: diff.code === 0 ? diff.stdout : "" });
|
|
1906
|
+
});
|
|
1907
|
+
app.get("/review-state/:project/:sessionDir", async (c) => {
|
|
1908
|
+
const project = c.req.param("project");
|
|
1909
|
+
const sessionDir = c.req.param("sessionDir");
|
|
1910
|
+
const session = reader.getSession(project, sessionDir);
|
|
1911
|
+
if (!session)
|
|
1912
|
+
return c.json({ error: "Session not found" }, 404);
|
|
1913
|
+
const key = `${project}::${sessionDir}`;
|
|
1914
|
+
const cached = reviewCache.get(key);
|
|
1915
|
+
if (c.req.query("fresh") !== "1" && cached && Date.now() - cached.at < REVIEW_TTL_MS) {
|
|
1916
|
+
return c.json(await cached.value);
|
|
1917
|
+
}
|
|
1918
|
+
const projectPath = reader.getProjects()[project]?.path ?? null;
|
|
1919
|
+
const value = buildTaskReviewState(session, projectPath, { run, limitGh, exists: existsSync5 });
|
|
1920
|
+
reviewCache.set(key, { at: Date.now(), value });
|
|
1921
|
+
value.catch(() => reviewCache.delete(key));
|
|
1922
|
+
return c.json(await value);
|
|
1923
|
+
});
|
|
1924
|
+
app.get("/editors", (c) => c.json({
|
|
1925
|
+
enabled: localOnly,
|
|
1926
|
+
editors: localOnly ? editors.map(({ id, label }) => ({ id, label })) : []
|
|
1927
|
+
}));
|
|
1928
|
+
app.post("/open-in-editor", async (c) => {
|
|
1929
|
+
if (!localOnly) {
|
|
1930
|
+
return c.json({ ok: false, error: "Opening an editor only works on the machine running Forge" }, 403);
|
|
1931
|
+
}
|
|
1932
|
+
const { project, sessionDir, editor } = await c.req.json();
|
|
1933
|
+
const target = editors.find((e) => e.id === editor);
|
|
1934
|
+
if (!target)
|
|
1935
|
+
return c.json({ ok: false, error: `Editor not available: ${editor}` }, 400);
|
|
1936
|
+
const session = reader.getSession(project, sessionDir);
|
|
1937
|
+
if (!session?.worktree || !existsSync5(session.worktree)) {
|
|
1938
|
+
return c.json({ ok: false, error: "This task has no workspace on disk yet" }, 404);
|
|
1939
|
+
}
|
|
1940
|
+
try {
|
|
1941
|
+
await openInEditor(target, session.worktree);
|
|
1942
|
+
return c.json({ ok: true });
|
|
1943
|
+
} catch (err) {
|
|
1944
|
+
return c.json({ ok: false, error: `Could not start ${target.label}: ${err.message}` }, 500);
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
app.post("/start", async (c) => {
|
|
1948
|
+
const { type, project, task, description, workflow, account, directory, skipPermissions, model, loopPrompt, loopInterval, name, harness } = await c.req.json();
|
|
1949
|
+
if (harness !== void 0 && !HARNESS_NAME_RE.test(harness)) {
|
|
1950
|
+
return c.json({ ok: false, error: "Invalid harness" }, 400);
|
|
1951
|
+
}
|
|
1952
|
+
if (type === "login") {
|
|
1953
|
+
if (!account || !ACCOUNT_NAME_RE.test(account) || !reader.getAccounts().includes(account)) {
|
|
1954
|
+
return c.json({ ok: false, error: "Unknown account" }, 400);
|
|
1955
|
+
}
|
|
1956
|
+
if (!harness)
|
|
1957
|
+
return c.json({ ok: false, error: "Harness is required" }, 400);
|
|
1958
|
+
const sessionDirName2 = `login-${account}-${harness}`;
|
|
1959
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1960
|
+
const sessionData2 = {
|
|
1961
|
+
project: ACCOUNTS_PROJECT,
|
|
1962
|
+
type: "login",
|
|
1963
|
+
account,
|
|
1964
|
+
harness,
|
|
1965
|
+
workflow: "",
|
|
1966
|
+
worktree: "",
|
|
1967
|
+
notes: "",
|
|
1968
|
+
status: "active",
|
|
1969
|
+
created: now,
|
|
1970
|
+
last_opened: now,
|
|
1971
|
+
opens: 0,
|
|
1972
|
+
sessionDir: sessionDirName2
|
|
1973
|
+
};
|
|
1974
|
+
pendingSessions.set(`${ACCOUNTS_PROJECT}::${sessionDirName2}`, sessionData2);
|
|
1975
|
+
return c.json({ ok: true, session: sessionData2 });
|
|
1976
|
+
}
|
|
1977
|
+
if (type === "general") {
|
|
1978
|
+
const acct = account || "default";
|
|
1979
|
+
const sessionDirName2 = `general-${acct}-${Date.now()}`;
|
|
1980
|
+
const projectPath = project ? reader.getProjects()[project]?.path : void 0;
|
|
1981
|
+
const projectName = project && projectPath ? project : GENERAL_PROJECT;
|
|
1982
|
+
const sessionData2 = {
|
|
1983
|
+
project: projectName,
|
|
1984
|
+
task: task?.trim() || void 0,
|
|
1985
|
+
type: "general",
|
|
1986
|
+
account: acct,
|
|
1987
|
+
harness: harness || void 0,
|
|
1988
|
+
model: model || void 0,
|
|
1989
|
+
workflow: "",
|
|
1990
|
+
worktree: projectPath ?? directory ?? "",
|
|
1991
|
+
notes: "",
|
|
1992
|
+
status: "active",
|
|
1993
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1994
|
+
last_opened: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1995
|
+
opens: 0,
|
|
1996
|
+
sessionDir: sessionDirName2,
|
|
1997
|
+
skipPermissions: skipPermissions ?? false
|
|
1998
|
+
};
|
|
1999
|
+
pendingSessions.set(`${projectName}::${sessionDirName2}`, sessionData2);
|
|
2000
|
+
return c.json({ ok: true, session: sessionData2, command: `cw launch ${acct}` });
|
|
2001
|
+
}
|
|
2002
|
+
if (type === "create") {
|
|
2003
|
+
const name2 = project?.trim();
|
|
2004
|
+
if (!name2)
|
|
2005
|
+
return c.json({ ok: false, error: "Project name is required" }, 400);
|
|
2006
|
+
const sessionDirName2 = `create-${name2}-${Date.now()}`;
|
|
2007
|
+
const sessionData2 = {
|
|
2008
|
+
project: CREATING_PROJECT,
|
|
2009
|
+
task: name2,
|
|
2010
|
+
type: "create",
|
|
2011
|
+
account: account ?? "",
|
|
2012
|
+
harness: harness || void 0,
|
|
2013
|
+
model: model || void 0,
|
|
2014
|
+
workflow: "",
|
|
2015
|
+
worktree: directory ?? "",
|
|
2016
|
+
notes: description ?? name2,
|
|
2017
|
+
status: "active",
|
|
2018
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2019
|
+
last_opened: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2020
|
+
opens: 0,
|
|
2021
|
+
sessionDir: sessionDirName2,
|
|
2022
|
+
skipPermissions: skipPermissions ?? false
|
|
2023
|
+
};
|
|
2024
|
+
pendingSessions.set(`${CREATING_PROJECT}::${sessionDirName2}`, sessionData2);
|
|
2025
|
+
return c.json({ ok: true, session: sessionData2 });
|
|
2026
|
+
}
|
|
2027
|
+
if (type === "loop") {
|
|
2028
|
+
if (!project)
|
|
2029
|
+
return c.json({ ok: false, error: "Project is required" }, 400);
|
|
2030
|
+
if (harness && harness !== "claude") {
|
|
2031
|
+
return c.json({ ok: false, error: "Loop runs on Claude Code only" }, 400);
|
|
2032
|
+
}
|
|
2033
|
+
const prompt = (loopPrompt ?? "").trim();
|
|
2034
|
+
if (!prompt)
|
|
2035
|
+
return c.json({ ok: false, error: "Loop prompt is required" }, 400);
|
|
2036
|
+
const interval = (loopInterval ?? "").trim();
|
|
2037
|
+
if (interval && !/^\d+[smh]$/.test(interval)) {
|
|
2038
|
+
return c.json({ ok: false, error: "Interval must be like 30s, 5m, 2h" }, 400);
|
|
2039
|
+
}
|
|
2040
|
+
const explicitName = name?.trim();
|
|
2041
|
+
if (explicitName && !/^[a-z0-9]([a-z0-9-]{0,28}[a-z0-9])?$/.test(explicitName)) {
|
|
2042
|
+
return c.json({ ok: false, error: "Invalid name. Use lowercase letters, numbers, hyphens (max 30 chars)" }, 400);
|
|
2043
|
+
}
|
|
2044
|
+
const slug = (explicitName || prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 30)).replace(/^-+|-+$/g, "");
|
|
2045
|
+
if (!slug)
|
|
2046
|
+
return c.json({ ok: false, error: "Could not derive a name from the prompt" }, 400);
|
|
2047
|
+
const sessionDirName2 = `loop-${slug}`;
|
|
2048
|
+
const loopSessionFile = join7(reader.cwHome, "sessions", project, sessionDirName2, "session.json");
|
|
2049
|
+
if (existsSync5(loopSessionFile)) {
|
|
2050
|
+
try {
|
|
2051
|
+
const meta = JSON.parse(readFileSync5(loopSessionFile, "utf-8"));
|
|
2052
|
+
if (meta.status === "active") {
|
|
2053
|
+
return c.json({ ok: false, error: `Loop "${slug}" already exists for ${project}. Pick a different name or open the existing session.` }, 409);
|
|
2054
|
+
}
|
|
2055
|
+
} catch {
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
const projectPath = reader.getProjects()[project]?.path ?? "";
|
|
2059
|
+
const sessionData2 = {
|
|
2060
|
+
project,
|
|
2061
|
+
task: slug,
|
|
2062
|
+
type: "loop",
|
|
2063
|
+
account: account ?? "",
|
|
2064
|
+
harness: harness || void 0,
|
|
2065
|
+
model: model || void 0,
|
|
2066
|
+
loop_prompt: prompt,
|
|
2067
|
+
loop_interval: interval,
|
|
2068
|
+
workflow: "",
|
|
2069
|
+
// This in-memory pending session may carry the project path (used as
|
|
2070
|
+
// the PTY's cwd), but it must never be persisted to CW's session.json —
|
|
2071
|
+
// `cw clean` treats a persisted worktree as a stale-space candidate to
|
|
2072
|
+
// rm -rf. CW itself writes 'worktree': '' when it creates the file.
|
|
2073
|
+
worktree: projectPath,
|
|
2074
|
+
notes: "",
|
|
2075
|
+
status: "active",
|
|
2076
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2077
|
+
last_opened: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2078
|
+
opens: 0,
|
|
2079
|
+
sessionDir: sessionDirName2,
|
|
2080
|
+
skipPermissions: skipPermissions ?? false
|
|
2081
|
+
};
|
|
2082
|
+
pendingSessions.set(`${project}::${sessionDirName2}`, sessionData2);
|
|
2083
|
+
return c.json({ ok: true, session: sessionData2 });
|
|
2084
|
+
}
|
|
2085
|
+
if (!project || !task) {
|
|
2086
|
+
return c.json({ ok: false, error: "Project and task are required" }, 400);
|
|
2087
|
+
}
|
|
2088
|
+
const cwHome = reader.cwHome;
|
|
2089
|
+
let taskSlug = task.trim();
|
|
2090
|
+
let taskUrl;
|
|
2091
|
+
let taskSource;
|
|
2092
|
+
if (taskSlug.startsWith("http")) {
|
|
2093
|
+
taskUrl = taskSlug;
|
|
2094
|
+
if (taskSlug.includes("github.com")) {
|
|
2095
|
+
taskSource = "github";
|
|
2096
|
+
const m = taskSlug.match(/(\d+)\s*$/);
|
|
2097
|
+
if (m)
|
|
2098
|
+
taskSlug = m[1];
|
|
2099
|
+
} else if (taskSlug.includes("linear.app")) {
|
|
2100
|
+
taskSource = "linear";
|
|
2101
|
+
const m = taskSlug.match(/([A-Z]+-\d+)/);
|
|
2102
|
+
if (m)
|
|
2103
|
+
taskSlug = m[1];
|
|
2104
|
+
} else if (taskSlug.includes("notion.so") || taskSlug.includes("notion.site")) {
|
|
2105
|
+
taskSource = "notion";
|
|
2106
|
+
const parts = taskSlug.split("/");
|
|
2107
|
+
const last = parts[parts.length - 1] ?? "";
|
|
2108
|
+
taskSlug = last.replace(/-[a-f0-9]+$/, "").slice(0, 30);
|
|
2109
|
+
} else {
|
|
2110
|
+
taskSource = "url";
|
|
2111
|
+
taskSlug = taskSlug.replace(/^https?:\/\//, "").replace(/\//g, "-").slice(0, 30);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
const dirPrefix = type === "review" ? "review-pr-" : "task-";
|
|
2115
|
+
const sessionDir = join7(cwHome, "sessions", project, `${dirPrefix}${taskSlug}`);
|
|
2116
|
+
const sessionFile = join7(sessionDir, "session.json");
|
|
2117
|
+
if (existsSync5(sessionFile)) {
|
|
2118
|
+
try {
|
|
2119
|
+
const meta = JSON.parse(readFileSync5(sessionFile, "utf-8"));
|
|
2120
|
+
if (meta.status === "active") {
|
|
2121
|
+
return c.json({ ok: false, error: `Session "${taskSlug}" already exists for ${project}. Pick a different name or open the existing task.` }, 409);
|
|
2122
|
+
}
|
|
2123
|
+
} catch {
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
const args = [];
|
|
2127
|
+
if (skipPermissions)
|
|
2128
|
+
args.push("--skip-permissions");
|
|
2129
|
+
if (type === "review") {
|
|
2130
|
+
args.push("review", project, task);
|
|
2131
|
+
if (account)
|
|
2132
|
+
args.push("--account", account);
|
|
2133
|
+
if (model)
|
|
2134
|
+
args.push("--model", model);
|
|
2135
|
+
} else {
|
|
2136
|
+
args.push("work", project, task);
|
|
2137
|
+
if (account)
|
|
2138
|
+
args.push("--account", account);
|
|
2139
|
+
if (workflow)
|
|
2140
|
+
args.push("--workflow", workflow);
|
|
2141
|
+
if (model)
|
|
2142
|
+
args.push("--model", model);
|
|
2143
|
+
}
|
|
2144
|
+
if (harness)
|
|
2145
|
+
args.push("--harness", harness);
|
|
2146
|
+
if (description) {
|
|
2147
|
+
const notesDir = join7(cwHome, "sessions", project, `${dirPrefix}${taskSlug}`);
|
|
2148
|
+
mkdirSync(notesDir, { recursive: true });
|
|
2149
|
+
const notesFile = join7(notesDir, type === "review" ? "REVIEW_NOTES.md" : "TASK_NOTES.md");
|
|
2150
|
+
if (!existsSync5(notesFile)) {
|
|
2151
|
+
const header = type === "review" ? "Review" : "Task";
|
|
2152
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2153
|
+
writeFileSync(notesFile, [
|
|
2154
|
+
`# ${header}: ${taskSlug}`,
|
|
2155
|
+
`**Project:** ${project}`,
|
|
2156
|
+
`**Created:** ${now}`,
|
|
2157
|
+
"",
|
|
2158
|
+
"## Description",
|
|
2159
|
+
description,
|
|
2160
|
+
"",
|
|
2161
|
+
"## Context",
|
|
2162
|
+
"<!-- Additional context discovered during work -->",
|
|
2163
|
+
"",
|
|
2164
|
+
"## Decisions",
|
|
2165
|
+
"<!-- Important decisions made during this task -->",
|
|
2166
|
+
"",
|
|
2167
|
+
"## Status",
|
|
2168
|
+
"- [ ] Pending",
|
|
2169
|
+
"",
|
|
2170
|
+
"## Notes",
|
|
2171
|
+
"<!-- Findings, context, references -->",
|
|
2172
|
+
""
|
|
2173
|
+
].join("\n"));
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
const sessionDirName = `${dirPrefix}${taskSlug}`;
|
|
2177
|
+
const sessionData = {
|
|
2178
|
+
project,
|
|
2179
|
+
task: type === "review" ? void 0 : taskSlug,
|
|
2180
|
+
pr: type === "review" ? taskSlug : void 0,
|
|
2181
|
+
type: type === "review" ? "review" : "task",
|
|
2182
|
+
account: account ?? "",
|
|
2183
|
+
harness: harness || void 0,
|
|
2184
|
+
workflow: workflow ?? "",
|
|
2185
|
+
model: model || void 0,
|
|
2186
|
+
source: taskSource,
|
|
2187
|
+
source_url: taskUrl,
|
|
2188
|
+
worktree: "",
|
|
2189
|
+
notes: "",
|
|
2190
|
+
status: "active",
|
|
2191
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2192
|
+
last_opened: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2193
|
+
opens: 0,
|
|
2194
|
+
sessionDir: sessionDirName,
|
|
2195
|
+
skipPermissions: skipPermissions ?? false
|
|
2196
|
+
};
|
|
2197
|
+
pendingSessions.set(`${project}::${sessionDirName}`, sessionData);
|
|
2198
|
+
return c.json({ ok: true, session: sessionData, command: `cw ${args.join(" ")}` });
|
|
2199
|
+
});
|
|
2200
|
+
app.post("/done", async (c) => {
|
|
2201
|
+
const { project, task, type, sessionDir } = await c.req.json();
|
|
2202
|
+
const sessionDirName = sessionDir ?? (type === "review" ? `review-pr-${task}` : type === "loop" ? `loop-${task}` : `task-${task}`);
|
|
2203
|
+
const args = type === "review" ? ["review", project, task, "--done"] : type === "loop" ? ["loop", project, task, "--done"] : ["work", project, task, "--done"];
|
|
2204
|
+
reviewCache.delete(`${project}::${sessionDirName}`);
|
|
2205
|
+
const result2 = await runCw(cwBin, args, cwDoneTimeoutMs);
|
|
2206
|
+
if (result2.code === 0)
|
|
2207
|
+
return c.json({ ok: true });
|
|
2208
|
+
const output = tailOutput(`${result2.stdout}
|
|
2209
|
+
${result2.stderr}`);
|
|
2210
|
+
const error = result2.timedOut ? [cwDoneTimeoutMessage(cwDoneTimeoutMs), output].filter(Boolean).join("\n") : output || "cw --done failed";
|
|
2211
|
+
return c.json({ ok: false, error }, 500);
|
|
2212
|
+
});
|
|
2213
|
+
app.post("/move-project", async (c) => {
|
|
2214
|
+
const { project, toAccount } = await c.req.json();
|
|
2215
|
+
if (!project || !toAccount) {
|
|
2216
|
+
return c.json({ ok: false, error: "Project and target account are required" }, 400);
|
|
2217
|
+
}
|
|
2218
|
+
if (!ACCOUNT_NAME_RE.test(toAccount)) {
|
|
2219
|
+
return c.json({ ok: false, error: "Invalid account name" }, 400);
|
|
2220
|
+
}
|
|
2221
|
+
const projects = reader.getProjects();
|
|
2222
|
+
if (!projects[project]) {
|
|
2223
|
+
return c.json({ ok: false, error: `Project "${project}" not found` }, 404);
|
|
2224
|
+
}
|
|
2225
|
+
if (projects[project].account === toAccount) {
|
|
2226
|
+
return c.json({ ok: false, error: `Project "${project}" is already in account "${toAccount}"` }, 409);
|
|
2227
|
+
}
|
|
2228
|
+
if (!reader.getAccounts().includes(toAccount)) {
|
|
2229
|
+
return c.json({ ok: false, error: `Account "${toAccount}" does not exist` }, 404);
|
|
2230
|
+
}
|
|
2231
|
+
projects[project] = { ...projects[project], account: toAccount };
|
|
2232
|
+
try {
|
|
2233
|
+
writeFileSync(join7(reader.cwHome, "projects.json"), JSON.stringify(projects, null, 2));
|
|
2234
|
+
return c.json({ ok: true });
|
|
2235
|
+
} catch (err) {
|
|
2236
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2237
|
+
return c.json({ ok: false, error: `Failed to update projects: ${message}` }, 500);
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
app.post("/delete-project", async (c) => {
|
|
2241
|
+
const { project, deleteFiles } = await c.req.json();
|
|
2242
|
+
const projects = reader.getProjects();
|
|
2243
|
+
const projPath = projects[project]?.path;
|
|
2244
|
+
try {
|
|
2245
|
+
execFileSync(cwBin, ["project", "remove", project, "--yes"], { encoding: "utf-8", timeout: 1e4, stdio: "pipe", env: envWithoutHarness() });
|
|
2246
|
+
} catch {
|
|
2247
|
+
}
|
|
2248
|
+
const sessionsDir = join7(reader.cwHome, "sessions", project);
|
|
2249
|
+
if (existsSync5(sessionsDir)) {
|
|
2250
|
+
try {
|
|
2251
|
+
rmSync(sessionsDir, { recursive: true, force: true });
|
|
2252
|
+
} catch {
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
if (deleteFiles && projPath) {
|
|
2256
|
+
try {
|
|
2257
|
+
rmSync(projPath, { recursive: true, force: true });
|
|
2258
|
+
} catch {
|
|
2259
|
+
return c.json({ ok: true, filesDeleted: false, reason: "Failed to delete directory" });
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
return c.json({ ok: true, filesDeleted: deleteFiles && !!projPath });
|
|
2263
|
+
});
|
|
2264
|
+
const resolveBrowsePath = (input) => {
|
|
2265
|
+
const home = homedir2();
|
|
2266
|
+
let p = input.trim();
|
|
2267
|
+
if (!p)
|
|
2268
|
+
p = home;
|
|
2269
|
+
if (p === "~" || p.startsWith("~/"))
|
|
2270
|
+
p = join7(home, p.slice(1).replace(/^\//, ""));
|
|
2271
|
+
if (!isAbsolute(p))
|
|
2272
|
+
return null;
|
|
2273
|
+
return resolve(p);
|
|
2274
|
+
};
|
|
2275
|
+
app.get("/browse-dirs", (c) => {
|
|
2276
|
+
const requested = c.req.query("path") ?? "~";
|
|
2277
|
+
const target = resolveBrowsePath(requested);
|
|
2278
|
+
if (!target) {
|
|
2279
|
+
return c.json({ ok: false, error: "Path must be absolute or start with ~" }, 400);
|
|
2280
|
+
}
|
|
2281
|
+
if (!existsSync5(target)) {
|
|
2282
|
+
return c.json({ ok: false, error: `Path does not exist: ${target}` }, 404);
|
|
2283
|
+
}
|
|
2284
|
+
let stat;
|
|
2285
|
+
try {
|
|
2286
|
+
stat = statSync(target);
|
|
2287
|
+
} catch {
|
|
2288
|
+
return c.json({ ok: false, error: "Cannot stat path" }, 500);
|
|
2289
|
+
}
|
|
2290
|
+
if (!stat.isDirectory()) {
|
|
2291
|
+
return c.json({ ok: false, error: "Not a directory" }, 400);
|
|
2292
|
+
}
|
|
2293
|
+
let entries = [];
|
|
2294
|
+
try {
|
|
2295
|
+
entries = readdirSync3(target, { withFileTypes: true });
|
|
2296
|
+
} catch (err) {
|
|
2297
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2298
|
+
return c.json({ ok: false, error: `Cannot read directory: ${message}` }, 500);
|
|
2299
|
+
}
|
|
2300
|
+
const dirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map(({ name }) => {
|
|
2301
|
+
const full = join7(target, name);
|
|
2302
|
+
return { name, path: full, isGitRepo: existsSync5(join7(full, ".git")) };
|
|
2303
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
2304
|
+
const parent = dirname(target);
|
|
2305
|
+
return c.json({
|
|
2306
|
+
ok: true,
|
|
2307
|
+
path: target,
|
|
2308
|
+
parent: parent === target ? null : parent,
|
|
2309
|
+
home: homedir2(),
|
|
2310
|
+
isGitRepo: existsSync5(join7(target, ".git")),
|
|
2311
|
+
entries: dirs
|
|
2312
|
+
});
|
|
2313
|
+
});
|
|
2314
|
+
app.post("/register-project", async (c) => {
|
|
2315
|
+
const { path: rawPath, account, alias, type } = await c.req.json();
|
|
2316
|
+
if (!rawPath || !account) {
|
|
2317
|
+
return c.json({ ok: false, error: "Path and account are required" }, 400);
|
|
2318
|
+
}
|
|
2319
|
+
if (!ACCOUNT_NAME_RE.test(account)) {
|
|
2320
|
+
return c.json({ ok: false, error: "Invalid account name" }, 400);
|
|
2321
|
+
}
|
|
2322
|
+
const projectPath = resolveBrowsePath(rawPath);
|
|
2323
|
+
if (!projectPath) {
|
|
2324
|
+
return c.json({ ok: false, error: "Path must be absolute or start with ~" }, 400);
|
|
2325
|
+
}
|
|
2326
|
+
if (!existsSync5(projectPath)) {
|
|
2327
|
+
return c.json({ ok: false, error: `Path does not exist: ${projectPath}` }, 404);
|
|
2328
|
+
}
|
|
2329
|
+
if (!existsSync5(join7(projectPath, ".git"))) {
|
|
2330
|
+
return c.json({ ok: false, error: "Selected directory is not a git repository" }, 400);
|
|
2331
|
+
}
|
|
2332
|
+
if (!reader.getAccounts().includes(account)) {
|
|
2333
|
+
return c.json({ ok: false, error: `Account "${account}" does not exist` }, 404);
|
|
2334
|
+
}
|
|
2335
|
+
const projectName = (alias?.trim() || basename(projectPath)).trim();
|
|
2336
|
+
if (reader.getProjects()[projectName]) {
|
|
2337
|
+
return c.json({ ok: false, error: `Project "${projectName}" is already registered` }, 409);
|
|
2338
|
+
}
|
|
2339
|
+
const args = ["project", "register", projectPath, "--account", account];
|
|
2340
|
+
if (alias?.trim())
|
|
2341
|
+
args.push("--alias", alias.trim());
|
|
2342
|
+
if (type?.trim())
|
|
2343
|
+
args.push("--type", type.trim());
|
|
2344
|
+
try {
|
|
2345
|
+
await execFileAsync2(cwBin, args, { encoding: "utf-8", timeout: 3e4, env: envWithoutHarness() });
|
|
2346
|
+
return c.json({ ok: true, project: projectName });
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2349
|
+
return c.json({ ok: false, error: `Failed to register project: ${message}` }, 500);
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2352
|
+
return app;
|
|
2353
|
+
}
|
|
2354
|
+
var execFileAsync2, GENERAL_PROJECT, CREATING_PROJECT, ACCOUNTS_PROJECT, pendingSessions, CW_DONE_TIMEOUT_MS;
|
|
2355
|
+
var init_cw_routes = __esm({
|
|
2356
|
+
"../core/dist/cw-routes.js"() {
|
|
2357
|
+
"use strict";
|
|
2358
|
+
init_cw_types();
|
|
2359
|
+
init_cw_doctor();
|
|
2360
|
+
init_usage();
|
|
2361
|
+
init_harness_capabilities();
|
|
2362
|
+
init_login_manager();
|
|
2363
|
+
init_api_key_login();
|
|
2364
|
+
init_task_review();
|
|
2365
|
+
init_editors();
|
|
2366
|
+
init_output();
|
|
2367
|
+
execFileAsync2 = promisify2(execFile4);
|
|
2368
|
+
GENERAL_PROJECT = "__general";
|
|
2369
|
+
CREATING_PROJECT = "__creating";
|
|
2370
|
+
ACCOUNTS_PROJECT = "__accounts";
|
|
2371
|
+
pendingSessions = /* @__PURE__ */ new Map();
|
|
2372
|
+
CW_DONE_TIMEOUT_MS = 6e4;
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
|
|
2376
|
+
// ../core/dist/plugins.js
|
|
2377
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
2378
|
+
import { basename as basename2, join as join8, resolve as resolve2, sep } from "node:path";
|
|
2379
|
+
function readJson(path) {
|
|
2380
|
+
try {
|
|
2381
|
+
const value = JSON.parse(readFileSync6(path, "utf-8"));
|
|
2382
|
+
return isRecord(value) ? value : {};
|
|
2383
|
+
} catch {
|
|
2384
|
+
return {};
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
function normalizeRepo(value) {
|
|
2388
|
+
if (!value)
|
|
2389
|
+
return void 0;
|
|
2390
|
+
const m = value.trim().match(/^(?:git@github\.com:|(?:https?|ssh|git):\/\/(?:[^@/]+@)?github\.com\/)?([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/i);
|
|
2391
|
+
return m ? `${m[1]}/${m[2]}`.toLowerCase() : void 0;
|
|
2392
|
+
}
|
|
2393
|
+
function createRemoteLookup(run) {
|
|
2394
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2395
|
+
return (projectPath) => {
|
|
2396
|
+
let hit = cache.get(projectPath);
|
|
2397
|
+
if (!hit) {
|
|
2398
|
+
hit = run("git", ["remote", "get-url", "origin"], projectPath).then((r) => r.code === 0 && r.stdout.trim() || null).catch(() => null);
|
|
2399
|
+
cache.set(projectPath, hit);
|
|
2400
|
+
}
|
|
2401
|
+
return hit;
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
function scanDirs(root) {
|
|
2405
|
+
if (!existsSync6(root))
|
|
2406
|
+
return [];
|
|
2407
|
+
return readdirSync4(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join8(root, e.name));
|
|
2408
|
+
}
|
|
2409
|
+
function skillDirs(installPath, manifest) {
|
|
2410
|
+
const listed = manifest["skills"];
|
|
2411
|
+
if (Array.isArray(listed)) {
|
|
2412
|
+
return listed.filter((p) => typeof p === "string").map((p) => resolve2(installPath, p)).filter((p) => inside(installPath, p));
|
|
2413
|
+
}
|
|
2414
|
+
if (typeof listed === "string") {
|
|
2415
|
+
const root = resolve2(installPath, listed);
|
|
2416
|
+
return inside(installPath, root) ? scanDirs(root) : [];
|
|
2417
|
+
}
|
|
2418
|
+
return scanDirs(join8(installPath, "skills"));
|
|
2419
|
+
}
|
|
2420
|
+
function readSkill(reader, dir) {
|
|
2421
|
+
try {
|
|
2422
|
+
const { frontmatter } = reader.parseFrontmatter(readFileSync6(join8(dir, "SKILL.md"), "utf-8"));
|
|
2423
|
+
return { name: String(frontmatter["name"] ?? basename2(dir)), description: String(frontmatter["description"] ?? ""), path: dir };
|
|
2424
|
+
} catch {
|
|
2425
|
+
return null;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
function manifestRepo(manifest) {
|
|
2429
|
+
const repo = manifest["repository"];
|
|
2430
|
+
if (typeof repo === "string")
|
|
2431
|
+
return repo;
|
|
2432
|
+
if (isRecord(repo) && typeof repo["url"] === "string")
|
|
2433
|
+
return repo["url"];
|
|
2434
|
+
return void 0;
|
|
2435
|
+
}
|
|
2436
|
+
function marketplaceRepo(marketplaces, marketplace) {
|
|
2437
|
+
const entry = marketplaces[marketplace];
|
|
2438
|
+
const source = isRecord(entry) ? entry["source"] : void 0;
|
|
2439
|
+
if (!isRecord(source))
|
|
2440
|
+
return void 0;
|
|
2441
|
+
const value = source["repo"] ?? source["url"];
|
|
2442
|
+
return typeof value === "string" ? value : void 0;
|
|
2443
|
+
}
|
|
2444
|
+
function projectSkillNames(projectPath) {
|
|
2445
|
+
const dir = join8(projectPath, ".claude", "skills");
|
|
2446
|
+
if (!existsSync6(dir))
|
|
2447
|
+
return [];
|
|
2448
|
+
return readdirSync4(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync6(join8(dir, e.name, "SKILL.md"))).map((e) => e.name).sort();
|
|
2449
|
+
}
|
|
2450
|
+
async function listPlugins(reader, remoteOf) {
|
|
2451
|
+
const scopes = [
|
|
2452
|
+
{ scope: "global", scopeRef: "global" },
|
|
2453
|
+
...reader.getAccounts().map((a) => ({ scope: "account", scopeRef: a }))
|
|
2454
|
+
];
|
|
2455
|
+
const found = /* @__PURE__ */ new Map();
|
|
2456
|
+
for (const { scope, scopeRef } of scopes) {
|
|
2457
|
+
const configDir = reader.getSkillConfigDir(scope, scopeRef);
|
|
2458
|
+
const installed = readJson(join8(configDir, "plugins", "installed_plugins.json"))["plugins"];
|
|
2459
|
+
if (!isRecord(installed))
|
|
2460
|
+
continue;
|
|
2461
|
+
const marketplaces = readJson(join8(configDir, "plugins", "known_marketplaces.json"));
|
|
2462
|
+
const enabledPlugins = readJson(join8(configDir, "settings.json"))["enabledPlugins"];
|
|
2463
|
+
for (const [id, entries] of Object.entries(installed)) {
|
|
2464
|
+
if (!Array.isArray(entries))
|
|
2465
|
+
continue;
|
|
2466
|
+
const user = entries.find((e) => isRecord(e) && e["scope"] === "user" && typeof e["installPath"] === "string");
|
|
2467
|
+
if (!user)
|
|
2468
|
+
continue;
|
|
2469
|
+
const marketplace = id.slice(id.lastIndexOf("@") + 1);
|
|
2470
|
+
const install = {
|
|
2471
|
+
scope,
|
|
2472
|
+
scopeRef,
|
|
2473
|
+
version: String(user["version"] ?? ""),
|
|
2474
|
+
enabled: !isRecord(enabledPlugins) || enabledPlugins[id] !== false,
|
|
2475
|
+
installPath: String(user["installPath"]),
|
|
2476
|
+
lastUpdated: typeof user["lastUpdated"] === "string" ? user["lastUpdated"] : void 0
|
|
2477
|
+
};
|
|
2478
|
+
const list = found.get(id) ?? [];
|
|
2479
|
+
list.push({ install, repo: marketplaceRepo(marketplaces, marketplace) });
|
|
2480
|
+
found.set(id, list);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
const projects = Object.entries(reader.getProjects());
|
|
2484
|
+
const remotes = await Promise.all(projects.map(async ([name, p]) => ({ name, path: p.path, repo: normalizeRepo(await remoteOf(p.path) ?? void 0) })));
|
|
2485
|
+
const plugins = [];
|
|
2486
|
+
for (const [id, list] of found) {
|
|
2487
|
+
const newest = [...list].sort((a, b) => (b.install.lastUpdated ?? "").localeCompare(a.install.lastUpdated ?? ""))[0];
|
|
2488
|
+
const manifest = readJson(join8(newest.install.installPath, ".claude-plugin", "plugin.json"));
|
|
2489
|
+
const repo = [manifestRepo(manifest), ...list.map((f) => f.repo)].map(normalizeRepo).find(Boolean);
|
|
2490
|
+
const match = repo ? remotes.find((r) => r.repo === repo) : void 0;
|
|
2491
|
+
plugins.push({
|
|
2492
|
+
id,
|
|
2493
|
+
name: id.slice(0, id.lastIndexOf("@")),
|
|
2494
|
+
marketplace: id.slice(id.lastIndexOf("@") + 1),
|
|
2495
|
+
description: String(manifest["description"] ?? ""),
|
|
2496
|
+
repo,
|
|
2497
|
+
installs: list.map((f) => f.install),
|
|
2498
|
+
skills: skillDirs(newest.install.installPath, manifest).flatMap((dir) => readSkill(reader, dir) ?? []),
|
|
2499
|
+
project: match?.name,
|
|
2500
|
+
contributing: match ? existsSync6(join8(match.path, "CONTRIBUTING.md")) : false,
|
|
2501
|
+
projectSkills: match ? projectSkillNames(match.path) : []
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
return plugins.sort((a, b) => a.name.localeCompare(b.name));
|
|
2505
|
+
}
|
|
2506
|
+
function readPluginSkill(plugin, name) {
|
|
2507
|
+
const skill = plugin.skills.find((s) => s.name === name);
|
|
2508
|
+
if (!skill)
|
|
2509
|
+
return null;
|
|
2510
|
+
try {
|
|
2511
|
+
return { name: skill.name, description: skill.description, content: readFileSync6(join8(skill.path, "SKILL.md"), "utf-8") };
|
|
2512
|
+
} catch {
|
|
2513
|
+
return null;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
var isRecord, inside;
|
|
2517
|
+
var init_plugins = __esm({
|
|
2518
|
+
"../core/dist/plugins.js"() {
|
|
2519
|
+
"use strict";
|
|
2520
|
+
isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2521
|
+
inside = (root, path) => {
|
|
2522
|
+
const r = resolve2(root);
|
|
2523
|
+
const p = resolve2(path);
|
|
2524
|
+
return p === r || p.startsWith(r + sep);
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2529
|
+
// ../core/dist/skill-routes.js
|
|
2530
|
+
import { Hono as Hono2 } from "hono";
|
|
2531
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, rmSync as rmSync2, existsSync as existsSync7, unlinkSync } from "node:fs";
|
|
2532
|
+
import { dirname as dirname2, join as join9 } from "node:path";
|
|
2533
|
+
function installFailure(result2) {
|
|
2534
|
+
try {
|
|
2535
|
+
const entries = JSON.parse(result2.stdout);
|
|
2536
|
+
const reason = entries.find((e) => e.status !== "installed")?.reason;
|
|
2537
|
+
if (reason)
|
|
2538
|
+
return reason;
|
|
2539
|
+
} catch {
|
|
2540
|
+
}
|
|
2541
|
+
return result2.stderr.trim().split("\n").pop() || `skills CLI exited with code ${result2.code}`;
|
|
2542
|
+
}
|
|
2543
|
+
function commandFailure(result2) {
|
|
2544
|
+
if (result2.code === 127)
|
|
2545
|
+
return "the claude CLI is not installed";
|
|
2546
|
+
const lines2 = `${result2.stderr}
|
|
2547
|
+
${result2.stdout}`.trim().split("\n").filter(Boolean);
|
|
2548
|
+
return lines2.find((l) => /error|fatal/i.test(l)) ?? lines2.pop() ?? `claude exited with code ${result2.code}`;
|
|
2549
|
+
}
|
|
2550
|
+
function skillRoutes(reader, options = {}) {
|
|
2551
|
+
const runnerFor = options.runnerFor ?? ((env) => createRunner(env, INSTALL_TIMEOUT_MS));
|
|
2552
|
+
const remoteOf = options.remoteOf ?? createRemoteLookup(runCommand);
|
|
2553
|
+
const app = new Hono2();
|
|
2554
|
+
const updating = /* @__PURE__ */ new Set();
|
|
2555
|
+
app.get("/", (c) => {
|
|
2556
|
+
const account = c.req.query("account");
|
|
2557
|
+
const project = c.req.query("project");
|
|
2558
|
+
return c.json(reader.getSkills(account, project));
|
|
2559
|
+
});
|
|
2560
|
+
app.get("/global/:name", (c) => {
|
|
2561
|
+
const name = c.req.param("name");
|
|
2562
|
+
const skill = reader.getSkill("global", "global", name);
|
|
2563
|
+
if (!skill)
|
|
2564
|
+
return c.json({ error: "Skill not found" }, 404);
|
|
2565
|
+
return c.json(skill);
|
|
2566
|
+
});
|
|
2567
|
+
app.get("/account/:account/:name", (c) => {
|
|
2568
|
+
const { account, name } = c.req.param();
|
|
2569
|
+
const skill = reader.getSkill("account", account, name);
|
|
2570
|
+
if (!skill)
|
|
2571
|
+
return c.json({ error: "Skill not found" }, 404);
|
|
2572
|
+
return c.json(skill);
|
|
2573
|
+
});
|
|
2574
|
+
app.get("/project/:project/:name", (c) => {
|
|
2575
|
+
const { project, name } = c.req.param();
|
|
2576
|
+
const skill = reader.getSkill("project", project, name);
|
|
2577
|
+
if (!skill)
|
|
2578
|
+
return c.json({ error: "Skill not found" }, 404);
|
|
2579
|
+
return c.json(skill);
|
|
2580
|
+
});
|
|
2581
|
+
app.post("/", async (c) => {
|
|
2582
|
+
const { scope, scopeRef, name, content } = await c.req.json();
|
|
2583
|
+
const ref = scopeRef ?? (scope === "global" ? "global" : "");
|
|
2584
|
+
const dir = reader.getSkillDir(scope, ref, name);
|
|
2585
|
+
if (existsSync7(join9(dir, "SKILL.md"))) {
|
|
2586
|
+
return c.json({ error: "Skill already exists" }, 409);
|
|
2587
|
+
}
|
|
2588
|
+
try {
|
|
2589
|
+
mkdirSync2(dir, { recursive: true });
|
|
2590
|
+
writeFileSync2(join9(dir, "SKILL.md"), content, "utf-8");
|
|
2591
|
+
return c.json({ ok: true, dir });
|
|
2592
|
+
} catch (err) {
|
|
2593
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2594
|
+
return c.json({ error: `Failed to create skill: ${message}` }, 500);
|
|
2595
|
+
}
|
|
2596
|
+
});
|
|
2597
|
+
const scopePatterns = [
|
|
2598
|
+
{ pattern: "/global/:name", scope: "global" },
|
|
2599
|
+
{ pattern: "/account/:account/:name", scope: "account", paramKey: "account" },
|
|
2600
|
+
{ pattern: "/project/:project/:name", scope: "project", paramKey: "project" }
|
|
2601
|
+
];
|
|
2602
|
+
for (const { pattern, scope, paramKey } of scopePatterns) {
|
|
2603
|
+
app.put(pattern, async (c) => {
|
|
2604
|
+
const params = c.req.param();
|
|
2605
|
+
const scopeRef = paramKey ? params[paramKey] ?? "" : "global";
|
|
2606
|
+
const name = params["name"] ?? "";
|
|
2607
|
+
const dir = reader.getSkillDir(scope, scopeRef, name);
|
|
2608
|
+
const skillMd = join9(dir, "SKILL.md");
|
|
2609
|
+
if (!existsSync7(skillMd)) {
|
|
2610
|
+
return c.json({ error: "Skill not found" }, 404);
|
|
2611
|
+
}
|
|
2612
|
+
const { content } = await c.req.json();
|
|
2613
|
+
try {
|
|
2614
|
+
writeFileSync2(skillMd, content, "utf-8");
|
|
2615
|
+
return c.json({ ok: true });
|
|
2616
|
+
} catch (err) {
|
|
2617
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2618
|
+
return c.json({ error: `Failed to update skill: ${message}` }, 500);
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
app.put(`${pattern}/references/:filename`, async (c) => {
|
|
2622
|
+
const params = c.req.param();
|
|
2623
|
+
const scopeRef = paramKey ? params[paramKey] ?? "" : "global";
|
|
2624
|
+
const name = params["name"] ?? "";
|
|
2625
|
+
const filename = params["filename"] ?? "";
|
|
2626
|
+
const dir = reader.getSkillDir(scope, scopeRef, name);
|
|
2627
|
+
const skillMd = join9(dir, "SKILL.md");
|
|
2628
|
+
if (!existsSync7(skillMd)) {
|
|
2629
|
+
return c.json({ error: "Skill not found" }, 404);
|
|
2630
|
+
}
|
|
2631
|
+
const { content } = await c.req.json();
|
|
2632
|
+
try {
|
|
2633
|
+
const refsDir = join9(dir, "references");
|
|
2634
|
+
mkdirSync2(refsDir, { recursive: true });
|
|
2635
|
+
writeFileSync2(join9(refsDir, filename), content, "utf-8");
|
|
2636
|
+
return c.json({ ok: true });
|
|
2637
|
+
} catch (err) {
|
|
2638
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2639
|
+
return c.json({ error: `Failed to write reference: ${message}` }, 500);
|
|
2640
|
+
}
|
|
2641
|
+
});
|
|
2642
|
+
app.delete(`${pattern}/references/:filename`, (c) => {
|
|
2643
|
+
const params = c.req.param();
|
|
2644
|
+
const scopeRef = paramKey ? params[paramKey] ?? "" : "global";
|
|
2645
|
+
const name = params["name"] ?? "";
|
|
2646
|
+
const filename = params["filename"] ?? "";
|
|
2647
|
+
const dir = reader.getSkillDir(scope, scopeRef, name);
|
|
2648
|
+
const refFile = join9(dir, "references", filename);
|
|
2649
|
+
if (!existsSync7(refFile)) {
|
|
2650
|
+
return c.json({ error: "Reference not found" }, 404);
|
|
2651
|
+
}
|
|
2652
|
+
try {
|
|
2653
|
+
unlinkSync(refFile);
|
|
2654
|
+
return c.json({ ok: true });
|
|
2655
|
+
} catch (err) {
|
|
2656
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2657
|
+
return c.json({ error: `Failed to delete reference: ${message}` }, 500);
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
app.delete(pattern, (c) => {
|
|
2661
|
+
const params = c.req.param();
|
|
2662
|
+
const scopeRef = paramKey ? params[paramKey] ?? "" : "global";
|
|
2663
|
+
const name = params["name"] ?? "";
|
|
2664
|
+
const dir = reader.getSkillDir(scope, scopeRef, name);
|
|
2665
|
+
if (!existsSync7(join9(dir, "SKILL.md"))) {
|
|
2666
|
+
return c.json({ error: "Skill not found" }, 404);
|
|
2667
|
+
}
|
|
2668
|
+
try {
|
|
2669
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
2670
|
+
return c.json({ ok: true });
|
|
2671
|
+
} catch (err) {
|
|
2672
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2673
|
+
return c.json({ error: `Failed to delete skill: ${message}` }, 500);
|
|
2674
|
+
}
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
app.get("/explore", async (c) => {
|
|
2678
|
+
const q = c.req.query("q") ?? "";
|
|
2679
|
+
try {
|
|
2680
|
+
const res = await globalThis.fetch(`https://skills.sh/api/search?q=${encodeURIComponent(q)}`);
|
|
2681
|
+
if (!res.ok) {
|
|
2682
|
+
return c.json({ results: [] });
|
|
2683
|
+
}
|
|
2684
|
+
const data = await res.json();
|
|
2685
|
+
const results = (data.skills ?? []).flatMap((s) => s.id ? [{
|
|
2686
|
+
name: s.name ?? s.id,
|
|
2687
|
+
slug: s.id,
|
|
2688
|
+
skillId: s.skillId ?? "",
|
|
2689
|
+
installs: s.installs ?? 0,
|
|
2690
|
+
source: "skills.sh",
|
|
2691
|
+
url: `https://skills.sh/${s.id}`,
|
|
2692
|
+
repo: s.source ?? ""
|
|
2693
|
+
}] : []);
|
|
2694
|
+
return c.json({ results });
|
|
2695
|
+
} catch {
|
|
2696
|
+
return c.json({ results: [] });
|
|
2697
|
+
}
|
|
2698
|
+
});
|
|
2699
|
+
app.post("/install", async (c) => {
|
|
2700
|
+
const { repo, skill, scope, scopeRef } = await c.req.json();
|
|
2701
|
+
if (!repo || !skill) {
|
|
2702
|
+
return c.json({ error: "repo and skill are required" }, 400);
|
|
2703
|
+
}
|
|
2704
|
+
const ref = scope === "global" ? "global" : scopeRef ?? "";
|
|
2705
|
+
const known = scope === "global" || (scope === "account" ? reader.getAccounts().includes(ref) : ref in reader.getProjects());
|
|
2706
|
+
if (!known) {
|
|
2707
|
+
return c.json({ error: `Unknown ${scope}: ${ref}` }, 404);
|
|
2708
|
+
}
|
|
2709
|
+
const configDir = reader.getSkillConfigDir(scope, ref);
|
|
2710
|
+
const args = ["--yes", "skills", "add", repo, "--skill", skill, "--agent", "claude-code", "--yes", "--json"];
|
|
2711
|
+
if (scope !== "project")
|
|
2712
|
+
args.push("--global");
|
|
2713
|
+
const run = runnerFor({ ...envWithoutHarness(), CLAUDE_CONFIG_DIR: configDir });
|
|
2714
|
+
const result2 = await run("npx", args, dirname2(configDir));
|
|
2715
|
+
if (result2.code !== 0) {
|
|
2716
|
+
return c.json({ error: `Failed to install skill: ${installFailure(result2)}` }, 500);
|
|
2717
|
+
}
|
|
2718
|
+
return c.json({ ok: true });
|
|
2719
|
+
});
|
|
2720
|
+
app.get("/plugins", async (c) => c.json(await listPlugins(reader, remoteOf)));
|
|
2721
|
+
app.get("/plugins/:id/skills/:name", async (c) => {
|
|
2722
|
+
const plugin = (await listPlugins(reader, remoteOf)).find((p) => p.id === c.req.param("id"));
|
|
2723
|
+
const skill = plugin && readPluginSkill(plugin, c.req.param("name"));
|
|
2724
|
+
if (!skill)
|
|
2725
|
+
return c.json({ error: "Skill not found" }, 404);
|
|
2726
|
+
return c.json(skill);
|
|
2727
|
+
});
|
|
2728
|
+
app.post("/plugins/:id/update", async (c) => {
|
|
2729
|
+
const id = c.req.param("id");
|
|
2730
|
+
const { scope, scopeRef } = await c.req.json().catch(() => ({}));
|
|
2731
|
+
const ref = scope === "global" ? "global" : scopeRef ?? "";
|
|
2732
|
+
const find = async () => (await listPlugins(reader, remoteOf)).find((p) => p.id === id);
|
|
2733
|
+
const plugin = await find();
|
|
2734
|
+
const install = plugin?.installs.find((i) => i.scope === scope && i.scopeRef === ref);
|
|
2735
|
+
if (!plugin || !install)
|
|
2736
|
+
return c.json({ error: `${id} is not installed in ${ref || "that scope"}` }, 404);
|
|
2737
|
+
const configDir = reader.getSkillConfigDir(install.scope, install.scopeRef);
|
|
2738
|
+
if (updating.has(configDir))
|
|
2739
|
+
return c.json({ error: `An update is already running for ${ref}` }, 409);
|
|
2740
|
+
updating.add(configDir);
|
|
2741
|
+
try {
|
|
2742
|
+
const { CLAUDE_CONFIG_DIR: _ignored, ...base } = envWithoutHarness();
|
|
2743
|
+
const run = runnerFor(install.scope === "global" ? base : { ...base, CLAUDE_CONFIG_DIR: configDir });
|
|
2744
|
+
for (const args of [["plugin", "marketplace", "update", plugin.marketplace], ["plugin", "update", plugin.id]]) {
|
|
2745
|
+
const result2 = await run("claude", args, configDir);
|
|
2746
|
+
if (result2.code !== 0)
|
|
2747
|
+
return c.json({ error: `Failed to update ${plugin.name}: ${commandFailure(result2)}` }, 500);
|
|
2748
|
+
}
|
|
2749
|
+
} finally {
|
|
2750
|
+
updating.delete(configDir);
|
|
2751
|
+
}
|
|
2752
|
+
const version = (await find())?.installs.find((i) => i.scope === install.scope && i.scopeRef === ref)?.version ?? install.version;
|
|
2753
|
+
return c.json({ ok: true, version });
|
|
2754
|
+
});
|
|
2755
|
+
return app;
|
|
2756
|
+
}
|
|
2757
|
+
var INSTALL_TIMEOUT_MS;
|
|
2758
|
+
var init_skill_routes = __esm({
|
|
2759
|
+
"../core/dist/skill-routes.js"() {
|
|
2760
|
+
"use strict";
|
|
2761
|
+
init_cw_doctor();
|
|
2762
|
+
init_task_review();
|
|
2763
|
+
init_plugins();
|
|
2764
|
+
INSTALL_TIMEOUT_MS = 12e4;
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
// ../core/dist/pty-manager.js
|
|
2769
|
+
import * as pty2 from "node-pty";
|
|
2770
|
+
import { existsSync as existsSync8, chmodSync, statSync as statSync2 } from "node:fs";
|
|
2771
|
+
import { join as join10, dirname as dirname3 } from "node:path";
|
|
2772
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
2773
|
+
function ensureSpawnHelperPermissions() {
|
|
2774
|
+
try {
|
|
2775
|
+
const require2 = createRequire2(import.meta.url);
|
|
2776
|
+
const ptyPath = dirname3(require2.resolve("node-pty/package.json"));
|
|
2777
|
+
const helper = join10(ptyPath, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
|
|
2778
|
+
if (existsSync8(helper)) {
|
|
2779
|
+
const mode = statSync2(helper).mode;
|
|
2780
|
+
if (!(mode & 73)) {
|
|
2781
|
+
chmodSync(helper, mode | 493);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
} catch {
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
function buildLaunch(session, isNew) {
|
|
2788
|
+
const env = envWithoutHarness();
|
|
2789
|
+
const harness = session.harness ?? "claude";
|
|
2790
|
+
const harnessFlag = isNew && session.harness ? ` --harness ${shellQuote(harness)}` : "";
|
|
2791
|
+
const prefix = session.skipPermissions ? "cw --skip-permissions" : "cw";
|
|
2792
|
+
if (session.type === "general") {
|
|
2793
|
+
let cmd2 = "cw launch";
|
|
2794
|
+
if (session.account)
|
|
2795
|
+
cmd2 += ` ${shellQuote(session.account)}`;
|
|
2796
|
+
if (harness === "claude") {
|
|
2797
|
+
if (session.model)
|
|
2798
|
+
cmd2 += ` --model ${shellQuote(session.model)}`;
|
|
2799
|
+
if (session.skipPermissions)
|
|
2800
|
+
cmd2 += " --dangerously-skip-permissions";
|
|
2801
|
+
}
|
|
2802
|
+
if (isNew && session.harness)
|
|
2803
|
+
env.CW_HARNESS = harness;
|
|
2804
|
+
return { command: cmd2, env };
|
|
2805
|
+
}
|
|
2806
|
+
if (session.type === "login") {
|
|
2807
|
+
return { command: `cw account login ${shellQuote(session.account)} --harness ${shellQuote(harness)}`, env };
|
|
2808
|
+
}
|
|
2809
|
+
if (session.type === "create") {
|
|
2810
|
+
const desc = session.notes || session.task || "New project";
|
|
2811
|
+
let cmd2 = `cw create ${shellQuote(desc)}`;
|
|
2812
|
+
if (supports(harness, "agent_teams"))
|
|
2813
|
+
cmd2 += " --team";
|
|
2814
|
+
if (session.task)
|
|
2815
|
+
cmd2 += ` --name ${shellQuote(session.task)}`;
|
|
2816
|
+
if (session.account)
|
|
2817
|
+
cmd2 += ` --account ${shellQuote(session.account)}`;
|
|
2818
|
+
if (session.model)
|
|
2819
|
+
cmd2 += ` --model ${shellQuote(session.model)}`;
|
|
2820
|
+
if (session.worktree)
|
|
2821
|
+
cmd2 += ` --dir ${shellQuote(session.worktree)}`;
|
|
2822
|
+
return { command: cmd2 + harnessFlag, env };
|
|
2823
|
+
}
|
|
2824
|
+
if (session.type === "review") {
|
|
2825
|
+
const prArg = session.source_url || session.pr;
|
|
2826
|
+
let cmd2 = `${prefix} review ${shellQuote(session.project)} ${shellQuote(String(prArg ?? ""))}`;
|
|
2827
|
+
if (session.account)
|
|
2828
|
+
cmd2 += ` --account ${shellQuote(session.account)}`;
|
|
2829
|
+
if (session.model)
|
|
2830
|
+
cmd2 += ` --model ${shellQuote(session.model)}`;
|
|
2831
|
+
return { command: cmd2 + harnessFlag, env };
|
|
2832
|
+
}
|
|
2833
|
+
if (session.type === "loop") {
|
|
2834
|
+
const prompt = session.loop_prompt ?? "";
|
|
2835
|
+
const slug = session.sessionDir?.replace(/^loop-/, "") ?? session.task ?? "";
|
|
2836
|
+
let cmd2 = `${prefix} loop ${shellQuote(session.project)} ${shellQuote(prompt)} --name ${shellQuote(slug)}`;
|
|
2837
|
+
if (session.loop_interval)
|
|
2838
|
+
cmd2 += ` --every ${shellQuote(session.loop_interval)}`;
|
|
2839
|
+
if (session.account)
|
|
2840
|
+
cmd2 += ` --account ${shellQuote(session.account)}`;
|
|
2841
|
+
if (session.model)
|
|
2842
|
+
cmd2 += ` --model ${shellQuote(session.model)}`;
|
|
2843
|
+
return { command: cmd2 + harnessFlag, env };
|
|
2844
|
+
}
|
|
2845
|
+
const taskArg = session.source_url || session.task;
|
|
2846
|
+
let cmd = `${prefix} work ${shellQuote(session.project)} ${shellQuote(taskArg ?? "")}`;
|
|
2847
|
+
if (session.account)
|
|
2848
|
+
cmd += ` --account ${shellQuote(session.account)}`;
|
|
2849
|
+
if (session.workflow)
|
|
2850
|
+
cmd += ` --workflow ${shellQuote(session.workflow)}`;
|
|
2851
|
+
if (session.model)
|
|
2852
|
+
cmd += ` --model ${shellQuote(session.model)}`;
|
|
2853
|
+
return { command: cmd + harnessFlag, env };
|
|
2854
|
+
}
|
|
2855
|
+
var SCROLLBACK_LIMIT, IDLE_TIMEOUT_MS, CLEANUP_INTERVAL_MS, shellQuote, PTYManager;
|
|
2856
|
+
var init_pty_manager = __esm({
|
|
2857
|
+
"../core/dist/pty-manager.js"() {
|
|
2858
|
+
"use strict";
|
|
2859
|
+
init_cw_doctor();
|
|
2860
|
+
init_harness_capabilities();
|
|
2861
|
+
SCROLLBACK_LIMIT = 5e3;
|
|
2862
|
+
IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
2863
|
+
CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
2864
|
+
shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
2865
|
+
ensureSpawnHelperPermissions();
|
|
2866
|
+
PTYManager = class {
|
|
2867
|
+
states;
|
|
2868
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2869
|
+
cleanupTimer = null;
|
|
2870
|
+
constructor(states = null) {
|
|
2871
|
+
this.states = states;
|
|
2872
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
|
|
2873
|
+
}
|
|
2874
|
+
makeKey(project, sessionDir) {
|
|
2875
|
+
return `${project}::${sessionDir}`;
|
|
2876
|
+
}
|
|
2877
|
+
getOrCreate(project, sessionDir, session, options = {}) {
|
|
2878
|
+
const key = this.makeKey(project, sessionDir);
|
|
2879
|
+
const existing = this.sessions.get(key);
|
|
2880
|
+
if (existing)
|
|
2881
|
+
return existing;
|
|
2882
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
2883
|
+
const { command, env } = buildLaunch(session, options.isNew ?? false);
|
|
2884
|
+
const cwd = session.worktree && existsSync8(session.worktree) ? session.worktree : session.type === "general" || session.type === "create" || session.type === "login" ? process.env.HOME ?? process.cwd() : process.cwd();
|
|
2885
|
+
let ptyProcess;
|
|
2886
|
+
try {
|
|
2887
|
+
ptyProcess = pty2.spawn(shell, ["-c", command], {
|
|
2888
|
+
name: "xterm-256color",
|
|
2889
|
+
cols: 120,
|
|
2890
|
+
rows: 40,
|
|
2891
|
+
cwd,
|
|
2892
|
+
env
|
|
2893
|
+
});
|
|
2894
|
+
} catch (err) {
|
|
2895
|
+
console.error(`[pty] Failed to spawn for ${key}: ${err}`);
|
|
2896
|
+
return null;
|
|
2897
|
+
}
|
|
2898
|
+
const ptySession = {
|
|
2899
|
+
pty: ptyProcess,
|
|
2900
|
+
scrollback: [],
|
|
2901
|
+
clients: /* @__PURE__ */ new Set(),
|
|
2902
|
+
cwd,
|
|
2903
|
+
command,
|
|
2904
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2905
|
+
lastClientDisconnect: null,
|
|
2906
|
+
onDataDisposable: null,
|
|
2907
|
+
onExitDisposable: null
|
|
2908
|
+
};
|
|
2909
|
+
this.states?.track(key, session.harness ?? "claude");
|
|
2910
|
+
ptySession.onDataDisposable = ptyProcess.onData((data) => {
|
|
2911
|
+
this.states?.output(key, data);
|
|
2912
|
+
ptySession.scrollback.push(data);
|
|
2913
|
+
if (ptySession.scrollback.length > SCROLLBACK_LIMIT) {
|
|
2914
|
+
ptySession.scrollback.splice(0, ptySession.scrollback.length - SCROLLBACK_LIMIT);
|
|
2915
|
+
}
|
|
2916
|
+
const message = JSON.stringify({ type: "output", data });
|
|
2917
|
+
for (const client of ptySession.clients) {
|
|
2918
|
+
try {
|
|
2919
|
+
client.send(message);
|
|
2920
|
+
} catch {
|
|
2921
|
+
ptySession.clients.delete(client);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
});
|
|
2925
|
+
ptySession.onExitDisposable = ptyProcess.onExit(({ exitCode }) => {
|
|
2926
|
+
this.states?.exit(key, exitCode);
|
|
2927
|
+
const message = JSON.stringify({ type: "exit", code: exitCode });
|
|
2928
|
+
for (const client of ptySession.clients) {
|
|
2929
|
+
try {
|
|
2930
|
+
client.send(message);
|
|
2931
|
+
} catch {
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
this.sessions.delete(key);
|
|
2935
|
+
});
|
|
2936
|
+
this.sessions.set(key, ptySession);
|
|
2937
|
+
return ptySession;
|
|
2938
|
+
}
|
|
2939
|
+
attach(sessionId, client) {
|
|
2940
|
+
const session = this.sessions.get(sessionId);
|
|
2941
|
+
if (!session)
|
|
2942
|
+
return;
|
|
2943
|
+
session.clients.add(client);
|
|
2944
|
+
session.lastClientDisconnect = null;
|
|
2945
|
+
if (session.scrollback.length > 0) {
|
|
2946
|
+
const scrollbackData = session.scrollback.join("");
|
|
2947
|
+
try {
|
|
2948
|
+
client.send(JSON.stringify({ type: "scrollback", data: scrollbackData }));
|
|
2949
|
+
} catch {
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
detach(sessionId, client) {
|
|
2954
|
+
const session = this.sessions.get(sessionId);
|
|
2955
|
+
if (!session)
|
|
2956
|
+
return;
|
|
2957
|
+
session.clients.delete(client);
|
|
2958
|
+
if (session.clients.size === 0) {
|
|
2959
|
+
session.lastClientDisconnect = /* @__PURE__ */ new Date();
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
kill(sessionId) {
|
|
2963
|
+
const session = this.sessions.get(sessionId);
|
|
2964
|
+
if (!session)
|
|
2965
|
+
return;
|
|
2966
|
+
try {
|
|
2967
|
+
session.onDataDisposable.dispose();
|
|
2968
|
+
session.onExitDisposable.dispose();
|
|
2969
|
+
session.pty.kill();
|
|
2970
|
+
} catch {
|
|
2971
|
+
}
|
|
2972
|
+
this.states?.forget(sessionId);
|
|
2973
|
+
this.sessions.delete(sessionId);
|
|
2974
|
+
}
|
|
2975
|
+
has(sessionId) {
|
|
2976
|
+
return this.sessions.has(sessionId);
|
|
2977
|
+
}
|
|
2978
|
+
get(sessionId) {
|
|
2979
|
+
return this.sessions.get(sessionId);
|
|
2980
|
+
}
|
|
2981
|
+
// the state tracker reads the screen at the size the user sees it
|
|
2982
|
+
resize(sessionId, cols, rows) {
|
|
2983
|
+
const session = this.sessions.get(sessionId);
|
|
2984
|
+
if (!session)
|
|
2985
|
+
return;
|
|
2986
|
+
session.pty.resize(cols, rows);
|
|
2987
|
+
this.states?.resize(sessionId, cols, rows);
|
|
2988
|
+
}
|
|
2989
|
+
cleanup() {
|
|
2990
|
+
const now = Date.now();
|
|
2991
|
+
for (const [key, session] of this.sessions) {
|
|
2992
|
+
if (session.clients.size === 0 && session.lastClientDisconnect && now - session.lastClientDisconnect.getTime() > IDLE_TIMEOUT_MS) {
|
|
2993
|
+
this.kill(key);
|
|
2994
|
+
}
|
|
2995
|
+
if (session.clients.size === 0 && !session.lastClientDisconnect && now - session.createdAt.getTime() > IDLE_TIMEOUT_MS) {
|
|
2996
|
+
this.kill(key);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
dispose() {
|
|
3001
|
+
if (this.cleanupTimer) {
|
|
3002
|
+
clearInterval(this.cleanupTimer);
|
|
3003
|
+
this.cleanupTimer = null;
|
|
3004
|
+
}
|
|
3005
|
+
for (const key of this.sessions.keys()) {
|
|
3006
|
+
this.kill(key);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
3012
|
+
|
|
3013
|
+
// ../core/dist/state-classifier-local.js
|
|
3014
|
+
function latestMarker(text, markers) {
|
|
3015
|
+
let best = null;
|
|
3016
|
+
let bestAt = -1;
|
|
3017
|
+
for (const marker of markers) {
|
|
3018
|
+
const at = lastIndex(text, marker.re);
|
|
3019
|
+
if (at > bestAt) {
|
|
3020
|
+
best = marker;
|
|
3021
|
+
bestAt = at;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return best;
|
|
3025
|
+
}
|
|
3026
|
+
function classifyLocal(input) {
|
|
3027
|
+
if (input.exitCode !== null)
|
|
3028
|
+
return result("exited", 1);
|
|
3029
|
+
if (input.quietMs < ACTIVE_MS)
|
|
3030
|
+
return result("working", input.harness === "claude" ? 0.95 : 0.6);
|
|
3031
|
+
if (input.harness !== "claude")
|
|
3032
|
+
return result("idle", 0.5);
|
|
3033
|
+
const marker = latestMarker(input.text, CLAUDE_MARKERS);
|
|
3034
|
+
return marker ? result(marker.state, marker.confidence) : result("idle", 0.3);
|
|
3035
|
+
}
|
|
3036
|
+
var ACTIVE_MS, CLAUDE_MARKERS, lastIndex, result;
|
|
3037
|
+
var init_state_classifier_local = __esm({
|
|
3038
|
+
"../core/dist/state-classifier-local.js"() {
|
|
3039
|
+
"use strict";
|
|
3040
|
+
ACTIVE_MS = 1500;
|
|
3041
|
+
CLAUDE_MARKERS = [
|
|
3042
|
+
{ state: "permission", confidence: 0.95, re: /Do you want to [^?\n]{1,80}\?[\s\S]{0,40}❯ ?1\. Yes/g },
|
|
3043
|
+
{ state: "permission", confidence: 0.95, re: /Esc to cancel · Tab to amend|Enter to confirm · Esc to cancel/g },
|
|
3044
|
+
{ state: "waiting", confidence: 0.9, re: new RegExp("(?:^|[^(\\p{L}\\d_])\\p{Lu}\\p{Ll}+ed for (?:\\d+[hm] )*\\d+s\\b", "gmu") },
|
|
3045
|
+
// a redraw can reuse the digits of the spinner above, leaving "Baked for done 10:12 am"
|
|
3046
|
+
{ state: "waiting", confidence: 0.9, re: new RegExp("\\p{Lu}\\p{Ll}+ed for [^\\n\u2026]{0,20}?\\bdone \\d{1,2}:\\d{2}", "gu") },
|
|
3047
|
+
{ state: "waiting", confidence: 0.9, re: /Interrupted · What should Claude do instead\?/g },
|
|
3048
|
+
{ state: "waiting", confidence: 0.8, re: /Claude Code v\d+\.\d+/g },
|
|
3049
|
+
{ state: "error", confidence: 0.85, re: /API Error|usage limit reached|You've hit your (?:usage )?limit|Credit balance is too low|Please run \/login|OAuth token (?:has )?expired/gi },
|
|
3050
|
+
// a spinner that stopped moving: a stall or a slow redraw, so it is only a guess
|
|
3051
|
+
{ state: "working", confidence: 0.5, re: new RegExp("\\p{Lu}\\p{Ll}+ing\u2026|\\(\\d+s \xB7 |esc to interrupt", "gu") }
|
|
3052
|
+
];
|
|
3053
|
+
lastIndex = (text, re) => {
|
|
3054
|
+
let at = -1;
|
|
3055
|
+
for (const m of text.matchAll(re))
|
|
3056
|
+
at = (m.index ?? 0) + m[0].length;
|
|
3057
|
+
return at;
|
|
3058
|
+
};
|
|
3059
|
+
result = (state, confidence) => ({ state, confidence, source: "local" });
|
|
3060
|
+
}
|
|
3061
|
+
});
|
|
3062
|
+
|
|
3063
|
+
// ../core/dist/state-classifier-jev.js
|
|
3064
|
+
function jevScreen(text, lines2 = SCREEN_LINES) {
|
|
3065
|
+
const kept = text.split("\n").map((l) => l.replace(FOOTER_NOISE, "").trim()).filter(Boolean).filter((l, i, all) => l !== all[i - 1]).slice(-lines2).join("\n");
|
|
3066
|
+
return kept.slice(-SCREEN_CHARS);
|
|
3067
|
+
}
|
|
3068
|
+
function createJevClassifier({ apiKey, model = "jev-latest", timeoutMs = 3e3, fetch: fetch2 = globalThis.fetch }) {
|
|
3069
|
+
return {
|
|
3070
|
+
name: "jev",
|
|
3071
|
+
async classify(input) {
|
|
3072
|
+
if (input.exitCode !== null)
|
|
3073
|
+
return { state: "exited", confidence: 1, source: "jev" };
|
|
3074
|
+
const body = {
|
|
3075
|
+
model,
|
|
3076
|
+
state: { harness: input.harness, seconds_since_last_output: Math.round(input.quietMs / 1e3), screen: jevScreen(input.screen ?? input.text) },
|
|
3077
|
+
questions: {
|
|
3078
|
+
state: {
|
|
3079
|
+
type: "choice",
|
|
3080
|
+
instructions: "What is the coding agent in `screen` doing right now? `screen` is the bottom of its terminal with the newest line last; older lines can be leftovers of a redraw.",
|
|
3081
|
+
criteria: CRITERIA
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
};
|
|
3085
|
+
const res = await fetch2(JEV_URL, {
|
|
3086
|
+
method: "POST",
|
|
3087
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
3088
|
+
body: JSON.stringify(body),
|
|
3089
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3090
|
+
});
|
|
3091
|
+
if (!res.ok)
|
|
3092
|
+
throw new Error(`Jev answered ${res.status}`);
|
|
3093
|
+
const data = await res.json();
|
|
3094
|
+
const answer = data.answers?.state;
|
|
3095
|
+
if (!answer || !(answer.choice in CRITERIA) || typeof answer.confidence !== "number") {
|
|
3096
|
+
throw new Error(`Unexpected Jev answer: ${JSON.stringify(answer)}`);
|
|
3097
|
+
}
|
|
3098
|
+
return { state: answer.choice, confidence: answer.confidence, source: "jev" };
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3102
|
+
var JEV_URL, SCREEN_LINES, SCREEN_CHARS, CRITERIA, FOOTER_NOISE;
|
|
3103
|
+
var init_state_classifier_jev = __esm({
|
|
3104
|
+
"../core/dist/state-classifier-jev.js"() {
|
|
3105
|
+
"use strict";
|
|
3106
|
+
JEV_URL = "https://api.typesafe.ai/v1/systemone";
|
|
3107
|
+
SCREEN_LINES = 30;
|
|
3108
|
+
SCREEN_CHARS = 4e3;
|
|
3109
|
+
CRITERIA = {
|
|
3110
|
+
working: "The agent is still busy: a spinner, a progress line or a running tool is the newest thing above the prompt. Status bars and notices below the prompt are not progress.",
|
|
3111
|
+
waiting: "The agent finished its turn or was interrupted and is waiting at an empty prompt for the next message.",
|
|
3112
|
+
permission: "The agent shows a question with options the user must pick, such as approving a command or edit, trusting a folder, or answering yes or no.",
|
|
3113
|
+
error: "The agent stopped on an error: an API error, a usage or rate limit, a failed login or a crash.",
|
|
3114
|
+
idle: "Nothing on screen shows which of the other options applies."
|
|
3115
|
+
};
|
|
3116
|
+
FOOTER_NOISE = /Checking for updates|new task\? \/clear to save [\d.]+k? tokens/g;
|
|
3117
|
+
}
|
|
3118
|
+
});
|
|
3119
|
+
|
|
3120
|
+
// ../core/dist/state-classifier.js
|
|
3121
|
+
function terminalText(raw) {
|
|
3122
|
+
return raw.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[\d*[GC]/g, " ").replace(/\x1b\[[\d;]*[ABEFHfd]/g, "\n").replace(/\x1b\[[0-9;?<>=]*[ -/]*[@-~]/g, "").replace(/\x1b[()][0-9A-Za-z]/g, "").replace(/\x1b[@-_]/g, "").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
|
|
3123
|
+
}
|
|
3124
|
+
var init_state_classifier = __esm({
|
|
3125
|
+
"../core/dist/state-classifier.js"() {
|
|
3126
|
+
"use strict";
|
|
3127
|
+
}
|
|
3128
|
+
});
|
|
3129
|
+
|
|
3130
|
+
// ../core/dist/terminal-screen.js
|
|
3131
|
+
import xterm from "@xterm/headless";
|
|
3132
|
+
var Terminal, SCROLLBACK, TerminalScreen;
|
|
3133
|
+
var init_terminal_screen = __esm({
|
|
3134
|
+
"../core/dist/terminal-screen.js"() {
|
|
3135
|
+
"use strict";
|
|
3136
|
+
({ Terminal } = xterm);
|
|
3137
|
+
SCROLLBACK = 200;
|
|
3138
|
+
TerminalScreen = class {
|
|
3139
|
+
term;
|
|
3140
|
+
constructor(cols = 120, rows = 40) {
|
|
3141
|
+
this.term = new Terminal({ cols, rows, scrollback: SCROLLBACK, allowProposedApi: true });
|
|
3142
|
+
}
|
|
3143
|
+
write(data) {
|
|
3144
|
+
this.term.write(data);
|
|
3145
|
+
}
|
|
3146
|
+
// a resize applies at once while writes wait to be parsed, so it waits its turn behind them
|
|
3147
|
+
resize(cols, rows) {
|
|
3148
|
+
this.term.write("", () => this.term.resize(cols, rows));
|
|
3149
|
+
}
|
|
3150
|
+
// rows oldest first, scrollback included, without the blank rows under the last one written
|
|
3151
|
+
read() {
|
|
3152
|
+
return new Promise((resolve5) => this.term.write("", () => {
|
|
3153
|
+
const buffer = this.term.buffer.active;
|
|
3154
|
+
const rows = [];
|
|
3155
|
+
for (let y = 0; y < buffer.length; y++) {
|
|
3156
|
+
const line = buffer.getLine(y);
|
|
3157
|
+
if (!line)
|
|
3158
|
+
continue;
|
|
3159
|
+
const text = line.translateToString(false);
|
|
3160
|
+
if (line.isWrapped && rows.length)
|
|
3161
|
+
rows[rows.length - 1] += text;
|
|
3162
|
+
else
|
|
3163
|
+
rows.push(text);
|
|
3164
|
+
}
|
|
3165
|
+
const trimmed = rows.map((r) => r.trimEnd());
|
|
3166
|
+
while (trimmed.length && !trimmed[trimmed.length - 1])
|
|
3167
|
+
trimmed.pop();
|
|
3168
|
+
resolve5(trimmed.join("\n"));
|
|
3169
|
+
}));
|
|
3170
|
+
}
|
|
3171
|
+
dispose() {
|
|
3172
|
+
this.term.dispose();
|
|
3173
|
+
}
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
});
|
|
3177
|
+
|
|
3178
|
+
// ../core/dist/session-state.js
|
|
3179
|
+
function remoteClassifierFromEnv(env = process.env) {
|
|
3180
|
+
const shadow = env.FORGE_JEV_SHADOW === "1";
|
|
3181
|
+
if (env.FORGE_STATE_CLASSIFIER !== "jev")
|
|
3182
|
+
return { remote: null, shadow: false, warning: null };
|
|
3183
|
+
if (!env.TYPESAFE_API_KEY)
|
|
3184
|
+
return { remote: null, shadow: false, warning: "FORGE_STATE_CLASSIFIER=jev needs TYPESAFE_API_KEY; reading session states with local rules only" };
|
|
3185
|
+
return { remote: createJevClassifier({ apiKey: env.TYPESAFE_API_KEY, model: env.FORGE_JEV_MODEL || void 0 }), shadow, warning: null };
|
|
3186
|
+
}
|
|
3187
|
+
var TAIL_CHARS, StateTracker;
|
|
3188
|
+
var init_session_state = __esm({
|
|
3189
|
+
"../core/dist/session-state.js"() {
|
|
3190
|
+
"use strict";
|
|
3191
|
+
init_state_classifier_local();
|
|
3192
|
+
init_state_classifier_jev();
|
|
3193
|
+
init_state_classifier();
|
|
3194
|
+
init_terminal_screen();
|
|
3195
|
+
TAIL_CHARS = 32e3;
|
|
3196
|
+
StateTracker = class {
|
|
3197
|
+
sessions = /* @__PURE__ */ new Map();
|
|
3198
|
+
remote;
|
|
3199
|
+
settleMs;
|
|
3200
|
+
burstMs;
|
|
3201
|
+
localTrust;
|
|
3202
|
+
remoteMin;
|
|
3203
|
+
shadow;
|
|
3204
|
+
keepExitedMs;
|
|
3205
|
+
onError;
|
|
3206
|
+
onRemote;
|
|
3207
|
+
constructor(options = {}) {
|
|
3208
|
+
this.remote = options.remote ?? null;
|
|
3209
|
+
this.settleMs = options.settleMs ?? 1500;
|
|
3210
|
+
this.burstMs = options.burstMs ?? 2e3;
|
|
3211
|
+
this.localTrust = options.localTrust ?? 0.9;
|
|
3212
|
+
this.remoteMin = options.remoteMin ?? 0.6;
|
|
3213
|
+
this.shadow = options.shadow ?? false;
|
|
3214
|
+
this.keepExitedMs = options.keepExitedMs ?? 10 * 60 * 1e3;
|
|
3215
|
+
this.onError = options.onError ?? (() => {
|
|
3216
|
+
});
|
|
3217
|
+
this.onRemote = options.onRemote ?? (() => {
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
get remoteName() {
|
|
3221
|
+
return this.remote?.name ?? null;
|
|
3222
|
+
}
|
|
3223
|
+
get isShadow() {
|
|
3224
|
+
return this.remote !== null && this.shadow;
|
|
3225
|
+
}
|
|
3226
|
+
track(key, harness) {
|
|
3227
|
+
const now = Date.now();
|
|
3228
|
+
this.forget(key);
|
|
3229
|
+
const tracked = {
|
|
3230
|
+
harness,
|
|
3231
|
+
raw: "",
|
|
3232
|
+
lastOutputAt: now,
|
|
3233
|
+
burstStart: now,
|
|
3234
|
+
version: 0,
|
|
3235
|
+
timer: null,
|
|
3236
|
+
screen: new TerminalScreen(),
|
|
3237
|
+
askedScreen: null,
|
|
3238
|
+
exitedAt: null,
|
|
3239
|
+
entry: { state: "working", confidence: 0.6, since: now, source: "local" }
|
|
3240
|
+
};
|
|
3241
|
+
this.sessions.set(key, tracked);
|
|
3242
|
+
this.schedule(key, tracked);
|
|
3243
|
+
}
|
|
3244
|
+
output(key, chunk) {
|
|
3245
|
+
const tracked = this.sessions.get(key);
|
|
3246
|
+
if (!tracked || tracked.exitedAt !== null)
|
|
3247
|
+
return;
|
|
3248
|
+
tracked.screen.write(chunk);
|
|
3249
|
+
if (!terminalText(chunk).trim())
|
|
3250
|
+
return;
|
|
3251
|
+
const now = Date.now();
|
|
3252
|
+
tracked.raw = (tracked.raw + chunk).slice(-TAIL_CHARS);
|
|
3253
|
+
tracked.lastOutputAt = now;
|
|
3254
|
+
tracked.version++;
|
|
3255
|
+
tracked.burstStart ??= now;
|
|
3256
|
+
if (now - tracked.burstStart >= this.burstMs && tracked.entry.state !== "working") {
|
|
3257
|
+
this.apply(tracked, { state: "working", confidence: 0.95, source: "local" });
|
|
3258
|
+
}
|
|
3259
|
+
this.schedule(key, tracked);
|
|
3260
|
+
}
|
|
3261
|
+
exit(key, exitCode) {
|
|
3262
|
+
const tracked = this.sessions.get(key);
|
|
3263
|
+
if (!tracked)
|
|
3264
|
+
return;
|
|
3265
|
+
if (tracked.timer)
|
|
3266
|
+
clearTimeout(tracked.timer);
|
|
3267
|
+
tracked.timer = null;
|
|
3268
|
+
tracked.exitedAt = Date.now();
|
|
3269
|
+
tracked.version++;
|
|
3270
|
+
this.apply(tracked, { state: "exited", confidence: 1, source: "local" });
|
|
3271
|
+
tracked.entry.exitCode = exitCode;
|
|
3272
|
+
}
|
|
3273
|
+
resize(key, cols, rows) {
|
|
3274
|
+
this.sessions.get(key)?.screen.resize(cols, rows);
|
|
3275
|
+
}
|
|
3276
|
+
forget(key) {
|
|
3277
|
+
const tracked = this.sessions.get(key);
|
|
3278
|
+
if (tracked?.timer)
|
|
3279
|
+
clearTimeout(tracked.timer);
|
|
3280
|
+
tracked?.screen.dispose();
|
|
3281
|
+
this.sessions.delete(key);
|
|
3282
|
+
}
|
|
3283
|
+
snapshot() {
|
|
3284
|
+
const now = Date.now();
|
|
3285
|
+
const out = {};
|
|
3286
|
+
for (const [key, tracked] of this.sessions) {
|
|
3287
|
+
if (tracked.exitedAt !== null && now - tracked.exitedAt > this.keepExitedMs) {
|
|
3288
|
+
this.sessions.delete(key);
|
|
3289
|
+
continue;
|
|
3290
|
+
}
|
|
3291
|
+
out[key] = { ...tracked.entry };
|
|
3292
|
+
}
|
|
3293
|
+
return out;
|
|
3294
|
+
}
|
|
3295
|
+
dispose() {
|
|
3296
|
+
for (const key of [...this.sessions.keys()])
|
|
3297
|
+
this.forget(key);
|
|
3298
|
+
}
|
|
3299
|
+
schedule(key, tracked) {
|
|
3300
|
+
if (tracked.timer)
|
|
3301
|
+
clearTimeout(tracked.timer);
|
|
3302
|
+
tracked.timer = setTimeout(() => this.settle(key, tracked), this.settleMs);
|
|
3303
|
+
}
|
|
3304
|
+
apply(tracked, c) {
|
|
3305
|
+
if (c.state !== tracked.entry.state)
|
|
3306
|
+
tracked.entry.since = Date.now();
|
|
3307
|
+
tracked.entry.state = c.state;
|
|
3308
|
+
tracked.entry.confidence = c.confidence;
|
|
3309
|
+
tracked.entry.source = c.source;
|
|
3310
|
+
}
|
|
3311
|
+
settle(key, tracked) {
|
|
3312
|
+
tracked.timer = null;
|
|
3313
|
+
tracked.burstStart = null;
|
|
3314
|
+
if (this.sessions.get(key) !== tracked || tracked.exitedAt !== null)
|
|
3315
|
+
return;
|
|
3316
|
+
const quietMs = Math.max(Date.now() - tracked.lastOutputAt, this.settleMs);
|
|
3317
|
+
const input = { text: terminalText(tracked.raw), quietMs, harness: tracked.harness, exitCode: null };
|
|
3318
|
+
const local = classifyLocal(input);
|
|
3319
|
+
this.apply(tracked, local);
|
|
3320
|
+
if (!this.remote)
|
|
3321
|
+
return;
|
|
3322
|
+
if (!this.shadow && (local.confidence >= this.localTrust || local.state === "error"))
|
|
3323
|
+
return;
|
|
3324
|
+
const remote = this.remote;
|
|
3325
|
+
const version = tracked.version;
|
|
3326
|
+
void tracked.screen.read().then((screen) => {
|
|
3327
|
+
if (this.sessions.get(key) !== tracked || tracked.version !== version)
|
|
3328
|
+
return;
|
|
3329
|
+
if (!screen.trim() || screen === tracked.askedScreen)
|
|
3330
|
+
return;
|
|
3331
|
+
tracked.askedScreen = screen;
|
|
3332
|
+
const startedAt = Date.now();
|
|
3333
|
+
const record = (answer, error, outcome) => this.onRemote({
|
|
3334
|
+
at: startedAt,
|
|
3335
|
+
key,
|
|
3336
|
+
harness: input.harness,
|
|
3337
|
+
screen,
|
|
3338
|
+
local,
|
|
3339
|
+
remote: answer,
|
|
3340
|
+
error,
|
|
3341
|
+
latencyMs: Date.now() - startedAt,
|
|
3342
|
+
outcome
|
|
3343
|
+
});
|
|
3344
|
+
remote.classify({ ...input, screen }).then((answer) => {
|
|
3345
|
+
if (this.shadow)
|
|
3346
|
+
return record(answer, null, "shadow");
|
|
3347
|
+
if (this.sessions.get(key) !== tracked || tracked.version !== version)
|
|
3348
|
+
return record(answer, null, "stale");
|
|
3349
|
+
if (answer.confidence < this.remoteMin)
|
|
3350
|
+
return record(answer, null, "low-confidence");
|
|
3351
|
+
this.apply(tracked, answer);
|
|
3352
|
+
record(answer, null, "applied");
|
|
3353
|
+
}, (err) => {
|
|
3354
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
3355
|
+
record(null, error.message, "failed");
|
|
3356
|
+
this.onError(error);
|
|
3357
|
+
});
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
3362
|
+
});
|
|
3363
|
+
|
|
3364
|
+
// ../core/dist/state-log.js
|
|
3365
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
3366
|
+
import { appendFile } from "node:fs/promises";
|
|
3367
|
+
import { homedir as homedir3 } from "node:os";
|
|
3368
|
+
import { dirname as dirname4, resolve as resolve3 } from "node:path";
|
|
3369
|
+
function expandHome(path) {
|
|
3370
|
+
return path === "~" || path.startsWith("~/") ? homedir3() + path.slice(1) : path;
|
|
3371
|
+
}
|
|
3372
|
+
function stateLogLine(call) {
|
|
3373
|
+
return JSON.stringify({
|
|
3374
|
+
at: new Date(call.at).toISOString(),
|
|
3375
|
+
key: call.key,
|
|
3376
|
+
harness: call.harness,
|
|
3377
|
+
outcome: call.outcome,
|
|
3378
|
+
local: { state: call.local.state, confidence: call.local.confidence },
|
|
3379
|
+
remote: call.remote && { state: call.remote.state, confidence: call.remote.confidence },
|
|
3380
|
+
error: call.error,
|
|
3381
|
+
latencyMs: call.latencyMs,
|
|
3382
|
+
screen: jevScreen(call.screen)
|
|
3383
|
+
}) + "\n";
|
|
3384
|
+
}
|
|
3385
|
+
function createStateLog(path, onError = () => {
|
|
3386
|
+
}) {
|
|
3387
|
+
const file = resolve3(expandHome(path));
|
|
3388
|
+
mkdirSync3(dirname4(file), { recursive: true });
|
|
3389
|
+
let queue = Promise.resolve();
|
|
3390
|
+
return (call) => {
|
|
3391
|
+
const line = stateLogLine(call);
|
|
3392
|
+
queue = queue.then(() => appendFile(file, line)).catch((err) => onError(err instanceof Error ? err : new Error(String(err))));
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
var init_state_log = __esm({
|
|
3396
|
+
"../core/dist/state-log.js"() {
|
|
3397
|
+
"use strict";
|
|
3398
|
+
init_state_classifier_jev();
|
|
3399
|
+
}
|
|
3400
|
+
});
|
|
3401
|
+
|
|
3402
|
+
// ../core/dist/pty-routes.js
|
|
3403
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
3404
|
+
function takeSession(reader, project, sessionDir) {
|
|
3405
|
+
const sessionId = `${project}::${sessionDir}`;
|
|
3406
|
+
const pending = pendingSessions.get(sessionId);
|
|
3407
|
+
if (pending) {
|
|
3408
|
+
pendingSessions.delete(sessionId);
|
|
3409
|
+
return { session: pending, isNew: true };
|
|
3410
|
+
}
|
|
3411
|
+
const session = reader.getSession(project, sessionDir);
|
|
3412
|
+
return session ? { session, isNew: false } : null;
|
|
3413
|
+
}
|
|
3414
|
+
function createTerminalWss(manager, reader, { localOnly = true } = {}) {
|
|
3415
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
3416
|
+
function wireWs(ws, sessionId, project, sessionDir) {
|
|
3417
|
+
const client = {
|
|
3418
|
+
send: (data) => {
|
|
3419
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
3420
|
+
ws.send(data);
|
|
3421
|
+
},
|
|
3422
|
+
close: () => ws.close()
|
|
3423
|
+
};
|
|
3424
|
+
manager.attach(sessionId, client);
|
|
3425
|
+
ws.on("message", (raw) => {
|
|
3426
|
+
try {
|
|
3427
|
+
const msg = JSON.parse(raw.toString());
|
|
3428
|
+
if (msg.type === "input") {
|
|
3429
|
+
const ptySession = manager.get(sessionId);
|
|
3430
|
+
if (ptySession)
|
|
3431
|
+
ptySession.pty.write(msg.data);
|
|
3432
|
+
} else if (msg.type === "resize") {
|
|
3433
|
+
if (msg.cols && msg.rows)
|
|
3434
|
+
manager.resize(sessionId, msg.cols, msg.rows);
|
|
3435
|
+
}
|
|
3436
|
+
} catch {
|
|
3437
|
+
}
|
|
3438
|
+
});
|
|
3439
|
+
ws.on("close", () => {
|
|
3440
|
+
console.log(`[pty-ws] Disconnected: ${project}/${sessionDir}`);
|
|
3441
|
+
manager.detach(sessionId, client);
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
wss.on("connection", (ws, project, sessionDir) => {
|
|
3445
|
+
console.log(`[pty-ws] Connected: ${project}/${sessionDir}`);
|
|
3446
|
+
const sessionId = `${project}::${sessionDir}`;
|
|
3447
|
+
if (manager.has(sessionId)) {
|
|
3448
|
+
wireWs(ws, sessionId, project, sessionDir);
|
|
3449
|
+
return;
|
|
3450
|
+
}
|
|
3451
|
+
const taken = takeSession(reader, project, sessionDir);
|
|
3452
|
+
if (!taken) {
|
|
3453
|
+
console.log(`[pty-ws] Session not found: ${project}/${sessionDir}`);
|
|
3454
|
+
ws.send(JSON.stringify({ type: "error", message: `Session not found: ${project}/${sessionDir}` }));
|
|
3455
|
+
ws.close();
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
const ptySession = manager.getOrCreate(project, sessionDir, taken.session, { isNew: taken.isNew });
|
|
3459
|
+
if (!ptySession) {
|
|
3460
|
+
console.error(`[pty-ws] Failed to spawn terminal for: ${project}/${sessionDir}`);
|
|
3461
|
+
ws.send(JSON.stringify({ type: "error", message: `Failed to start terminal for ${project}/${sessionDir}` }));
|
|
3462
|
+
ws.close();
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
wireWs(ws, sessionId, project, sessionDir);
|
|
3466
|
+
});
|
|
3467
|
+
function attachToServer(server) {
|
|
3468
|
+
server.on("upgrade", (request, socket, head) => {
|
|
3469
|
+
if (localOnly && !isLocalRequest(request.headers.host, request.headers.origin)) {
|
|
3470
|
+
socket.destroy();
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
3474
|
+
const parsed = parseTerminalUpgradeUrl(url.pathname);
|
|
3475
|
+
if (parsed) {
|
|
3476
|
+
const { project, sessionDir } = parsed;
|
|
3477
|
+
console.log(`[pty-ws] Upgrade request: ${project}/${sessionDir}`);
|
|
3478
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
3479
|
+
wss.emit("connection", ws, project, sessionDir);
|
|
3480
|
+
});
|
|
3481
|
+
} else {
|
|
3482
|
+
socket.destroy();
|
|
3483
|
+
}
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
return { wss, attachToServer };
|
|
3487
|
+
}
|
|
3488
|
+
function parseTerminalUpgradeUrl(pathname) {
|
|
3489
|
+
const match = pathname.match(/^\/ws\/terminal\/([^/]+)\/([^/]+)$/);
|
|
3490
|
+
if (!match)
|
|
3491
|
+
return null;
|
|
3492
|
+
try {
|
|
3493
|
+
return { project: decodeURIComponent(match[1]), sessionDir: decodeURIComponent(match[2]) };
|
|
3494
|
+
} catch {
|
|
3495
|
+
return null;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
var init_pty_routes = __esm({
|
|
3499
|
+
"../core/dist/pty-routes.js"() {
|
|
3500
|
+
"use strict";
|
|
3501
|
+
init_cw_routes();
|
|
3502
|
+
init_origin_guard();
|
|
3503
|
+
}
|
|
3504
|
+
});
|
|
3505
|
+
|
|
3506
|
+
// ../core/dist/liveframe.js
|
|
3507
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
3508
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
3509
|
+
import { homedir as homedir4 } from "node:os";
|
|
3510
|
+
import { join as join11 } from "node:path";
|
|
3511
|
+
function slugify(name) {
|
|
3512
|
+
const slug = name.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
3513
|
+
return slug || "untitled";
|
|
3514
|
+
}
|
|
3515
|
+
function failureMessage(result2) {
|
|
3516
|
+
if (result2.code === 127)
|
|
3517
|
+
return INSTALL_HINT;
|
|
3518
|
+
const lastLine = tailOutput(result2.stderr, 1) || tailOutput(result2.stdout, 1);
|
|
3519
|
+
return lastLine.replace(/^✗\s*/, "").trim() || `lf exited with code ${result2.code}`;
|
|
3520
|
+
}
|
|
3521
|
+
function parseFrameUrl(stdout) {
|
|
3522
|
+
return FRAME_URL_RE.exec(stripAnsi(stdout))?.[0] ?? null;
|
|
3523
|
+
}
|
|
3524
|
+
function readJson2(path) {
|
|
3525
|
+
try {
|
|
3526
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
3527
|
+
} catch {
|
|
3528
|
+
return null;
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
var LiveframeError, DEFAULT_API, DEFAULT_ACCOUNT, INSTALL_HINT, LF_TIMEOUT_MS, FRAME_URL_RE, SEGMENT_RE, isFrameSegment, Liveframe;
|
|
3532
|
+
var init_liveframe = __esm({
|
|
3533
|
+
"../core/dist/liveframe.js"() {
|
|
3534
|
+
"use strict";
|
|
3535
|
+
init_task_review();
|
|
3536
|
+
init_output();
|
|
3537
|
+
LiveframeError = class extends Error {
|
|
3538
|
+
status;
|
|
3539
|
+
constructor(message, status = 502) {
|
|
3540
|
+
super(message);
|
|
3541
|
+
this.status = status;
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
DEFAULT_API = "https://liveframe.monokulabs.com";
|
|
3545
|
+
DEFAULT_ACCOUNT = "monoku";
|
|
3546
|
+
INSTALL_HINT = `The Liveframe CLI is not installed. Install it with: npm i -g ${DEFAULT_API}/cli.tgz`;
|
|
3547
|
+
LF_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3548
|
+
FRAME_URL_RE = /https?:\/\/\S+\/f\/\S+/;
|
|
3549
|
+
SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
3550
|
+
isFrameSegment = (value) => SEGMENT_RE.test(value);
|
|
3551
|
+
Liveframe = class {
|
|
3552
|
+
account;
|
|
3553
|
+
root;
|
|
3554
|
+
home;
|
|
3555
|
+
run;
|
|
3556
|
+
busy = /* @__PURE__ */ new Set();
|
|
3557
|
+
constructor(options = {}) {
|
|
3558
|
+
this.account = options.account ?? process.env.FORGE_LIVEFRAME_ACCOUNT ?? DEFAULT_ACCOUNT;
|
|
3559
|
+
this.root = options.root ?? join11(homedir4(), "liveframe");
|
|
3560
|
+
this.home = options.home ?? process.env.LIVEFRAME_HOME ?? join11(homedir4(), ".liveframe");
|
|
3561
|
+
this.run = options.run ?? createRunner(void 0, LF_TIMEOUT_MS);
|
|
3562
|
+
}
|
|
3563
|
+
// reads only the API base and whether a credential exists, never the credential itself
|
|
3564
|
+
config() {
|
|
3565
|
+
const file = readJson2(join11(this.home, "config.json")) ?? {};
|
|
3566
|
+
const api = process.env.LIVEFRAME_API ?? (typeof file.api === "string" ? file.api : DEFAULT_API);
|
|
3567
|
+
return { api: api.replace(/\/+$/, ""), signedIn: Boolean(process.env.LIVEFRAME_TOKEN || file.oauth || file.token) };
|
|
3568
|
+
}
|
|
3569
|
+
async status() {
|
|
3570
|
+
const { api, signedIn } = this.config();
|
|
3571
|
+
const version = await this.run("lf", ["--version"], homedir4());
|
|
3572
|
+
return { account: this.account, installed: version.code === 0, signedIn, api, root: this.root };
|
|
3573
|
+
}
|
|
3574
|
+
frameDir(project, frame) {
|
|
3575
|
+
return join11(this.root, project, frame);
|
|
3576
|
+
}
|
|
3577
|
+
readFrame(project, frame, api) {
|
|
3578
|
+
const dir = this.frameDir(project, frame);
|
|
3579
|
+
const json = readJson2(join11(dir, "liveframe.json"));
|
|
3580
|
+
if (!json || typeof json.key !== "string")
|
|
3581
|
+
return null;
|
|
3582
|
+
const branch = typeof json.branch === "string" ? json.branch : "main";
|
|
3583
|
+
return {
|
|
3584
|
+
project,
|
|
3585
|
+
frame,
|
|
3586
|
+
dir,
|
|
3587
|
+
key: json.key,
|
|
3588
|
+
kind: typeof json.kind === "string" ? json.kind : "view",
|
|
3589
|
+
branch,
|
|
3590
|
+
url: `${api}/f/${json.key}/${branch === "main" ? "" : `${branch}/`}`
|
|
3591
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
getFrame(project, frame) {
|
|
3594
|
+
if (!isFrameSegment(project) || !isFrameSegment(frame))
|
|
3595
|
+
return null;
|
|
3596
|
+
return this.readFrame(project, frame, this.config().api);
|
|
3597
|
+
}
|
|
3598
|
+
listFrames() {
|
|
3599
|
+
if (!existsSync9(this.root))
|
|
3600
|
+
return [];
|
|
3601
|
+
const { api } = this.config();
|
|
3602
|
+
const frames = [];
|
|
3603
|
+
for (const project of readdirSync5(this.root, { withFileTypes: true })) {
|
|
3604
|
+
if (!project.isDirectory() || !isFrameSegment(project.name))
|
|
3605
|
+
continue;
|
|
3606
|
+
for (const frame of readdirSync5(join11(this.root, project.name), { withFileTypes: true })) {
|
|
3607
|
+
if (!frame.isDirectory() || !isFrameSegment(frame.name))
|
|
3608
|
+
continue;
|
|
3609
|
+
const found = this.readFrame(project.name, frame.name, api);
|
|
3610
|
+
if (found)
|
|
3611
|
+
frames.push(found);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
return frames;
|
|
3615
|
+
}
|
|
3616
|
+
async lf(args, cwd) {
|
|
3617
|
+
const result2 = await this.run("lf", args, cwd);
|
|
3618
|
+
if (result2.code !== 0)
|
|
3619
|
+
throw new LiveframeError(failureMessage(result2));
|
|
3620
|
+
return result2;
|
|
3621
|
+
}
|
|
3622
|
+
// one lf command per frame folder at a time
|
|
3623
|
+
async exclusive(project, frame, work) {
|
|
3624
|
+
const key = `${project}/${frame}`;
|
|
3625
|
+
if (this.busy.has(key))
|
|
3626
|
+
throw new LiveframeError(`${key} is busy with another lf command`, 409);
|
|
3627
|
+
this.busy.add(key);
|
|
3628
|
+
try {
|
|
3629
|
+
return await work();
|
|
3630
|
+
} finally {
|
|
3631
|
+
this.busy.delete(key);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
// runs lf into a fresh frame folder, removing whatever it left if it fails
|
|
3635
|
+
intoNewDir(project, frame, args) {
|
|
3636
|
+
return this.exclusive(project, frame, async () => {
|
|
3637
|
+
const projectDir = join11(this.root, project);
|
|
3638
|
+
const dir = this.frameDir(project, frame);
|
|
3639
|
+
if (existsSync9(dir))
|
|
3640
|
+
throw new LiveframeError(`${project}/${frame} already exists in ${this.root}`, 409);
|
|
3641
|
+
await mkdir(projectDir, { recursive: true });
|
|
3642
|
+
try {
|
|
3643
|
+
await this.lf(args(dir), this.root);
|
|
3644
|
+
} catch (err) {
|
|
3645
|
+
await rm(dir, { recursive: true, force: true });
|
|
3646
|
+
if (readdirSync5(projectDir).length === 0)
|
|
3647
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
3648
|
+
throw err;
|
|
3649
|
+
}
|
|
3650
|
+
const found = this.getFrame(project, frame);
|
|
3651
|
+
if (!found)
|
|
3652
|
+
throw new LiveframeError(`lf finished but ${dir} has no liveframe.json`);
|
|
3653
|
+
return found;
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
async createFrame(projectName, frameName) {
|
|
3657
|
+
return this.intoNewDir(slugify(projectName), slugify(frameName), (dir) => ["new", frameName, "--project", projectName, "--kind", "view", "--dir", dir, "--yes"]);
|
|
3658
|
+
}
|
|
3659
|
+
async pullFrame(project, frame) {
|
|
3660
|
+
if (!isFrameSegment(project) || !isFrameSegment(frame))
|
|
3661
|
+
throw new LiveframeError("Use project/frame slugs, for example reservas/checkout-flow", 400);
|
|
3662
|
+
return this.intoNewDir(project, frame, (dir) => ["pull", `${project}/${frame}`, dir]);
|
|
3663
|
+
}
|
|
3664
|
+
async pushFrame(project, frame, note) {
|
|
3665
|
+
const found = this.getFrame(project, frame);
|
|
3666
|
+
if (!found)
|
|
3667
|
+
throw new LiveframeError(`No frame ${project}/${frame} in ${this.root}`, 404);
|
|
3668
|
+
const result2 = await this.exclusive(project, frame, () => this.lf(["push", ...note ? ["-m", note] : []], found.dir));
|
|
3669
|
+
const url = parseFrameUrl(result2.stdout);
|
|
3670
|
+
if (!url)
|
|
3671
|
+
throw new LiveframeError("lf push finished without a frame URL");
|
|
3672
|
+
return url;
|
|
3673
|
+
}
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
|
|
3678
|
+
// ../core/dist/liveframe-routes.js
|
|
3679
|
+
import { Hono as Hono3 } from "hono";
|
|
3680
|
+
function liveframeRoutes(liveframe = new Liveframe()) {
|
|
3681
|
+
const app = new Hono3();
|
|
3682
|
+
app.onError((err, c) => {
|
|
3683
|
+
if (err instanceof LiveframeError)
|
|
3684
|
+
return c.json({ error: err.message }, err.status);
|
|
3685
|
+
throw err;
|
|
3686
|
+
});
|
|
3687
|
+
app.get("/status", async (c) => c.json(await liveframe.status()));
|
|
3688
|
+
app.get("/frames", (c) => c.json(liveframe.listFrames()));
|
|
3689
|
+
app.post("/frames", async (c) => {
|
|
3690
|
+
const { project, name } = await c.req.json();
|
|
3691
|
+
if (!project?.trim() || !name?.trim())
|
|
3692
|
+
return c.json({ error: "project and name are required" }, 400);
|
|
3693
|
+
return c.json(await liveframe.createFrame(project.trim(), name.trim()), 201);
|
|
3694
|
+
});
|
|
3695
|
+
app.post("/pull", async (c) => {
|
|
3696
|
+
const { target } = await c.req.json();
|
|
3697
|
+
const [project = "", frame = ""] = (target ?? "").trim().split("/");
|
|
3698
|
+
return c.json(await liveframe.pullFrame(project, frame), 201);
|
|
3699
|
+
});
|
|
3700
|
+
app.post("/frames/:project/:frame/push", async (c) => {
|
|
3701
|
+
const { project, frame } = c.req.param();
|
|
3702
|
+
const { note } = await c.req.json().catch(() => ({ note: void 0 }));
|
|
3703
|
+
return c.json({ url: await liveframe.pushFrame(project, frame, note?.trim() || void 0) });
|
|
3704
|
+
});
|
|
3705
|
+
return app;
|
|
3706
|
+
}
|
|
3707
|
+
var init_liveframe_routes = __esm({
|
|
3708
|
+
"../core/dist/liveframe-routes.js"() {
|
|
3709
|
+
"use strict";
|
|
3710
|
+
init_liveframe();
|
|
3711
|
+
}
|
|
3712
|
+
});
|
|
3713
|
+
|
|
3714
|
+
// ../core/dist/server.js
|
|
3715
|
+
import { Hono as Hono4 } from "hono";
|
|
3716
|
+
import { cors } from "hono/cors";
|
|
3717
|
+
import { join as join12, basename as basename3, resolve as resolve4 } from "node:path";
|
|
3718
|
+
import { readdirSync as readdirSync6, existsSync as existsSync10 } from "node:fs";
|
|
3719
|
+
function createForgeServer(options) {
|
|
3720
|
+
const { dataDir, db: externalDb, authToken, localOnly = true } = options;
|
|
3721
|
+
const dbPath = join12(dataDir, "forge.db");
|
|
3722
|
+
const modulesDir = join12(dataDir, "modules");
|
|
3723
|
+
const db = externalDb ?? new ForgeDB(dbPath);
|
|
3724
|
+
const loader = new ModuleLoader(modulesDir);
|
|
3725
|
+
const runner = new ActionRunner();
|
|
3726
|
+
loader.discover();
|
|
3727
|
+
const app = new Hono4();
|
|
3728
|
+
app.use("*", localOnly ? localOriginGuard() : cors());
|
|
3729
|
+
if (authToken) {
|
|
3730
|
+
app.use("/api/*", bearerAuth(authToken));
|
|
3731
|
+
}
|
|
3732
|
+
const cwReader = new CWReader();
|
|
3733
|
+
const loginManager = new LoginManager(resolveCwBin(cwReader.cwHome));
|
|
3734
|
+
app.route("/api/cw", cwRoutes(cwReader, { loginManager, localOnly }));
|
|
3735
|
+
app.route("/api/skills", skillRoutes(cwReader));
|
|
3736
|
+
const { remote, shadow, warning } = remoteClassifierFromEnv();
|
|
3737
|
+
if (warning)
|
|
3738
|
+
console.warn(`[state] ${warning}`);
|
|
3739
|
+
const logPath = process.env.FORGE_STATE_LOG;
|
|
3740
|
+
if (logPath && !remote)
|
|
3741
|
+
console.warn("[state] FORGE_STATE_LOG only records remote classifier calls; set FORGE_STATE_CLASSIFIER=jev");
|
|
3742
|
+
const onRemote = logPath && remote ? createStateLog(logPath, (err) => console.warn(`[state] could not write ${logPath}: ${err.message}`)) : void 0;
|
|
3743
|
+
let lastStateError = "";
|
|
3744
|
+
const states = new StateTracker({
|
|
3745
|
+
remote,
|
|
3746
|
+
shadow,
|
|
3747
|
+
onRemote,
|
|
3748
|
+
onError: (err) => {
|
|
3749
|
+
if (err.message !== lastStateError)
|
|
3750
|
+
console.warn(`[state] ${remote?.name ?? "remote"} classifier failed, using local rules: ${err.message}`);
|
|
3751
|
+
lastStateError = err.message;
|
|
3752
|
+
}
|
|
3753
|
+
});
|
|
3754
|
+
const ptyManager = new PTYManager(states);
|
|
3755
|
+
const terminalWss = createTerminalWss(ptyManager, cwReader, { localOnly });
|
|
3756
|
+
app.route("/api/liveframe", liveframeRoutes());
|
|
3757
|
+
app.get("/api/cw/session-states", (c) => c.json({ classifier: states.remoteName ?? "local", shadow: states.isShadow, states: states.snapshot() }));
|
|
3758
|
+
app.post("/api/cw/terminal/kill", async (c) => {
|
|
3759
|
+
const { project, sessionDir } = await c.req.json();
|
|
3760
|
+
const sessionId = `${project}::${sessionDir}`;
|
|
3761
|
+
ptyManager.kill(sessionId);
|
|
3762
|
+
return c.json({ ok: true });
|
|
3763
|
+
});
|
|
3764
|
+
async function resolveAction(c) {
|
|
3765
|
+
const moduleName = c.req.param("module");
|
|
3766
|
+
const actionId = c.req.param("action");
|
|
3767
|
+
const { projectId } = await c.req.json();
|
|
3768
|
+
const action = loader.getAction(moduleName, actionId);
|
|
3769
|
+
if (!action) {
|
|
3770
|
+
return c.json({ error: "Action not found" }, 404);
|
|
3771
|
+
}
|
|
3772
|
+
const project = projectId ? db.getProject(projectId) : void 0;
|
|
3773
|
+
const cwd = project?.path ?? process.cwd();
|
|
3774
|
+
const logId = db.logAction({
|
|
3775
|
+
projectId: projectId ?? null,
|
|
3776
|
+
moduleId: moduleName,
|
|
3777
|
+
actionId,
|
|
3778
|
+
command: action.command
|
|
3779
|
+
});
|
|
3780
|
+
return { action, cwd, logId };
|
|
3781
|
+
}
|
|
3782
|
+
app.get("/api/health", (c) => {
|
|
3783
|
+
return c.json({
|
|
3784
|
+
status: "ok",
|
|
3785
|
+
version: "0.1.0",
|
|
3786
|
+
modules: loader.getLoadedCount()
|
|
3787
|
+
});
|
|
3788
|
+
});
|
|
3789
|
+
app.get("/api/projects", (c) => {
|
|
3790
|
+
return c.json(db.listProjects());
|
|
3791
|
+
});
|
|
3792
|
+
app.post("/api/projects", async (c) => {
|
|
3793
|
+
const { name, path } = await c.req.json();
|
|
3794
|
+
const id = db.addProject({ name, path });
|
|
3795
|
+
const project = db.getProject(id);
|
|
3796
|
+
return c.json(project, 201);
|
|
3797
|
+
});
|
|
3798
|
+
app.delete("/api/projects/:id", (c) => {
|
|
3799
|
+
db.removeProject(c.req.param("id"));
|
|
3800
|
+
return c.json({ ok: true });
|
|
3801
|
+
});
|
|
3802
|
+
app.get("/api/modules", (c) => {
|
|
3803
|
+
const installed = db.listModules();
|
|
3804
|
+
const loaded = loader.getLoaded();
|
|
3805
|
+
return c.json(installed.map((m) => ({
|
|
3806
|
+
...m,
|
|
3807
|
+
manifest: loaded.get(m.name) ?? null
|
|
3808
|
+
})));
|
|
3809
|
+
});
|
|
3810
|
+
app.get("/api/modules/available", (c) => {
|
|
3811
|
+
const manifests = loader.discover();
|
|
3812
|
+
return c.json(manifests);
|
|
3813
|
+
});
|
|
3814
|
+
app.post("/api/actions/:module/:action", async (c) => {
|
|
3815
|
+
const resolved = await resolveAction(c);
|
|
3816
|
+
if (resolved instanceof Response)
|
|
3817
|
+
return resolved;
|
|
3818
|
+
const { action, cwd, logId } = resolved;
|
|
3819
|
+
const result2 = await runner.exec(action.command, { cwd });
|
|
3820
|
+
db.completeAction(logId, result2.exitCode);
|
|
3821
|
+
return c.json({
|
|
3822
|
+
logId,
|
|
3823
|
+
exitCode: result2.exitCode,
|
|
3824
|
+
output: result2.output,
|
|
3825
|
+
timedOut: result2.timedOut
|
|
3826
|
+
});
|
|
3827
|
+
});
|
|
3828
|
+
app.post("/api/actions/:module/:action/stream", async (c) => {
|
|
3829
|
+
const resolved = await resolveAction(c);
|
|
3830
|
+
if (resolved instanceof Response)
|
|
3831
|
+
return resolved;
|
|
3832
|
+
const { action, cwd, logId } = resolved;
|
|
3833
|
+
return new Response(new ReadableStream({
|
|
3834
|
+
async start(controller) {
|
|
3835
|
+
const encoder = new TextEncoder();
|
|
3836
|
+
const send = (event, data) => {
|
|
3837
|
+
controller.enqueue(encoder.encode(`event: ${event}
|
|
3838
|
+
data: ${JSON.stringify(data)}
|
|
3839
|
+
|
|
3840
|
+
`));
|
|
3841
|
+
};
|
|
3842
|
+
send("start", { logId, command: action.command });
|
|
3843
|
+
const result2 = await runner.exec(action.command, {
|
|
3844
|
+
cwd,
|
|
3845
|
+
onData: (chunk) => send("output", { chunk })
|
|
3846
|
+
});
|
|
3847
|
+
db.completeAction(logId, result2.exitCode);
|
|
3848
|
+
send("done", { exitCode: result2.exitCode, timedOut: result2.timedOut });
|
|
3849
|
+
controller.close();
|
|
3850
|
+
}
|
|
3851
|
+
}), {
|
|
3852
|
+
headers: {
|
|
3853
|
+
"Content-Type": "text/event-stream",
|
|
3854
|
+
"Cache-Control": "no-cache",
|
|
3855
|
+
Connection: "keep-alive"
|
|
3856
|
+
}
|
|
3857
|
+
});
|
|
3858
|
+
});
|
|
3859
|
+
app.get("/api/action-logs", (c) => {
|
|
3860
|
+
const moduleId = c.req.query("moduleId");
|
|
3861
|
+
const limitStr = c.req.query("limit");
|
|
3862
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
3863
|
+
return c.json(db.listActionLogs({ moduleId, limit }));
|
|
3864
|
+
});
|
|
3865
|
+
app.get("/api/modules/:module/settings", (c) => {
|
|
3866
|
+
const moduleId = c.req.param("module");
|
|
3867
|
+
return c.json(db.getModuleSettings(moduleId));
|
|
3868
|
+
});
|
|
3869
|
+
app.put("/api/modules/:module/settings", async (c) => {
|
|
3870
|
+
const moduleId = c.req.param("module");
|
|
3871
|
+
const body = await c.req.json();
|
|
3872
|
+
for (const [key, value] of Object.entries(body)) {
|
|
3873
|
+
db.setModuleSetting(moduleId, key, String(value));
|
|
3874
|
+
}
|
|
3875
|
+
return c.json({ ok: true });
|
|
3876
|
+
});
|
|
3877
|
+
app.get("/api/registry/search", async (c) => {
|
|
3878
|
+
const q = c.req.query("q") ?? "forge-dev";
|
|
3879
|
+
try {
|
|
3880
|
+
const npmRes = await globalThis.fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}+keywords:forge-module&size=20`);
|
|
3881
|
+
const data = await npmRes.json();
|
|
3882
|
+
const results = (data.objects ?? []).map((o) => ({
|
|
3883
|
+
name: o.package.name,
|
|
3884
|
+
version: o.package.version,
|
|
3885
|
+
description: o.package.description
|
|
3886
|
+
}));
|
|
3887
|
+
return c.json({ results });
|
|
3888
|
+
} catch {
|
|
3889
|
+
return c.json({ results: [] });
|
|
3890
|
+
}
|
|
3891
|
+
});
|
|
3892
|
+
app.get("/api/filesystem/browse", (c) => {
|
|
3893
|
+
const home = process.env.HOME ?? "/tmp";
|
|
3894
|
+
const dir = resolve4(c.req.query("path") ?? process.cwd());
|
|
3895
|
+
if (!dir.startsWith(home)) {
|
|
3896
|
+
return c.json({ error: "Access denied: path must be within home directory" }, 403);
|
|
3897
|
+
}
|
|
3898
|
+
if (!existsSync10(dir)) {
|
|
3899
|
+
return c.json({ error: "Directory not found" }, 404);
|
|
3900
|
+
}
|
|
3901
|
+
try {
|
|
3902
|
+
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
3903
|
+
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({
|
|
3904
|
+
name: e.name,
|
|
3905
|
+
path: join12(dir, e.name),
|
|
3906
|
+
hasPackageJson: existsSync10(join12(dir, e.name, "package.json")),
|
|
3907
|
+
hasGit: existsSync10(join12(dir, e.name, ".git"))
|
|
3908
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
3909
|
+
return c.json({
|
|
3910
|
+
current: dir,
|
|
3911
|
+
name: basename3(dir),
|
|
3912
|
+
parent: join12(dir, ".."),
|
|
3913
|
+
directories: dirs
|
|
3914
|
+
});
|
|
3915
|
+
} catch {
|
|
3916
|
+
return c.json({ error: "Cannot read directory" }, 500);
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3919
|
+
const fetch2 = (path, init) => {
|
|
3920
|
+
return app.request(path, init);
|
|
3921
|
+
};
|
|
3922
|
+
return {
|
|
3923
|
+
app,
|
|
3924
|
+
fetch: fetch2,
|
|
3925
|
+
attachTerminalWs: (server) => terminalWss.attachToServer(server),
|
|
3926
|
+
close: () => {
|
|
3927
|
+
ptyManager.dispose();
|
|
3928
|
+
states.dispose();
|
|
3929
|
+
loginManager.dispose();
|
|
3930
|
+
db.close();
|
|
3931
|
+
}
|
|
3932
|
+
};
|
|
3933
|
+
}
|
|
3934
|
+
var init_server = __esm({
|
|
3935
|
+
"../core/dist/server.js"() {
|
|
3936
|
+
"use strict";
|
|
3937
|
+
init_db();
|
|
3938
|
+
init_modules();
|
|
3939
|
+
init_runner();
|
|
3940
|
+
init_auth();
|
|
3941
|
+
init_origin_guard();
|
|
3942
|
+
init_cw_reader();
|
|
3943
|
+
init_cw_routes();
|
|
3944
|
+
init_skill_routes();
|
|
3945
|
+
init_pty_manager();
|
|
3946
|
+
init_session_state();
|
|
3947
|
+
init_state_log();
|
|
3948
|
+
init_pty_routes();
|
|
3949
|
+
init_login_manager();
|
|
3950
|
+
init_liveframe_routes();
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3953
|
+
|
|
3954
|
+
// ../core/dist/db-postgres.js
|
|
3955
|
+
var db_postgres_exports = {};
|
|
3956
|
+
__export(db_postgres_exports, {
|
|
3957
|
+
PostgresDB: () => PostgresDB
|
|
3958
|
+
});
|
|
3959
|
+
import postgres from "postgres";
|
|
3960
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3961
|
+
var PostgresDB;
|
|
3962
|
+
var init_db_postgres = __esm({
|
|
3963
|
+
"../core/dist/db-postgres.js"() {
|
|
3964
|
+
"use strict";
|
|
3965
|
+
PostgresDB = class {
|
|
3966
|
+
sql;
|
|
3967
|
+
constructor(connectionUrl) {
|
|
3968
|
+
this.sql = postgres(connectionUrl);
|
|
3969
|
+
}
|
|
3970
|
+
async migrate() {
|
|
3971
|
+
await this.sql`
|
|
3972
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
3973
|
+
id TEXT PRIMARY KEY,
|
|
3974
|
+
name TEXT NOT NULL UNIQUE,
|
|
3975
|
+
path TEXT NOT NULL,
|
|
3976
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
3977
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
3978
|
+
)
|
|
3979
|
+
`;
|
|
3980
|
+
await this.sql`
|
|
3981
|
+
CREATE TABLE IF NOT EXISTS modules (
|
|
3982
|
+
id TEXT PRIMARY KEY,
|
|
3983
|
+
name TEXT NOT NULL UNIQUE,
|
|
3984
|
+
version TEXT NOT NULL,
|
|
3985
|
+
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
3986
|
+
installed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
3987
|
+
)
|
|
3988
|
+
`;
|
|
3989
|
+
await this.sql`
|
|
3990
|
+
CREATE TABLE IF NOT EXISTS action_logs (
|
|
3991
|
+
id TEXT PRIMARY KEY,
|
|
3992
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
3993
|
+
module_id TEXT NOT NULL,
|
|
3994
|
+
action_id TEXT NOT NULL,
|
|
3995
|
+
command TEXT NOT NULL,
|
|
3996
|
+
exit_code INTEGER,
|
|
3997
|
+
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
3998
|
+
finished_at TIMESTAMPTZ
|
|
3999
|
+
)
|
|
4000
|
+
`;
|
|
4001
|
+
await this.sql`
|
|
4002
|
+
CREATE TABLE IF NOT EXISTS module_settings (
|
|
4003
|
+
module_id TEXT NOT NULL,
|
|
4004
|
+
key TEXT NOT NULL,
|
|
4005
|
+
value TEXT NOT NULL,
|
|
4006
|
+
PRIMARY KEY (module_id, key)
|
|
4007
|
+
)
|
|
4008
|
+
`;
|
|
4009
|
+
}
|
|
4010
|
+
listTables() {
|
|
4011
|
+
return ["projects", "modules", "action_logs", "module_settings"];
|
|
4012
|
+
}
|
|
4013
|
+
addProject(input) {
|
|
4014
|
+
const id = randomUUID2();
|
|
4015
|
+
this.sql`INSERT INTO projects (id, name, path) VALUES (${id}, ${input.name}, ${input.path})`.execute();
|
|
4016
|
+
return id;
|
|
4017
|
+
}
|
|
4018
|
+
listProjects() {
|
|
4019
|
+
throw new Error("PostgresDB.listProjects() not implemented \u2014 team mode is not yet supported");
|
|
4020
|
+
}
|
|
4021
|
+
getProject(_id) {
|
|
4022
|
+
throw new Error("PostgresDB.getProject() not implemented \u2014 team mode is not yet supported");
|
|
4023
|
+
}
|
|
4024
|
+
removeProject(id) {
|
|
4025
|
+
this.sql`DELETE FROM projects WHERE id = ${id}`.execute();
|
|
4026
|
+
}
|
|
4027
|
+
addModule(input) {
|
|
4028
|
+
const id = randomUUID2();
|
|
4029
|
+
this.sql`INSERT INTO modules (id, name, version) VALUES (${id}, ${input.name}, ${input.version})`.execute();
|
|
4030
|
+
return id;
|
|
4031
|
+
}
|
|
4032
|
+
listModules() {
|
|
4033
|
+
throw new Error("PostgresDB.listModules() not implemented \u2014 team mode is not yet supported");
|
|
4034
|
+
}
|
|
4035
|
+
removeModule(name) {
|
|
4036
|
+
this.sql`DELETE FROM modules WHERE name = ${name}`.execute();
|
|
4037
|
+
}
|
|
4038
|
+
logAction(input) {
|
|
4039
|
+
const id = randomUUID2();
|
|
4040
|
+
this.sql`INSERT INTO action_logs (id, project_id, module_id, action_id, command) VALUES (${id}, ${input.projectId}, ${input.moduleId}, ${input.actionId}, ${input.command})`.execute();
|
|
4041
|
+
return id;
|
|
4042
|
+
}
|
|
4043
|
+
getActionLog(_id) {
|
|
4044
|
+
throw new Error("PostgresDB.getActionLog() not implemented \u2014 team mode is not yet supported");
|
|
4045
|
+
}
|
|
4046
|
+
completeAction(id, exitCode) {
|
|
4047
|
+
this.sql`UPDATE action_logs SET exit_code = ${exitCode}, finished_at = now() WHERE id = ${id}`.execute();
|
|
4048
|
+
}
|
|
4049
|
+
getModuleSettings(_moduleId) {
|
|
4050
|
+
throw new Error("PostgresDB.getModuleSettings() not implemented \u2014 team mode is not yet supported");
|
|
4051
|
+
}
|
|
4052
|
+
setModuleSetting(moduleId, key, value) {
|
|
4053
|
+
this.sql`INSERT INTO module_settings (module_id, key, value) VALUES (${moduleId}, ${key}, ${value}) ON CONFLICT (module_id, key) DO UPDATE SET value = EXCLUDED.value`.execute();
|
|
4054
|
+
}
|
|
4055
|
+
listActionLogs(_opts) {
|
|
4056
|
+
throw new Error("PostgresDB.listActionLogs() not implemented \u2014 team mode is not yet supported");
|
|
4057
|
+
}
|
|
4058
|
+
close() {
|
|
4059
|
+
this.sql.end();
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
4062
|
+
}
|
|
4063
|
+
});
|
|
4064
|
+
|
|
4065
|
+
// ../core/dist/db-factory.js
|
|
4066
|
+
async function createDatabase(opts) {
|
|
4067
|
+
if (opts.mode === "team" && opts.databaseUrl) {
|
|
4068
|
+
const { PostgresDB: PostgresDB2 } = await Promise.resolve().then(() => (init_db_postgres(), db_postgres_exports));
|
|
4069
|
+
const db = new PostgresDB2(opts.databaseUrl);
|
|
4070
|
+
await db.migrate();
|
|
4071
|
+
return db;
|
|
4072
|
+
}
|
|
4073
|
+
const { join: join16 } = await import("node:path");
|
|
4074
|
+
return new ForgeDB(join16(opts.dataDir, "forge.db"));
|
|
4075
|
+
}
|
|
4076
|
+
var init_db_factory = __esm({
|
|
4077
|
+
"../core/dist/db-factory.js"() {
|
|
4078
|
+
"use strict";
|
|
4079
|
+
init_db();
|
|
4080
|
+
}
|
|
4081
|
+
});
|
|
4082
|
+
|
|
4083
|
+
// ../core/dist/init.js
|
|
4084
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4085
|
+
import { join as join13 } from "node:path";
|
|
4086
|
+
import { homedir as homedir5 } from "node:os";
|
|
4087
|
+
function getForgeDir() {
|
|
4088
|
+
return join13(homedir5(), ".forge");
|
|
4089
|
+
}
|
|
4090
|
+
function ensureForgeDir() {
|
|
4091
|
+
const forgeDir = getForgeDir();
|
|
4092
|
+
if (existsSync11(forgeDir)) {
|
|
4093
|
+
return { forgeDir, created: false };
|
|
4094
|
+
}
|
|
4095
|
+
mkdirSync4(forgeDir, { recursive: true });
|
|
4096
|
+
for (const d of FORGE_SUBDIRS) {
|
|
4097
|
+
mkdirSync4(join13(forgeDir, d), { recursive: true });
|
|
4098
|
+
}
|
|
4099
|
+
writeFileSync3(join13(forgeDir, "config.json"), JSON.stringify({
|
|
4100
|
+
port: 3e3,
|
|
4101
|
+
theme: "dark",
|
|
4102
|
+
openBrowser: true,
|
|
4103
|
+
dataDir: forgeDir
|
|
4104
|
+
}, null, 2));
|
|
4105
|
+
return { forgeDir, created: true };
|
|
4106
|
+
}
|
|
4107
|
+
var FORGE_SUBDIRS;
|
|
4108
|
+
var init_init = __esm({
|
|
4109
|
+
"../core/dist/init.js"() {
|
|
4110
|
+
"use strict";
|
|
4111
|
+
FORGE_SUBDIRS = ["modules", "templates", "cache", "logs"];
|
|
4112
|
+
}
|
|
4113
|
+
});
|
|
4114
|
+
|
|
4115
|
+
// ../core/dist/cw-install.js
|
|
4116
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
4117
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4118
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync12, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4119
|
+
import { delimiter as delimiter2, join as join14 } from "node:path";
|
|
4120
|
+
function readCwVersion(file) {
|
|
4121
|
+
try {
|
|
4122
|
+
const match = /^CW_VERSION="([^"]+)"/m.exec(readFileSync8(file, "utf-8"));
|
|
4123
|
+
return match ? match[1] : null;
|
|
4124
|
+
} catch {
|
|
4125
|
+
return null;
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
function compareVersions(a, b) {
|
|
4129
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
4130
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
4131
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
4132
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
4133
|
+
if (diff !== 0)
|
|
4134
|
+
return diff;
|
|
4135
|
+
}
|
|
4136
|
+
return 0;
|
|
4137
|
+
}
|
|
4138
|
+
function readCwBundle(dir) {
|
|
4139
|
+
try {
|
|
4140
|
+
const { ref } = JSON.parse(readFileSync8(join14(dir, ".bundle.json"), "utf-8"));
|
|
4141
|
+
const version = readCwVersion(join14(dir, "cw"));
|
|
4142
|
+
if (!ref || !version || !existsSync12(join14(dir, "install.sh")))
|
|
4143
|
+
return null;
|
|
4144
|
+
return { dir, version, ref };
|
|
4145
|
+
} catch {
|
|
4146
|
+
return null;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
function decideCwInstall({ bundle, installed, marker, cwOnPath, skip }) {
|
|
4150
|
+
if (skip)
|
|
4151
|
+
return { action: "skip", reason: "FORGE_SKIP_CW_INSTALL is set" };
|
|
4152
|
+
if (!bundle)
|
|
4153
|
+
return { action: "skip", reason: "this Forge carries no CW" };
|
|
4154
|
+
if (!installed) {
|
|
4155
|
+
return cwOnPath ? { action: "skip", reason: "using the cw found on PATH" } : { action: "install", reason: "CW is not installed" };
|
|
4156
|
+
}
|
|
4157
|
+
if (!installed.version)
|
|
4158
|
+
return { action: "skip", reason: "cannot read the installed CW version" };
|
|
4159
|
+
const newer = compareVersions(bundle.version, installed.version);
|
|
4160
|
+
const ownedByForge = marker !== null && marker.sha256 === installed.sha256;
|
|
4161
|
+
if (ownedByForge) {
|
|
4162
|
+
if (marker.ref === bundle.ref)
|
|
4163
|
+
return { action: "skip", reason: `CW ${installed.version} is current` };
|
|
4164
|
+
return newer >= 0 ? { action: "update", reason: `CW ${installed.version} \u2192 ${bundle.version}` } : { action: "skip", reason: `installed CW ${installed.version} is newer than ${bundle.version}` };
|
|
4165
|
+
}
|
|
4166
|
+
return newer > 0 ? { action: "update", reason: `CW ${installed.version} \u2192 ${bundle.version}` } : { action: "skip", reason: `CW ${installed.version} is current` };
|
|
4167
|
+
}
|
|
4168
|
+
function findOnPath(name, pathVar, excludeDir) {
|
|
4169
|
+
for (const dir of (pathVar ?? "").split(delimiter2)) {
|
|
4170
|
+
if (!dir || dir === excludeDir)
|
|
4171
|
+
continue;
|
|
4172
|
+
const candidate = join14(dir, name);
|
|
4173
|
+
try {
|
|
4174
|
+
if (!statSync3(candidate).isFile())
|
|
4175
|
+
continue;
|
|
4176
|
+
accessSync2(candidate, constants2.X_OK);
|
|
4177
|
+
return candidate;
|
|
4178
|
+
} catch {
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
return null;
|
|
4182
|
+
}
|
|
4183
|
+
function pathWithCw(cwHome, pathVar) {
|
|
4184
|
+
const bin = join14(cwHome, "bin");
|
|
4185
|
+
const rest = (pathVar ?? "").split(delimiter2).filter((dir) => dir && dir !== bin);
|
|
4186
|
+
return [bin, ...rest].join(delimiter2);
|
|
4187
|
+
}
|
|
4188
|
+
function readMarker(cwHome) {
|
|
4189
|
+
try {
|
|
4190
|
+
return JSON.parse(readFileSync8(join14(cwHome, MARKER_FILE), "utf-8"));
|
|
4191
|
+
} catch {
|
|
4192
|
+
return null;
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
function failureMessage2(output) {
|
|
4196
|
+
const lines2 = output.replace(ANSI_RE3, "").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4197
|
+
const errors = lines2.filter((l) => l.startsWith("[\u2717]")).map((l) => l.slice(3).trim());
|
|
4198
|
+
return (errors.length > 0 ? errors : lines2.slice(-3)).join(" ") || "the CW installer failed";
|
|
4199
|
+
}
|
|
4200
|
+
async function ensureCw(options) {
|
|
4201
|
+
const { cwHome, bundleDir } = options;
|
|
4202
|
+
const env = options.env ?? process.env;
|
|
4203
|
+
const run = options.run ?? runInstall;
|
|
4204
|
+
const bin = join14(cwHome, "bin", "cw");
|
|
4205
|
+
const bundle = readCwBundle(bundleDir);
|
|
4206
|
+
const installed = existsSync12(bin) ? { version: readCwVersion(bin), sha256: sha256Of(bin) } : null;
|
|
4207
|
+
const decision = decideCwInstall({
|
|
4208
|
+
bundle,
|
|
4209
|
+
installed,
|
|
4210
|
+
marker: readMarker(cwHome),
|
|
4211
|
+
cwOnPath: findOnPath("cw", env.PATH, join14(cwHome, "bin")) !== null,
|
|
4212
|
+
skip: env.FORGE_SKIP_CW_INSTALL === "1"
|
|
4213
|
+
});
|
|
4214
|
+
if (decision.action === "skip" || !bundle)
|
|
4215
|
+
return { action: "skipped", message: decision.reason };
|
|
4216
|
+
const { code, output } = await run(join14(bundle.dir, "install.sh"), ["--no-shell"], { ...env, CW_HOME: cwHome });
|
|
4217
|
+
if (code !== 0 || !existsSync12(bin))
|
|
4218
|
+
return { action: "failed", message: failureMessage2(output) };
|
|
4219
|
+
const marker = { version: bundle.version, ref: bundle.ref, sha256: sha256Of(bin) };
|
|
4220
|
+
writeFileSync4(join14(cwHome, MARKER_FILE), JSON.stringify(marker, null, 2) + "\n");
|
|
4221
|
+
return {
|
|
4222
|
+
action: decision.action === "install" ? "installed" : "updated",
|
|
4223
|
+
version: bundle.version,
|
|
4224
|
+
message: decision.reason
|
|
4225
|
+
};
|
|
4226
|
+
}
|
|
4227
|
+
var MARKER_FILE, INSTALL_TIMEOUT_MS2, ANSI_RE3, sha256Of, runInstall;
|
|
4228
|
+
var init_cw_install = __esm({
|
|
4229
|
+
"../core/dist/cw-install.js"() {
|
|
4230
|
+
"use strict";
|
|
4231
|
+
MARKER_FILE = ".forge-cw.json";
|
|
4232
|
+
INSTALL_TIMEOUT_MS2 = 12e4;
|
|
4233
|
+
ANSI_RE3 = /\u001b\[[0-9;?]*[A-Za-z]/g;
|
|
4234
|
+
sha256Of = (file) => createHash2("sha256").update(readFileSync8(file)).digest("hex");
|
|
4235
|
+
runInstall = (script, args, env) => new Promise((resolve5) => {
|
|
4236
|
+
execFile5("bash", [script, ...args], { env, timeout: INSTALL_TIMEOUT_MS2, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
4237
|
+
const output = `${String(stdout)}${String(stderr)}`;
|
|
4238
|
+
if (!err)
|
|
4239
|
+
return resolve5({ code: 0, output });
|
|
4240
|
+
const code = err.code;
|
|
4241
|
+
resolve5({ code: typeof code === "number" ? code : 1, output: output || err.message });
|
|
4242
|
+
});
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4245
|
+
});
|
|
4246
|
+
|
|
4247
|
+
// ../core/dist/index.js
|
|
4248
|
+
var dist_exports = {};
|
|
4249
|
+
__export(dist_exports, {
|
|
4250
|
+
ACCOUNT_NAME_RE: () => ACCOUNT_NAME_RE,
|
|
4251
|
+
ActionRunner: () => ActionRunner,
|
|
4252
|
+
CWReader: () => CWReader,
|
|
4253
|
+
ForgeDB: () => ForgeDB,
|
|
4254
|
+
HARNESS_CAPABILITIES: () => HARNESS_CAPABILITIES,
|
|
4255
|
+
HARNESS_NAME_RE: () => HARNESS_NAME_RE,
|
|
4256
|
+
Liveframe: () => Liveframe,
|
|
4257
|
+
LiveframeError: () => LiveframeError,
|
|
4258
|
+
MODEL_NAME_RE: () => MODEL_NAME_RE,
|
|
4259
|
+
ModuleLoader: () => ModuleLoader,
|
|
4260
|
+
PROVIDER_NAME_RE: () => PROVIDER_NAME_RE,
|
|
4261
|
+
PTYManager: () => PTYManager,
|
|
4262
|
+
bearerAuth: () => bearerAuth,
|
|
4263
|
+
createDatabase: () => createDatabase,
|
|
4264
|
+
createForgeServer: () => createForgeServer,
|
|
4265
|
+
createTerminalWss: () => createTerminalWss,
|
|
4266
|
+
cwRoutes: () => cwRoutes,
|
|
4267
|
+
ensureCw: () => ensureCw,
|
|
4268
|
+
ensureForgeDir: () => ensureForgeDir,
|
|
4269
|
+
getForgeDir: () => getForgeDir,
|
|
4270
|
+
liveframeRoutes: () => liveframeRoutes,
|
|
4271
|
+
pathWithCw: () => pathWithCw,
|
|
4272
|
+
quietSqliteWarning: () => quietSqliteWarning,
|
|
4273
|
+
resolveListenOptions: () => resolveListenOptions,
|
|
4274
|
+
skillRoutes: () => skillRoutes,
|
|
4275
|
+
supports: () => supports
|
|
4276
|
+
});
|
|
4277
|
+
var init_dist = __esm({
|
|
4278
|
+
"../core/dist/index.js"() {
|
|
4279
|
+
"use strict";
|
|
4280
|
+
init_server();
|
|
4281
|
+
init_db();
|
|
4282
|
+
init_modules();
|
|
4283
|
+
init_runner();
|
|
4284
|
+
init_auth();
|
|
4285
|
+
init_origin_guard();
|
|
4286
|
+
init_db_factory();
|
|
4287
|
+
init_init();
|
|
4288
|
+
init_cw_reader();
|
|
4289
|
+
init_cw_install();
|
|
4290
|
+
init_cw_routes();
|
|
4291
|
+
init_skill_routes();
|
|
4292
|
+
init_harness_capabilities();
|
|
4293
|
+
init_cw_types();
|
|
4294
|
+
init_pty_manager();
|
|
4295
|
+
init_pty_routes();
|
|
4296
|
+
init_liveframe_routes();
|
|
4297
|
+
init_liveframe();
|
|
4298
|
+
}
|
|
4299
|
+
});
|
|
4300
|
+
|
|
4301
|
+
// src/index.ts
|
|
4302
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
4303
|
+
import { homedir as homedir6 } from "node:os";
|
|
4304
|
+
import { join as join15 } from "node:path";
|
|
4305
|
+
function parseArgs(argv) {
|
|
4306
|
+
const out = { open: true };
|
|
4307
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4308
|
+
const arg = argv[i];
|
|
4309
|
+
if (arg === "--port" || arg === "-p") out.port = argv[++i];
|
|
4310
|
+
else if (arg.startsWith("--port=")) out.port = arg.slice("--port=".length);
|
|
4311
|
+
else if (arg === "--no-open") out.open = false;
|
|
4312
|
+
}
|
|
4313
|
+
return out;
|
|
4314
|
+
}
|
|
4315
|
+
var packageRoot = join15(import.meta.dirname, "..");
|
|
4316
|
+
var isCheckout = existsSync13(join15(packageRoot, "src"));
|
|
4317
|
+
var consoleDist = isCheckout ? join15(packageRoot, "../console/dist") : join15(packageRoot, "console");
|
|
4318
|
+
async function main() {
|
|
4319
|
+
const { ensureForgeDir: ensureForgeDir2, createForgeServer: createForgeServer2, createDatabase: createDatabase2, resolveListenOptions: resolveListenOptions2, ensureCw: ensureCw2, pathWithCw: pathWithCw2, quietSqliteWarning: quietSqliteWarning2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
4320
|
+
quietSqliteWarning2();
|
|
4321
|
+
const args = parseArgs(process.argv.slice(2));
|
|
4322
|
+
const { forgeDir, created } = ensureForgeDir2();
|
|
4323
|
+
if (created) {
|
|
4324
|
+
console.log("First run \u2014 initialized Forge.");
|
|
4325
|
+
}
|
|
4326
|
+
const cwHome = join15(process.env.HOME ?? homedir6(), ".cw");
|
|
4327
|
+
const cw = isCheckout ? { action: "skipped", message: "a checkout does not install CW" } : await ensureCw2({ cwHome, bundleDir: join15(packageRoot, "cw") });
|
|
4328
|
+
if (cw.action === "installed") console.log(` Installed CW ${cw.version} in ${cwHome}`);
|
|
4329
|
+
else if (cw.action === "updated") console.log(` Updated ${cw.message}`);
|
|
4330
|
+
else if (cw.action === "failed") console.log(` Could not install CW: ${cw.message}`);
|
|
4331
|
+
process.env.PATH = pathWithCw2(cwHome, process.env.PATH);
|
|
4332
|
+
const port = parseInt(args.port ?? process.env.FORGE_PORT ?? "3000", 10);
|
|
4333
|
+
const dbUrl = process.env.FORGE_DB_URL;
|
|
4334
|
+
const authToken = process.env.FORGE_AUTH_TOKEN;
|
|
4335
|
+
const isTeam = !!dbUrl;
|
|
4336
|
+
const { host, localOnly } = resolveListenOptions2(isTeam);
|
|
4337
|
+
const db = await createDatabase2({
|
|
4338
|
+
mode: isTeam ? "team" : "local",
|
|
4339
|
+
dataDir: forgeDir,
|
|
4340
|
+
databaseUrl: dbUrl
|
|
4341
|
+
});
|
|
4342
|
+
const server = createForgeServer2({
|
|
4343
|
+
dataDir: forgeDir,
|
|
4344
|
+
port,
|
|
4345
|
+
db,
|
|
4346
|
+
authToken: isTeam ? authToken : void 0,
|
|
4347
|
+
localOnly
|
|
4348
|
+
});
|
|
4349
|
+
const { serveStatic } = await import("@hono/node-server/serve-static");
|
|
4350
|
+
const { readFileSync: readFileSync9 } = await import("node:fs");
|
|
4351
|
+
if (existsSync13(join15(consoleDist, "index.html"))) {
|
|
4352
|
+
server.app.use("/*", serveStatic({ root: consoleDist }));
|
|
4353
|
+
const indexHtml = readFileSync9(join15(consoleDist, "index.html"), "utf-8");
|
|
4354
|
+
server.app.get("*", (c) => c.html(indexHtml));
|
|
4355
|
+
}
|
|
4356
|
+
const { serve } = await import("@hono/node-server");
|
|
4357
|
+
const httpServer = serve({ fetch: server.app.fetch, port, hostname: host });
|
|
4358
|
+
server.attachTerminalWs(httpServer);
|
|
4359
|
+
console.log(`
|
|
4360
|
+
Forge Console running at http://localhost:${port}
|
|
4361
|
+
Mode: ${isTeam ? "team (PostgreSQL)" : "local (SQLite)"}
|
|
4362
|
+
${authToken ? " Auth: bearer token required\n" : ""}
|
|
4363
|
+
Dashboard: http://localhost:${port}
|
|
4364
|
+
API: http://localhost:${port}/api/health
|
|
4365
|
+
|
|
4366
|
+
Press Ctrl+C to stop
|
|
4367
|
+
`);
|
|
4368
|
+
if (args.open && process.env.FORGE_NO_OPEN !== "1") {
|
|
4369
|
+
try {
|
|
4370
|
+
const open = (await import("open")).default;
|
|
4371
|
+
await open(`http://localhost:${port}`);
|
|
4372
|
+
} catch {
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
main().catch(console.error);
|