clauder-hub 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/server/dist/index.js +265 -20
- package/web/dist/assets/{MonacoEditor-Bdblyy-v.js → MonacoEditor-7a1aZ6Pf.js} +3 -3
- package/web/dist/assets/{cssMode-DOakqY50.js → cssMode-D42SG8q2.js} +1 -1
- package/web/dist/assets/{freemarker2-WsMzPf-p.js → freemarker2-HuLVqAyk.js} +1 -1
- package/web/dist/assets/{handlebars-CFf3afIj.js → handlebars-CQbtHmDj.js} +1 -1
- package/web/dist/assets/{html-Duk6L3Ci.js → html-DCQkaSoP.js} +1 -1
- package/web/dist/assets/{htmlMode-CuaMCYCU.js → htmlMode-Occ0oux7.js} +1 -1
- package/web/dist/assets/{index-DpS67tuQ.js → index-382ZKQ6T.js} +132 -127
- package/web/dist/assets/{index-CGbzqzmX.js → index-DB6lAoRD.js} +1 -1
- package/web/dist/assets/index-YQ0zZelJ.css +32 -0
- package/web/dist/assets/{javascript-57wNn1QZ.js → javascript-DkkoffJP.js} +1 -1
- package/web/dist/assets/{jsonMode-DPgMOUkd.js → jsonMode-CvTmblXx.js} +1 -1
- package/web/dist/assets/{liquid-DHlnUOel.js → liquid-DhsiR38F.js} +1 -1
- package/web/dist/assets/{lspLanguageFeatures-BIy_BVAi.js → lspLanguageFeatures-lfpHn74b.js} +1 -1
- package/web/dist/assets/{mdx-SCDblWWe.js → mdx-LuMf9AxQ.js} +1 -1
- package/web/dist/assets/{python-DcjgL-6k.js → python-BkVjahJB.js} +1 -1
- package/web/dist/assets/{razor-q_Zkc8t3.js → razor-CoVA4mXW.js} +1 -1
- package/web/dist/assets/{tsMode-N3PGleJF.js → tsMode-C3jqF6o4.js} +1 -1
- package/web/dist/assets/{typescript-UQxkBsMs.js → typescript-DzEhu0NR.js} +1 -1
- package/web/dist/assets/{workers-BVzxuZVI.js → workers-DO15KvMQ.js} +1 -1
- package/web/dist/assets/{xml-BDNQC6ch.js → xml-Dnv-hWWk.js} +1 -1
- package/web/dist/assets/{yaml-FyKAiCgl.js → yaml-hUd7D5AP.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DI4uTQnz.css +0 -32
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Clauder Hub runs the real `claude` you already use, in real terminals, and adds
|
|
|
59
59
|
<tr>
|
|
60
60
|
<td>
|
|
61
61
|
<h3>Answer from your phone</h3>
|
|
62
|
-
Telegram and Slack bots relay every question with inline buttons, and BotFather-style commands (<code>/new</code>, <code>/sessions</code>, <code>/summary</code>, <code>/stop</code>) drive sessions from chat. No public URL needed.
|
|
62
|
+
Telegram and Slack bots relay every question with inline buttons, and BotFather-style commands (<code>/new</code>, <code>/jobs</code>, <code>/sessions</code>, <code>/summary</code>, <code>/stop</code>) drive sessions from chat. No public URL needed.
|
|
63
63
|
</td>
|
|
64
64
|
<td>
|
|
65
65
|
<picture>
|
package/package.json
CHANGED
package/server/dist/index.js
CHANGED
|
@@ -27214,6 +27214,8 @@ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status, created_at);
|
|
|
27214
27214
|
ensureColumn("sessions", "setup", "TEXT");
|
|
27215
27215
|
ensureColumn("channel_refs", "project_id", "TEXT");
|
|
27216
27216
|
ensureColumn("channel_threads", "project_id", "TEXT");
|
|
27217
|
+
ensureColumn("runs", "resume", "INTEGER NOT NULL DEFAULT 0");
|
|
27218
|
+
ensureColumn("runs", "queued_at", "TEXT");
|
|
27217
27219
|
}
|
|
27218
27220
|
function now() {
|
|
27219
27221
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -28558,14 +28560,14 @@ function claudeSettings() {
|
|
|
28558
28560
|
}
|
|
28559
28561
|
function saveClaudeSettings(input) {
|
|
28560
28562
|
const next = { ...claudeSettings(), ...input };
|
|
28561
|
-
const
|
|
28563
|
+
const check3 = (ok, message) => {
|
|
28562
28564
|
if (!ok) throw new Error(message);
|
|
28563
28565
|
};
|
|
28564
|
-
|
|
28565
|
-
|
|
28566
|
-
|
|
28567
|
-
|
|
28568
|
-
|
|
28566
|
+
check3(!next.permissionMode || PERMISSION_MODES.includes(next.permissionMode), `Unknown permission mode '${next.permissionMode}'.`);
|
|
28567
|
+
check3(!next.runPermissionMode || PERMISSION_MODES.includes(next.runPermissionMode), `Unknown permission mode '${next.runPermissionMode}'.`);
|
|
28568
|
+
check3(!next.effort || EFFORTS.includes(next.effort), `Effort is one of ${EFFORTS.join(", ")}.`);
|
|
28569
|
+
check3(word.test(next.model) && word.test(next.fallbackModel), "A model is one word, like opus or claude-sonnet-5-5.");
|
|
28570
|
+
check3(["", "on", "off"].includes(next.chrome), "Claude in Chrome is on, off or your own setting.");
|
|
28569
28571
|
next.addDirs = (Array.isArray(next.addDirs) ? next.addDirs : []).map((dir) => String(dir).trim()).filter(Boolean);
|
|
28570
28572
|
db.prepare("INSERT INTO settings (key, value, updated_at) VALUES ('claude', ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at").run(
|
|
28571
28573
|
JSON.stringify(next),
|
|
@@ -28743,6 +28745,14 @@ If the category was given as "auto", choose the one that fits the task best. Do
|
|
|
28743
28745
|
|
|
28744
28746
|
Finish what is missing, then send the done block again.`
|
|
28745
28747
|
},
|
|
28748
|
+
{
|
|
28749
|
+
key: "typed.resumeRun",
|
|
28750
|
+
label: "First message of a resumed run",
|
|
28751
|
+
audience: "claude-typed",
|
|
28752
|
+
description: "Typed into a stopped run's own session when it is resumed from the queue (Resume on the Automation page, /resume on a channel).",
|
|
28753
|
+
placeholders: [],
|
|
28754
|
+
text: "You were stopped part-way through this task. Check what is already done, then carry on from where you left off, without redoing finished work."
|
|
28755
|
+
},
|
|
28746
28756
|
{
|
|
28747
28757
|
key: "typed.addMcp",
|
|
28748
28758
|
label: "First message of an Add MCP server task",
|
|
@@ -28893,6 +28903,8 @@ Reply *install* to run it (project scope) and resume the session, or *skip* to c
|
|
|
28893
28903
|
|
|
28894
28904
|
{{p}}new - start a session (with a prompt, or I ask for one)
|
|
28895
28905
|
{{p}}run - queue an unattended run
|
|
28906
|
+
{{p}}jobs - the saved jobs, to run one ({{p}}job <name> runs one by name)
|
|
28907
|
+
{{p}}resume - carry on a stopped run
|
|
28896
28908
|
{{p}}sessions - the sessions, to pick one
|
|
28897
28909
|
{{p}}summary - what a session did
|
|
28898
28910
|
{{p}}stop - stop a running session
|
|
@@ -28947,7 +28959,7 @@ Reply to this message to talk to the session. Its questions come here too.`
|
|
|
28947
28959
|
{{p}}projects - the projects, to pick one
|
|
28948
28960
|
{{p}}use <project> - work on a project
|
|
28949
28961
|
{{p}}addproject <words> - add a project: describe it, I find the folder
|
|
28950
|
-
Then, on that project: {{p}}new, {{p}}run, {{p}}sessions, {{p}}summary, {{p}}stop, {{p}}queue, {{p}}cancel.
|
|
28962
|
+
Then, on that project: {{p}}new, {{p}}run, {{p}}jobs, {{p}}resume, {{p}}sessions, {{p}}summary, {{p}}stop, {{p}}queue, {{p}}cancel.
|
|
28951
28963
|
|
|
28952
28964
|
Working on: {{current}}`
|
|
28953
28965
|
},
|
|
@@ -28967,13 +28979,29 @@ Working on: {{current}}`
|
|
|
28967
28979
|
placeholders: ["request", "p"],
|
|
28968
28980
|
text: 'I found no project folder that looks like "{{request}}". Describe it another way (its name, what it does), or send its full path. Or {{p}}cancel.'
|
|
28969
28981
|
},
|
|
28982
|
+
{
|
|
28983
|
+
key: "channel.jobConfirm",
|
|
28984
|
+
label: "Channel command: confirming a job run",
|
|
28985
|
+
audience: "channel",
|
|
28986
|
+
description: "Asked before a saved job is queued from the channel ({{p}}job, or a button from {{p}}jobs).",
|
|
28987
|
+
placeholders: ["job", "prompt"],
|
|
28988
|
+
text: "Run {{job}} now? It joins the queue as an unattended run; its questions and its result come here.\n\n{{prompt}}"
|
|
28989
|
+
},
|
|
28990
|
+
{
|
|
28991
|
+
key: "channel.resumeConfirm",
|
|
28992
|
+
label: "Channel command: confirming a resume",
|
|
28993
|
+
audience: "channel",
|
|
28994
|
+
description: "Asked before a stopped run is queued again to carry on its own session ({{p}}resume).",
|
|
28995
|
+
placeholders: ["run"],
|
|
28996
|
+
text: "Resume {{run}}? It joins the back of the queue and carries on its own conversation from where it stopped."
|
|
28997
|
+
},
|
|
28970
28998
|
{
|
|
28971
28999
|
key: "channel.stopConfirm",
|
|
28972
29000
|
label: "Channel command: confirming a stop",
|
|
28973
29001
|
audience: "channel",
|
|
28974
29002
|
description: "Asked before a session is stopped from the channel.",
|
|
28975
29003
|
placeholders: ["session"],
|
|
28976
|
-
text: "Stop {{session}}? Its terminal closes; the conversation is kept and can be resumed from the dashboard."
|
|
29004
|
+
text: "Stop {{session}}? Its terminal closes; the conversation is kept and can be resumed from the dashboard, or with resume for a run."
|
|
28977
29005
|
}
|
|
28978
29006
|
];
|
|
28979
29007
|
}
|
|
@@ -29074,6 +29102,7 @@ function spawnTerminal(input) {
|
|
|
29074
29102
|
clients: /* @__PURE__ */ new Set(),
|
|
29075
29103
|
backlog: [],
|
|
29076
29104
|
backlogSize: 0,
|
|
29105
|
+
lastOutputAt: Date.now(),
|
|
29077
29106
|
cols,
|
|
29078
29107
|
rows
|
|
29079
29108
|
};
|
|
@@ -29081,6 +29110,7 @@ function spawnTerminal(input) {
|
|
|
29081
29110
|
child.onData(bindProject((data) => {
|
|
29082
29111
|
terminal.backlog.push(data);
|
|
29083
29112
|
terminal.backlogSize += data.length;
|
|
29113
|
+
terminal.lastOutputAt = Date.now();
|
|
29084
29114
|
while (terminal.backlogSize > BACKLOG_LIMIT && terminal.backlog.length > 1) {
|
|
29085
29115
|
terminal.backlogSize -= terminal.backlog.shift().length;
|
|
29086
29116
|
}
|
|
@@ -29167,11 +29197,11 @@ function killTerminal(terminal) {
|
|
|
29167
29197
|
function waitForExit(terminal, timeoutMs = 1e4) {
|
|
29168
29198
|
return new Promise((resolve) => {
|
|
29169
29199
|
const started = Date.now();
|
|
29170
|
-
const
|
|
29200
|
+
const check3 = () => {
|
|
29171
29201
|
if (!terminals.has(terminal.id) || Date.now() - started > timeoutMs) return resolve();
|
|
29172
|
-
setTimeout(
|
|
29202
|
+
setTimeout(check3, 100);
|
|
29173
29203
|
};
|
|
29174
|
-
|
|
29204
|
+
check3();
|
|
29175
29205
|
});
|
|
29176
29206
|
}
|
|
29177
29207
|
function killAll() {
|
|
@@ -30365,6 +30395,23 @@ function queueFromChannel(prompt) {
|
|
|
30365
30395
|
const { position } = enqueue({ source: "manual", prompt, category: "auto" });
|
|
30366
30396
|
return { title: "\u{1F5C2} Run queued", lines: [renderPrompt("channel.queued", { position: String(position) })], buttons: [[{ label: "\u{1F5C2} Queue", command: "queue" }]] };
|
|
30367
30397
|
}
|
|
30398
|
+
function findJob(arg) {
|
|
30399
|
+
const wanted = arg.trim().toLowerCase();
|
|
30400
|
+
if (!wanted) return null;
|
|
30401
|
+
const jobs = listJobs();
|
|
30402
|
+
const exact = getJob(arg.trim()) ?? jobs.find((job) => job.name.toLowerCase() === wanted);
|
|
30403
|
+
if (exact) return exact;
|
|
30404
|
+
const matches = jobs.filter((job) => job.name.toLowerCase().includes(wanted));
|
|
30405
|
+
return matches.length === 1 ? matches[0] : null;
|
|
30406
|
+
}
|
|
30407
|
+
function jobPicker(title, jobs, empty) {
|
|
30408
|
+
if (!jobs.length) return empty;
|
|
30409
|
+
return {
|
|
30410
|
+
title,
|
|
30411
|
+
lines: jobs.slice(0, 15).map((job, index) => `${index + 1}. ${job.name} \xB7 ${job.kind === "cron" ? `cron ${job.schedule}` : "manual / webhook"}${job.enabled ? "" : " \xB7 disabled"}`),
|
|
30412
|
+
buttons: jobs.slice(0, 15).map((job, index) => [{ label: short(`\u25B6\uFE0F ${index + 1}. ${job.name}`, 60), command: `job ${job.id}` }])
|
|
30413
|
+
};
|
|
30414
|
+
}
|
|
30368
30415
|
function queueCard() {
|
|
30369
30416
|
const runs = listRuns(50).filter((run4) => run4.status === "queued" || run4.status === "running");
|
|
30370
30417
|
if (!runs.length) return "The queue is empty.";
|
|
@@ -30450,6 +30497,58 @@ async function runCommand(channelId, input, from, options = {}) {
|
|
|
30450
30497
|
if (arg) return name === "new" ? startFromChannel(channelId, arg, from, options) : queueFromChannel(arg);
|
|
30451
30498
|
awaiting.set(key, name);
|
|
30452
30499
|
return { title: name === "new" ? "\u25B6\uFE0F New session" : "\u{1F5C2} New run", lines: [renderPrompt(name === "new" ? "channel.askNew" : "channel.askRun", { p: p2 })], buttons: [[{ label: "Cancel", command: "cancel" }]] };
|
|
30500
|
+
case "jobs":
|
|
30501
|
+
return jobPicker("\u2699\uFE0F Jobs: pick one to run", listJobs(), "No jobs yet. Create one on the Automation page.");
|
|
30502
|
+
case "job": {
|
|
30503
|
+
if (!arg) return jobPicker("\u2699\uFE0F Run which job?", listJobs(), "No jobs yet. Create one on the Automation page.");
|
|
30504
|
+
const job = findJob(arg);
|
|
30505
|
+
if (!job) {
|
|
30506
|
+
const like = listJobs().filter((item) => item.name.toLowerCase().includes(arg.toLowerCase()));
|
|
30507
|
+
return jobPicker(`\u{1F914} Which job did you mean?`, like.length ? like : listJobs(), `No job like "${short(arg, 60)}". ${p2}jobs lists them.`);
|
|
30508
|
+
}
|
|
30509
|
+
return {
|
|
30510
|
+
title: "\u2699\uFE0F Run the job?",
|
|
30511
|
+
lines: [renderPrompt("channel.jobConfirm", { job: job.name, prompt: short(flat(job.prompt), 300) })],
|
|
30512
|
+
buttons: [
|
|
30513
|
+
[
|
|
30514
|
+
{ label: "Yes, run it", command: `job! ${job.id}`, tone: "primary" },
|
|
30515
|
+
{ label: "No", command: "cancel" }
|
|
30516
|
+
]
|
|
30517
|
+
]
|
|
30518
|
+
};
|
|
30519
|
+
}
|
|
30520
|
+
case "job!": {
|
|
30521
|
+
const job = getJob(arg);
|
|
30522
|
+
if (!job) return `That job is gone. ${p2}jobs lists them.`;
|
|
30523
|
+
const { position } = enqueue({ job, source: "manual" });
|
|
30524
|
+
return { title: `\u{1F5C2} ${job.name} queued`, lines: [renderPrompt("channel.queued", { position: String(position) })], buttons: [[{ label: "\u{1F5C2} Queue", command: "queue" }]] };
|
|
30525
|
+
}
|
|
30526
|
+
case "resume": {
|
|
30527
|
+
const runs = resumable();
|
|
30528
|
+
const run4 = arg ? runs.find((item) => item.id === arg) ?? runs[Number(arg) - 1] : void 0;
|
|
30529
|
+
if (!run4) {
|
|
30530
|
+
if (!runs.length) return "No stopped run to resume.";
|
|
30531
|
+
return {
|
|
30532
|
+
title: "\u23EF Resume which run?",
|
|
30533
|
+
lines: runs.map((item, index) => `${index + 1}. ${short(flat(item.jobName ?? item.prompt), 80)} \xB7 stopped ${item.finishedAt ? new Date(item.finishedAt).toLocaleString() : ""}`),
|
|
30534
|
+
buttons: runs.map((item, index) => [{ label: short(`\u23EF ${index + 1}. ${flat(item.jobName ?? item.prompt)}`, 60), command: `resume ${item.id}` }])
|
|
30535
|
+
};
|
|
30536
|
+
}
|
|
30537
|
+
return {
|
|
30538
|
+
title: "\u23EF Resume the run?",
|
|
30539
|
+
lines: [renderPrompt("channel.resumeConfirm", { run: flat(run4.jobName ?? short(run4.prompt, 80)) })],
|
|
30540
|
+
buttons: [
|
|
30541
|
+
[
|
|
30542
|
+
{ label: "Yes, resume it", command: `resume! ${run4.id}`, tone: "primary" },
|
|
30543
|
+
{ label: "No", command: "cancel" }
|
|
30544
|
+
]
|
|
30545
|
+
]
|
|
30546
|
+
};
|
|
30547
|
+
}
|
|
30548
|
+
case "resume!": {
|
|
30549
|
+
const { run: run4, position } = resumeRun(arg);
|
|
30550
|
+
return { title: `\u23EF ${flat(run4.jobName ?? short(run4.prompt, 60))} queued again`, lines: [renderPrompt("channel.queued", { position: String(position) })], buttons: [[{ label: "\u{1F5C2} Queue", command: "queue" }]] };
|
|
30551
|
+
}
|
|
30453
30552
|
case "cancel":
|
|
30454
30553
|
suggested.delete(suggestKey(channelId, from));
|
|
30455
30554
|
return focused.delete(key) ? "Cancelled. Plain messages go nowhere until you pick a session." : "Cancelled.";
|
|
@@ -30509,7 +30608,7 @@ async function runCommand(channelId, input, from, options = {}) {
|
|
|
30509
30608
|
return await interpret(channelId, from, input, options) ?? `I don't know ${p2}${name}. ${p2}help lists what I can do.`;
|
|
30510
30609
|
}
|
|
30511
30610
|
}
|
|
30512
|
-
var COMMAND, awaiting, focused, FOCUS_IDLE_MS, numbered, keyOf, HUB_COMMANDS, short, flat, STATUS, suggested, suggestKey, sessionLine, COMMAND_GUIDE, GUESSABLE, GLOBAL_COMMAND_MENU, COMMAND_MENU;
|
|
30611
|
+
var COMMAND, awaiting, focused, FOCUS_IDLE_MS, numbered, keyOf, HUB_COMMANDS, short, flat, STATUS, resumable, suggested, suggestKey, sessionLine, COMMAND_GUIDE, GUESSABLE, GLOBAL_COMMAND_MENU, COMMAND_MENU;
|
|
30513
30612
|
var init_commands = __esm({
|
|
30514
30613
|
"server/src/channels/commands.ts"() {
|
|
30515
30614
|
"use strict";
|
|
@@ -30527,16 +30626,20 @@ var init_commands = __esm({
|
|
|
30527
30626
|
FOCUS_IDLE_MS = 60 * 6e4;
|
|
30528
30627
|
numbered = /* @__PURE__ */ new Map();
|
|
30529
30628
|
keyOf = (channelId) => `${currentProject().id}:${channelId}`;
|
|
30530
|
-
HUB_COMMANDS = /* @__PURE__ */ new Set(["start", "help", "new", "run", "cancel", "leave", "sessions", "session", "summary", "stop", "stop!", "queue", "cancelrun"]);
|
|
30629
|
+
HUB_COMMANDS = /* @__PURE__ */ new Set(["start", "help", "new", "run", "jobs", "job", "job!", "resume", "resume!", "cancel", "leave", "sessions", "session", "summary", "stop", "stop!", "queue", "cancelrun"]);
|
|
30531
30630
|
short = (text, length) => text.length > length ? `${text.slice(0, length - 1)}\u2026` : text;
|
|
30532
30631
|
flat = (text) => text.replace(/\s+/g, " ").trim();
|
|
30533
30632
|
STATUS = { waiting: "\u23F3 waiting for you", running: "\u25B6\uFE0F running", done: "\u2705 done", exited: "\u23F9 stopped", external: "outside the hub" };
|
|
30633
|
+
resumable = () => listRuns(50).filter((run4) => run4.sessionId && run4.status === "exited").slice(0, 10);
|
|
30534
30634
|
suggested = /* @__PURE__ */ new Map();
|
|
30535
30635
|
suggestKey = (channelId, from) => `${keyOf(channelId)}:${from}`;
|
|
30536
30636
|
sessionLine = (session, index) => `${index + 1}. ${session.title} \xB7 ${STATUS[session.status] ?? session.status}${session.firstPrompt ? ` \xB7 asked: ${short(flat(session.firstPrompt), 120)}` : ""}`;
|
|
30537
30637
|
COMMAND_GUIDE = [
|
|
30538
30638
|
"new <what the session should do> - start a session",
|
|
30539
30639
|
"run <what to do> - queue an unattended run",
|
|
30640
|
+
"jobs - list the saved jobs, to run one",
|
|
30641
|
+
"job <name> - run a saved job now (asks first)",
|
|
30642
|
+
"resume - carry on a stopped run (asks first)",
|
|
30540
30643
|
"sessions - list the sessions",
|
|
30541
30644
|
"session <number> - pick a session, to talk to it",
|
|
30542
30645
|
"summary <number> - what a session did",
|
|
@@ -30544,13 +30647,15 @@ var init_commands = __esm({
|
|
|
30544
30647
|
"queue - the run queue",
|
|
30545
30648
|
"help - the commands"
|
|
30546
30649
|
].join("\n");
|
|
30547
|
-
GUESSABLE = /* @__PURE__ */ new Set(["new", "run", "sessions", "session", "summary", "stop", "queue", "help"]);
|
|
30650
|
+
GUESSABLE = /* @__PURE__ */ new Set(["new", "run", "jobs", "job", "resume", "sessions", "session", "summary", "stop", "queue", "help"]);
|
|
30548
30651
|
GLOBAL_COMMAND_MENU = [
|
|
30549
30652
|
{ command: "projects", description: "Pick a project" },
|
|
30550
30653
|
{ command: "use", description: "Work on a project: /use <name>" },
|
|
30551
30654
|
{ command: "addproject", description: "Add a project: describe it, I find it" },
|
|
30552
30655
|
{ command: "new", description: "Start a session in the picked project" },
|
|
30553
30656
|
{ command: "run", description: "Queue a run in the picked project" },
|
|
30657
|
+
{ command: "jobs", description: "The picked project's jobs, to run one" },
|
|
30658
|
+
{ command: "resume", description: "Carry on a stopped run in the picked project" },
|
|
30554
30659
|
{ command: "sessions", description: "The picked project's sessions" },
|
|
30555
30660
|
{ command: "summary", description: "What a session did" },
|
|
30556
30661
|
{ command: "stop", description: "Stop a running session" },
|
|
@@ -30561,6 +30666,8 @@ var init_commands = __esm({
|
|
|
30561
30666
|
COMMAND_MENU = [
|
|
30562
30667
|
{ command: "new", description: "Start a session" },
|
|
30563
30668
|
{ command: "run", description: "Queue an unattended run" },
|
|
30669
|
+
{ command: "jobs", description: "Run a saved job" },
|
|
30670
|
+
{ command: "resume", description: "Carry on a stopped run" },
|
|
30564
30671
|
{ command: "sessions", description: "Pick a session" },
|
|
30565
30672
|
{ command: "summary", description: "What a session did" },
|
|
30566
30673
|
{ command: "stop", description: "Stop a running session" },
|
|
@@ -31171,6 +31278,113 @@ var init_telegram = __esm({
|
|
|
31171
31278
|
}
|
|
31172
31279
|
});
|
|
31173
31280
|
|
|
31281
|
+
// server/src/watchdog.ts
|
|
31282
|
+
var watchdog_exports = {};
|
|
31283
|
+
__export(watchdog_exports, {
|
|
31284
|
+
answerTrust: () => answerTrust,
|
|
31285
|
+
startWatchdog: () => startWatchdog
|
|
31286
|
+
});
|
|
31287
|
+
function screenTail(terminal) {
|
|
31288
|
+
let text = "";
|
|
31289
|
+
for (let index = terminal.backlog.length - 1; index >= 0 && text.length < SCREEN_TAIL * 2; index -= 1) text = terminal.backlog[index] + text;
|
|
31290
|
+
return text.slice(-SCREEN_TAIL * 2).replace(ANSI, "").slice(-SCREEN_TAIL);
|
|
31291
|
+
}
|
|
31292
|
+
async function answerTrust(terminal, trust) {
|
|
31293
|
+
if (!trust) return pressKeys(terminal, "\x1B");
|
|
31294
|
+
for (let step = 0; step < 4; step += 1) {
|
|
31295
|
+
const flat2 = squeeze(screenTail(terminal).slice(-1500));
|
|
31296
|
+
const cursor = flat2.lastIndexOf("\u276F");
|
|
31297
|
+
const after = cursor >= 0 ? flat2.slice(cursor + 1) : "";
|
|
31298
|
+
if (after.startsWith("yes")) return pressKeys(terminal, "\r");
|
|
31299
|
+
const yesAt = flat2.lastIndexOf("yes,itrust");
|
|
31300
|
+
await pressKeys(terminal, yesAt > cursor ? "\x1B[B" : "\x1B[A");
|
|
31301
|
+
await sleep2(300);
|
|
31302
|
+
}
|
|
31303
|
+
throw new Error("Could not find the trust option on the screen.");
|
|
31304
|
+
}
|
|
31305
|
+
function lastLines(screen) {
|
|
31306
|
+
const lines2 = screen.split(/\r?\n/).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
31307
|
+
return lines2.slice(-14).join("\n").slice(-1200);
|
|
31308
|
+
}
|
|
31309
|
+
function check2() {
|
|
31310
|
+
for (const terminal of listTerminals()) {
|
|
31311
|
+
if (terminal.finished || terminal.restarting) continue;
|
|
31312
|
+
const open = listInteractions({ sessionId: terminal.sessionId, status: "open" });
|
|
31313
|
+
const screen = screenTail(terminal);
|
|
31314
|
+
const trustOpen = open.find((item) => item.data?.trust);
|
|
31315
|
+
if (showsTrustDialog(screen)) {
|
|
31316
|
+
if (!trustOpen) {
|
|
31317
|
+
createInteraction({
|
|
31318
|
+
sessionId: terminal.sessionId,
|
|
31319
|
+
kind: "question",
|
|
31320
|
+
title: "Trust this folder?",
|
|
31321
|
+
body: "Claude Code asks whether to trust the files in this project's folder before it starts. Nothing runs until it is answered.",
|
|
31322
|
+
options: [
|
|
31323
|
+
{ key: "1", label: "Yes, trust this folder" },
|
|
31324
|
+
{ key: "2", label: "No, exit" }
|
|
31325
|
+
],
|
|
31326
|
+
data: { trust: true, questions: [{ question: "Trust this folder?", options: ["Yes, trust this folder", "No, exit"] }] }
|
|
31327
|
+
});
|
|
31328
|
+
}
|
|
31329
|
+
continue;
|
|
31330
|
+
}
|
|
31331
|
+
if (trustOpen) closeOpen(terminal.sessionId, "terminal", ["question"]);
|
|
31332
|
+
const stalled = open.find((item) => item.data?.stalled);
|
|
31333
|
+
if (stalled && terminal.lastOutputAt > Date.parse(stalled.createdAt) + 5e3) {
|
|
31334
|
+
closeOpen(terminal.sessionId, "terminal", ["turn"]);
|
|
31335
|
+
setSessionStatus(terminal.sessionId, "running");
|
|
31336
|
+
continue;
|
|
31337
|
+
}
|
|
31338
|
+
const quietFor = Date.now() - terminal.lastOutputAt;
|
|
31339
|
+
if (quietFor < STALL_MS || open.length || reported.get(terminal.id) === terminal.lastOutputAt) continue;
|
|
31340
|
+
if (!getSessionRow(terminal.sessionId)?.run_id) continue;
|
|
31341
|
+
reported.set(terminal.id, terminal.lastOutputAt);
|
|
31342
|
+
createInteraction({
|
|
31343
|
+
sessionId: terminal.sessionId,
|
|
31344
|
+
kind: "turn",
|
|
31345
|
+
title: "This run looks stuck",
|
|
31346
|
+
body: `Nothing has changed on its screen for ${Math.round(quietFor / 6e4)} minutes. It may be waiting on a dialog, or it stopped without reporting done. Reply to type into it, or stop it.
|
|
31347
|
+
|
|
31348
|
+
${lastLines(screen)}`,
|
|
31349
|
+
data: { stalled: true }
|
|
31350
|
+
});
|
|
31351
|
+
}
|
|
31352
|
+
for (const id of reported.keys()) if (!allTerminals().some((terminal) => terminal.id === id)) reported.delete(id);
|
|
31353
|
+
}
|
|
31354
|
+
function startWatchdog(intervalMs = 3e3) {
|
|
31355
|
+
const timer = setInterval(bindProject(() => {
|
|
31356
|
+
try {
|
|
31357
|
+
check2();
|
|
31358
|
+
} catch (error) {
|
|
31359
|
+
console.error("[watchdog]", error.message);
|
|
31360
|
+
}
|
|
31361
|
+
}), intervalMs);
|
|
31362
|
+
return () => clearInterval(timer);
|
|
31363
|
+
}
|
|
31364
|
+
var TRUST_MARKERS, STALL_MS, SCREEN_TAIL, ANSI, squeeze, showsTrustDialog, sleep2, reported;
|
|
31365
|
+
var init_watchdog = __esm({
|
|
31366
|
+
"server/src/watchdog.ts"() {
|
|
31367
|
+
"use strict";
|
|
31368
|
+
init_context();
|
|
31369
|
+
init_interactions();
|
|
31370
|
+
init_sessions();
|
|
31371
|
+
init_terminals();
|
|
31372
|
+
TRUST_MARKERS = ["doyoutrustthefilesinthisfolder", "isthisaprojectyoucreatedoroneyoutrust", "yes,itrustthisfolder"];
|
|
31373
|
+
STALL_MS = Number(process.env.CLAUDER_HUB_STALL_MS) || 8 * 6e4;
|
|
31374
|
+
SCREEN_TAIL = 8e3;
|
|
31375
|
+
ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][A-Za-z0-9]|\x1b[=>78]/g;
|
|
31376
|
+
squeeze = (text) => text.toLowerCase().replace(/\s+/g, "");
|
|
31377
|
+
showsTrustDialog = (screen) => {
|
|
31378
|
+
const flat2 = squeeze(screen.slice(-3e3));
|
|
31379
|
+
if (!TRUST_MARKERS.some((marker) => flat2.includes(marker))) return false;
|
|
31380
|
+
const end = Math.max(flat2.lastIndexOf("entertoconfirm"), flat2.lastIndexOf("itrustthisfolder"), flat2.lastIndexOf("no,exit"));
|
|
31381
|
+
return end >= 0 && flat2.length - end < 120;
|
|
31382
|
+
};
|
|
31383
|
+
sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
31384
|
+
reported = /* @__PURE__ */ new Map();
|
|
31385
|
+
}
|
|
31386
|
+
});
|
|
31387
|
+
|
|
31174
31388
|
// server/src/mcpSuggestions.ts
|
|
31175
31389
|
var mcpSuggestions_exports = {};
|
|
31176
31390
|
__export(mcpSuggestions_exports, {
|
|
@@ -31395,6 +31609,11 @@ function closeOpen(sessionId, by, kinds) {
|
|
|
31395
31609
|
}
|
|
31396
31610
|
async function answerQuestion(interaction, answer) {
|
|
31397
31611
|
const terminal = terminalForSession(interaction.sessionId);
|
|
31612
|
+
if (interaction.data.trust) {
|
|
31613
|
+
const { answerTrust: answerTrust2 } = await Promise.resolve().then(() => (init_watchdog(), watchdog_exports));
|
|
31614
|
+
await answerTrust2(terminal, answer.trim() !== "2");
|
|
31615
|
+
return;
|
|
31616
|
+
}
|
|
31398
31617
|
const questions = interaction.data.questions ?? [];
|
|
31399
31618
|
const split = answer.includes("\n") ? answer.split("\n") : answer.split(/[;,]/);
|
|
31400
31619
|
const parts = questions.length > 1 ? split.map((part) => part.trim()) : [answer.trim()];
|
|
@@ -31596,7 +31815,7 @@ function bootGlobal() {
|
|
|
31596
31815
|
};
|
|
31597
31816
|
const stops = withProject(project, () => {
|
|
31598
31817
|
recoverRuns();
|
|
31599
|
-
const list = [startTranscriptWatcher(), startInteractionSweeper(), stopChannels];
|
|
31818
|
+
const list = [startTranscriptWatcher(), startInteractionSweeper(), startWatchdog(), stopChannels];
|
|
31600
31819
|
startChannels({ device: false });
|
|
31601
31820
|
return list;
|
|
31602
31821
|
});
|
|
@@ -31654,7 +31873,7 @@ function boot(row) {
|
|
|
31654
31873
|
withProject(project, () => {
|
|
31655
31874
|
const stranded = recoverRuns();
|
|
31656
31875
|
if (stranded) console.log(` note : ${row.name}: ${stranded} queued run(s) were running when the hub last stopped`);
|
|
31657
|
-
stops.push(startTranscriptWatcher(), startInteractionSweeper());
|
|
31876
|
+
stops.push(startTranscriptWatcher(), startInteractionSweeper(), startWatchdog());
|
|
31658
31877
|
stops.push(stopChannels);
|
|
31659
31878
|
startChannels();
|
|
31660
31879
|
stops.push(stopScheduler);
|
|
@@ -31735,6 +31954,7 @@ var init_projects = __esm({
|
|
|
31735
31954
|
init_db();
|
|
31736
31955
|
init_events();
|
|
31737
31956
|
init_interactions();
|
|
31957
|
+
init_watchdog();
|
|
31738
31958
|
init_jobs();
|
|
31739
31959
|
init_paths();
|
|
31740
31960
|
init_terminals();
|
|
@@ -33148,11 +33368,32 @@ function enqueue(input) {
|
|
|
33148
33368
|
if (input.job) db.prepare("UPDATE jobs SET last_run_at = ? WHERE id = ?").run(now(), input.job.id);
|
|
33149
33369
|
broadcast("runs", {});
|
|
33150
33370
|
broadcast("jobs", {});
|
|
33151
|
-
const position =
|
|
33371
|
+
const position = queuePosition(id);
|
|
33152
33372
|
advance();
|
|
33153
33373
|
const run4 = listRuns(500).find((item) => item.id === id);
|
|
33154
33374
|
return { run: run4, position };
|
|
33155
33375
|
}
|
|
33376
|
+
function queuePosition(id) {
|
|
33377
|
+
return db.prepare(`SELECT COUNT(*) AS n FROM runs WHERE status = 'queued' AND ${IN_LINE} <= (SELECT ${IN_LINE} FROM runs WHERE id = ?)`).get(id).n;
|
|
33378
|
+
}
|
|
33379
|
+
function stopRun(id) {
|
|
33380
|
+
const run4 = listRuns(500).find((item) => item.id === id);
|
|
33381
|
+
if (!run4 || run4.status !== "running" || !run4.sessionId) throw new Error("That run is not running.");
|
|
33382
|
+
const terminal = terminalForSession(run4.sessionId);
|
|
33383
|
+
if (!terminal) throw new Error("That run has no running terminal.");
|
|
33384
|
+
killTerminal(terminal);
|
|
33385
|
+
}
|
|
33386
|
+
function resumeRun(id) {
|
|
33387
|
+
const run4 = listRuns(500).find((item) => item.id === id);
|
|
33388
|
+
if (!run4?.sessionId) throw new Error("That run never started, so there is nothing to resume.");
|
|
33389
|
+
if (run4.status === "queued" || run4.status === "running") throw new Error("That run is already queued or running.");
|
|
33390
|
+
if (terminalForSession(run4.sessionId)) throw new Error("Its session is already open.");
|
|
33391
|
+
db.prepare("UPDATE runs SET status = 'queued', resume = 1, queued_at = ?, finished_at = NULL WHERE id = ?").run(now(), id);
|
|
33392
|
+
broadcast("runs", {});
|
|
33393
|
+
const position = queuePosition(id);
|
|
33394
|
+
advance();
|
|
33395
|
+
return { run: listRuns(500).find((item) => item.id === id), position };
|
|
33396
|
+
}
|
|
33156
33397
|
function cancelRun(id) {
|
|
33157
33398
|
db.prepare("UPDATE runs SET status = 'cancelled', finished_at = ? WHERE id = ? AND status = 'queued'").run(now(), id);
|
|
33158
33399
|
broadcast("runs", {});
|
|
@@ -33177,11 +33418,11 @@ function advance() {
|
|
|
33177
33418
|
try {
|
|
33178
33419
|
const running = db.prepare("SELECT id FROM runs WHERE status = 'running' LIMIT 1").get();
|
|
33179
33420
|
if (running) return;
|
|
33180
|
-
const next = db.prepare(
|
|
33421
|
+
const next = db.prepare(`SELECT * FROM runs WHERE status = 'queued' ORDER BY ${IN_LINE} LIMIT 1`).get();
|
|
33181
33422
|
if (!next) return;
|
|
33182
33423
|
const job = next.job_id ? getJob(next.job_id) : null;
|
|
33183
33424
|
try {
|
|
33184
|
-
const terminal = startSession({
|
|
33425
|
+
const terminal = next.resume && next.session_id ? startSession({ resumeId: next.session_id, prompt: promptText("typed.resumeRun"), runId: next.id, permissionMode: claudeSettings().runPermissionMode }) : startSession({
|
|
33185
33426
|
prompt: next.prompt,
|
|
33186
33427
|
origin: next.source === "manual" ? "queue" : next.source,
|
|
33187
33428
|
runId: next.id,
|
|
@@ -33236,7 +33477,7 @@ function recoverRuns() {
|
|
|
33236
33477
|
db.prepare("UPDATE interactions SET status = 'closed', answered_by = 'hub restart' WHERE status = 'open'").run();
|
|
33237
33478
|
return stranded;
|
|
33238
33479
|
}
|
|
33239
|
-
var crons, cronKey, toJob, toRun, advancing, waitingForClaude;
|
|
33480
|
+
var crons, cronKey, toJob, toRun, IN_LINE, advancing, waitingForClaude;
|
|
33240
33481
|
var init_jobs = __esm({
|
|
33241
33482
|
"server/src/jobs.ts"() {
|
|
33242
33483
|
"use strict";
|
|
@@ -33276,10 +33517,12 @@ var init_jobs = __esm({
|
|
|
33276
33517
|
category: row.category,
|
|
33277
33518
|
status: row.status,
|
|
33278
33519
|
sessionId: row.session_id,
|
|
33520
|
+
resume: Boolean(row.resume),
|
|
33279
33521
|
createdAt: row.created_at,
|
|
33280
33522
|
startedAt: row.started_at,
|
|
33281
33523
|
finishedAt: row.finished_at
|
|
33282
33524
|
});
|
|
33525
|
+
IN_LINE = "COALESCE(queued_at, created_at)";
|
|
33283
33526
|
advancing = false;
|
|
33284
33527
|
waitingForClaude = /* @__PURE__ */ new Set();
|
|
33285
33528
|
}
|
|
@@ -34349,6 +34592,8 @@ api.post(
|
|
|
34349
34592
|
})
|
|
34350
34593
|
);
|
|
34351
34594
|
api.get("/runs", wrap2(() => listRuns()));
|
|
34595
|
+
api.post("/runs/:id/stop", wrap2((req) => stopRun(req.params.id)));
|
|
34596
|
+
api.post("/runs/:id/resume", wrap2((req) => resumeRun(req.params.id)));
|
|
34352
34597
|
api.post("/runs", wrap2((req) => enqueue({ source: "manual", prompt: String(req.body?.prompt ?? ""), category: req.body?.category || "auto" })));
|
|
34353
34598
|
api.post("/runs/:id/cancel", wrap2((req) => cancelRun(req.params.id)));
|
|
34354
34599
|
api.get("/mcp/suggestions", wrap2(() => listSuggestions()));
|