yeschef-cli 0.1.0__py3-none-any.whl
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.
- yeschef/__init__.py +3 -0
- yeschef/__main__.py +6 -0
- yeschef/agent/__init__.py +6 -0
- yeschef/agent/backends/__init__.py +53 -0
- yeschef/agent/backends/anthropic_compat.py +119 -0
- yeschef/agent/backends/base.py +50 -0
- yeschef/agent/backends/cli.py +99 -0
- yeschef/agent/backends/openai_compat.py +118 -0
- yeschef/agent/config.py +98 -0
- yeschef/agent/detect.py +98 -0
- yeschef/agent/harness.py +1009 -0
- yeschef/cli.py +1073 -0
- yeschef/hub/__init__.py +16 -0
- yeschef/hub/api.py +595 -0
- yeschef/hub/app.py +43 -0
- yeschef/hub/dashboard.html +206 -0
- yeschef/hub/events.py +78 -0
- yeschef/hub/mcp_server.py +941 -0
- yeschef/hub/schema.sql +106 -0
- yeschef/hub/store.py +1621 -0
- yeschef/models.py +431 -0
- yeschef/procs.py +109 -0
- yeschef/replay.py +224 -0
- yeschef/resources/__init__.py +0 -0
- yeschef/resources/agents/__init__.py +0 -0
- yeschef/resources/agents/yeschef-expediter.md +74 -0
- yeschef/resources/skill/SKILL.md +212 -0
- yeschef/resources/skill/__init__.py +0 -0
- yeschef/sdk/__init__.py +9 -0
- yeschef/sdk/client.py +359 -0
- yeschef/settings.py +100 -0
- yeschef/tools/__init__.py +5 -0
- yeschef/tools/executor.py +296 -0
- yeschef_cli-0.1.0.dist-info/METADATA +254 -0
- yeschef_cli-0.1.0.dist-info/RECORD +38 -0
- yeschef_cli-0.1.0.dist-info/WHEEL +4 -0
- yeschef_cli-0.1.0.dist-info/entry_points.txt +3 -0
- yeschef_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>yeschef</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f6f7f9; --panel: #ffffff; --line: #e3e6ea; --ink: #1b2027;
|
|
10
|
+
--dim: #6b7280; --accent: #2f7d55; --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
11
|
+
--online: #2f7d55; --working: #b7791f; --queued: #6b7280; --failed: #c0392b;
|
|
12
|
+
--done: #2f7d55; --cancel: #8a8f98; --input: #6d5bd0;
|
|
13
|
+
}
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:root:not([data-theme="light"]) {
|
|
16
|
+
--bg: #0d1117; --panel: #161b22; --line: #262c36; --ink: #e6edf3;
|
|
17
|
+
--dim: #8b949e; --accent: #3fb950;
|
|
18
|
+
--online: #3fb950; --working: #d29922; --queued: #8b949e; --failed: #f85149;
|
|
19
|
+
--done: #3fb950; --cancel: #6e7681; --input: #a371f7;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
* { box-sizing: border-box; }
|
|
23
|
+
body {
|
|
24
|
+
margin: 0; background: var(--bg); color: var(--ink);
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
26
|
+
font-size: 14px; line-height: 1.5;
|
|
27
|
+
}
|
|
28
|
+
header {
|
|
29
|
+
display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap;
|
|
30
|
+
padding: 18px 24px; border-bottom: 1px solid var(--line);
|
|
31
|
+
}
|
|
32
|
+
header h1 { margin: 0; font-size: 19px; letter-spacing: -0.01em; }
|
|
33
|
+
header h1 .ball { color: var(--accent); }
|
|
34
|
+
.fleet { color: var(--dim); font-size: 13px; }
|
|
35
|
+
.fleet b { color: var(--ink); }
|
|
36
|
+
.updated { margin-left: auto; color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
37
|
+
main { padding: 20px 24px; max-width: 1100px; margin: 0 auto; }
|
|
38
|
+
.stats { display: flex; gap: 28px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
39
|
+
.stat .n { font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; }
|
|
40
|
+
.stat .l { color: var(--dim); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
|
|
41
|
+
.caveat { color: var(--dim); font-size: 12px; margin: 4px 0 22px; max-width: 70ch; }
|
|
42
|
+
section { margin-bottom: 26px; }
|
|
43
|
+
h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim);
|
|
44
|
+
margin: 0 0 8px; font-weight: 600; }
|
|
45
|
+
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
|
|
46
|
+
table { width: 100%; border-collapse: collapse; }
|
|
47
|
+
th, td { text-align: left; padding: 9px 14px; border-bottom: 1px solid var(--line); }
|
|
48
|
+
th { color: var(--dim); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
|
|
49
|
+
tr:last-child td { border-bottom: none; }
|
|
50
|
+
td.mono, .mono { font-family: var(--mono); font-size: 12.5px; }
|
|
51
|
+
.tabnum { font-variant-numeric: tabular-nums; }
|
|
52
|
+
.pill { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px;
|
|
53
|
+
padding: 2px 9px; border-radius: 999px; border: 1px solid transparent; white-space: nowrap; }
|
|
54
|
+
.pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
55
|
+
.s-online, .s-completed { color: var(--done); background: color-mix(in srgb, var(--done) 12%, transparent); }
|
|
56
|
+
.s-working, .s-claimed { color: var(--working); background: color-mix(in srgb, var(--working) 14%, transparent); }
|
|
57
|
+
.s-queued { color: var(--queued); background: color-mix(in srgb, var(--queued) 14%, transparent); }
|
|
58
|
+
.s-failed { color: var(--failed); background: color-mix(in srgb, var(--failed) 13%, transparent); }
|
|
59
|
+
.s-cancelled { color: var(--cancel); background: color-mix(in srgb, var(--cancel) 14%, transparent); }
|
|
60
|
+
.s-input_required { color: var(--input); background: color-mix(in srgb, var(--input) 14%, transparent); }
|
|
61
|
+
.s-offline { color: var(--dim); background: color-mix(in srgb, var(--dim) 12%, transparent); }
|
|
62
|
+
.bar { height: 4px; background: var(--line); border-radius: 3px; overflow: hidden; min-width: 60px; }
|
|
63
|
+
.bar > i { display: block; height: 100%; background: var(--working); }
|
|
64
|
+
.empty { padding: 26px 14px; text-align: center; color: var(--dim); }
|
|
65
|
+
.empty code { font-family: var(--mono); background: color-mix(in srgb, var(--dim) 14%, transparent);
|
|
66
|
+
padding: 1px 6px; border-radius: 5px; }
|
|
67
|
+
.muted { color: var(--dim); }
|
|
68
|
+
.flag { font-size: 11px; color: var(--failed); font-family: var(--mono); }
|
|
69
|
+
.wrap { overflow-x: auto; }
|
|
70
|
+
.offline-banner { background: color-mix(in srgb, var(--failed) 12%, transparent);
|
|
71
|
+
border: 1px solid color-mix(in srgb, var(--failed) 30%, transparent);
|
|
72
|
+
color: var(--failed); border-radius: 10px; padding: 12px 16px; margin-bottom: 20px; }
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<header>
|
|
77
|
+
<h1><span class="ball">🍳</span> yeschef</h1>
|
|
78
|
+
<span class="fleet" id="fleet">connecting…</span>
|
|
79
|
+
<span class="updated" id="updated"></span>
|
|
80
|
+
</header>
|
|
81
|
+
<main id="app">
|
|
82
|
+
<div class="empty">loading…</div>
|
|
83
|
+
</main>
|
|
84
|
+
|
|
85
|
+
<script>
|
|
86
|
+
const $ = (h) => { const t = document.createElement("template"); t.innerHTML = h.trim(); return t.content.firstChild; };
|
|
87
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c]));
|
|
88
|
+
const ago = (s) => {
|
|
89
|
+
if (s == null) return "—";
|
|
90
|
+
if (s < 60) return Math.round(s) + "s";
|
|
91
|
+
if (s < 3600) return Math.round(s/60) + "m";
|
|
92
|
+
return (s/3600).toFixed(1) + "h";
|
|
93
|
+
};
|
|
94
|
+
const clock = (sec) => sec >= 3600 ? (sec/3600).toFixed(1)+"h" : Math.round(sec/60)+"m";
|
|
95
|
+
|
|
96
|
+
function statePill(state) {
|
|
97
|
+
return `<span class="pill s-${esc(state)}">${esc(state)}</span>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function render(d) {
|
|
101
|
+
const online = d.workers.filter(w => w.status === "online").length;
|
|
102
|
+
document.getElementById("fleet").innerHTML =
|
|
103
|
+
`<b>${online}</b>/${d.workers.length} cooks on · <b>${d.lifetime.tasks_completed ?? 0}</b> tickets cooked lifetime`;
|
|
104
|
+
document.getElementById("updated").textContent = "updated " + new Date().toLocaleTimeString();
|
|
105
|
+
|
|
106
|
+
const lt = d.lifetime;
|
|
107
|
+
const failTail = (lt.tasks_failed_cancelled ?? 0) > 0
|
|
108
|
+
? ` <span class="muted">(+${lt.tasks_failed_cancelled} failed/cancelled, ${clock(lt.work_seconds_failed || 0)})</span>` : "";
|
|
109
|
+
const taskTokens = lt.task_tokens ?? lt.local_tokens ?? 0;
|
|
110
|
+
|
|
111
|
+
const app = document.getElementById("app");
|
|
112
|
+
app.innerHTML = "";
|
|
113
|
+
|
|
114
|
+
if (online === 0) {
|
|
115
|
+
app.appendChild($(`<div class="offline-banner">No cooks are on the line. Clock one in with
|
|
116
|
+
<code style="font-family:var(--mono)">yeschef join</code> on a machine with a local model server.</div>`));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// lifetime stats
|
|
120
|
+
app.appendChild($(`<div class="stats">
|
|
121
|
+
<div class="stat"><div class="n tabnum">${lt.tasks_completed ?? 0}</div><div class="l">tickets cooked${failTail}</div></div>
|
|
122
|
+
<div class="stat"><div class="n tabnum">${(taskTokens).toLocaleString()}</div><div class="l">tokens cooked locally</div></div>
|
|
123
|
+
<div class="stat"><div class="n tabnum">${clock(lt.work_seconds || 0)}</div><div class="l">local compute</div></div>
|
|
124
|
+
</div>`));
|
|
125
|
+
app.appendChild($(`<p class="caveat">Locally-generated tokens are compute run on your hardware, not saved
|
|
126
|
+
Claude tokens one-for-one — local models are weaker and their output usually needs review.</p>`));
|
|
127
|
+
|
|
128
|
+
// cooks
|
|
129
|
+
const wsec = $(`<section><h2>The line</h2></section>`);
|
|
130
|
+
if (!d.workers.length) {
|
|
131
|
+
wsec.appendChild($(`<div class="card"><div class="empty">No cooks clocked in. Run <code>yeschef join</code>.</div></div>`));
|
|
132
|
+
} else {
|
|
133
|
+
const rows = d.workers.map(w => {
|
|
134
|
+
const busy = w.active_tasks ? `<span class="pill s-working">${w.active_tasks} active</span>` : `<span class="muted">idle</span>`;
|
|
135
|
+
const stale = w.status !== "online" ? `<span class="muted"> · ${ago(w.heartbeat_age_s)} ago</span>` : "";
|
|
136
|
+
return `<tr>
|
|
137
|
+
<td class="mono">${esc(w.name)}</td>
|
|
138
|
+
<td class="mono">${esc(w.backend || "—")}</td>
|
|
139
|
+
<td class="mono muted">${esc(w.node || "—")}</td>
|
|
140
|
+
<td>${statePill(w.status)}${stale}</td>
|
|
141
|
+
<td>${busy}</td>
|
|
142
|
+
</tr>`;
|
|
143
|
+
}).join("");
|
|
144
|
+
wsec.appendChild($(`<div class="card wrap"><table>
|
|
145
|
+
<thead><tr><th>cook</th><th>model</th><th>station</th><th>status</th><th>load</th></tr></thead>
|
|
146
|
+
<tbody>${rows}</tbody></table></div>`));
|
|
147
|
+
}
|
|
148
|
+
app.appendChild(wsec);
|
|
149
|
+
|
|
150
|
+
// tickets
|
|
151
|
+
const tsec = $(`<section><h2>The rail</h2></section>`);
|
|
152
|
+
if (!d.tasks.length) {
|
|
153
|
+
tsec.appendChild($(`<div class="card"><div class="empty">No tickets yet. Fire one from Claude Code, or <code>yeschef submit</code>.</div></div>`));
|
|
154
|
+
} else {
|
|
155
|
+
const rows = d.tasks.map(t => {
|
|
156
|
+
const live = t.state === "working" || t.state === "claimed";
|
|
157
|
+
const pct = t.progress && t.progress.pct != null ? t.progress.pct : (t.state === "completed" ? 100 : 0);
|
|
158
|
+
const prog = live
|
|
159
|
+
? `<div class="bar" title="${esc(t.progress?.message || "")}"><i style="width:${pct}%"></i></div>`
|
|
160
|
+
: (t.state === "completed" && t.files ? `<span class="muted">${t.files} file${t.files>1?"s":""}</span>`
|
|
161
|
+
: t.error ? `<span class="flag">${esc(t.error.slice(0,40))}</span>` : `<span class="muted">—</span>`);
|
|
162
|
+
const dur = t.ran_s != null ? ago(t.ran_s) : ago(t.age_s);
|
|
163
|
+
return `<tr>
|
|
164
|
+
<td>${statePill(t.state)}</td>
|
|
165
|
+
<td>${esc(t.title || t.id)}</td>
|
|
166
|
+
<td class="mono muted">${esc(t.assignee || t.selector || "—")}</td>
|
|
167
|
+
<td class="tabnum muted">${dur}</td>
|
|
168
|
+
<td style="min-width:120px">${prog}</td>
|
|
169
|
+
</tr>`;
|
|
170
|
+
}).join("");
|
|
171
|
+
tsec.appendChild($(`<div class="card wrap"><table>
|
|
172
|
+
<thead><tr><th>state</th><th>ticket</th><th>cook</th><th>age</th><th>progress</th></tr></thead>
|
|
173
|
+
<tbody>${rows}</tbody></table></div>`));
|
|
174
|
+
}
|
|
175
|
+
app.appendChild(tsec);
|
|
176
|
+
|
|
177
|
+
// rooms
|
|
178
|
+
const active = d.rooms.filter(r => !r.archived);
|
|
179
|
+
if (d.rooms.length) {
|
|
180
|
+
const rsec = $(`<section><h2>Talking it over ${active.length ? `· ${active.length} live` : ""}</h2></section>`);
|
|
181
|
+
const rows = d.rooms.slice(0, 12).map(r => `<tr>
|
|
182
|
+
<td>${esc(r.topic)}</td>
|
|
183
|
+
<td class="mono muted">${esc((r.members || []).join(", ") || "—")}</td>
|
|
184
|
+
<td>${r.archived ? `<span class="pill s-cancelled">${esc(r.archived_reason || "closed")}</span>` : `<span class="pill s-working">open</span>`}</td>
|
|
185
|
+
</tr>`).join("");
|
|
186
|
+
rsec.appendChild($(`<div class="card wrap"><table>
|
|
187
|
+
<thead><tr><th>topic</th><th>participants</th><th>state</th></tr></thead>
|
|
188
|
+
<tbody>${rows}</tbody></table></div>`));
|
|
189
|
+
app.appendChild(rsec);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function tick() {
|
|
194
|
+
try {
|
|
195
|
+
const r = await fetch("/overview", { cache: "no-store" });
|
|
196
|
+
if (!r.ok) throw new Error(r.status);
|
|
197
|
+
render(await r.json());
|
|
198
|
+
} catch (e) {
|
|
199
|
+
document.getElementById("fleet").innerHTML = `<span style="color:var(--failed)">hub unreachable</span>`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
tick();
|
|
203
|
+
setInterval(tick, 2500);
|
|
204
|
+
</script>
|
|
205
|
+
</body>
|
|
206
|
+
</html>
|
yeschef/hub/events.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""In-process fan-out of hub events to connected agents.
|
|
2
|
+
|
|
3
|
+
Each connected agent holds one queue per open stream. The store is synchronous, so it
|
|
4
|
+
publishes through a thread-safe hand-off onto the event loop.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import contextlib
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from collections.abc import AsyncIterator
|
|
13
|
+
|
|
14
|
+
from ..models import EventKind, now
|
|
15
|
+
|
|
16
|
+
MAX_QUEUE = 256
|
|
17
|
+
MAX_PENDING = 1024
|
|
18
|
+
"""Cap on events buffered before the bus is bound to a loop (CLI and embedded use)."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EventBus:
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._subs: dict[str, set[asyncio.Queue]] = defaultdict(set)
|
|
24
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
25
|
+
self._pending: list[tuple[str, dict]] = []
|
|
26
|
+
|
|
27
|
+
def bind(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
28
|
+
"""Attach to the serving loop; flush anything published before startup."""
|
|
29
|
+
self._loop = loop
|
|
30
|
+
pending, self._pending = self._pending, []
|
|
31
|
+
for target, event in pending:
|
|
32
|
+
self._deliver(target, event)
|
|
33
|
+
|
|
34
|
+
def publish(self, target: str, kind: EventKind | str, payload: dict) -> None:
|
|
35
|
+
"""Publish to one agent. Safe from any thread."""
|
|
36
|
+
event = {"kind": str(kind), "at": now(), **payload}
|
|
37
|
+
loop = self._loop
|
|
38
|
+
if loop is None:
|
|
39
|
+
# Nobody is listening yet (CLI, embedding, tests). Keep a bounded tail so a
|
|
40
|
+
# long-lived unbound Store cannot leak every event it ever published.
|
|
41
|
+
self._pending.append((target, event))
|
|
42
|
+
del self._pending[:-MAX_PENDING]
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
running = asyncio.get_running_loop()
|
|
46
|
+
except RuntimeError:
|
|
47
|
+
running = None
|
|
48
|
+
if running is loop:
|
|
49
|
+
self._deliver(target, event)
|
|
50
|
+
else:
|
|
51
|
+
loop.call_soon_threadsafe(self._deliver, target, event)
|
|
52
|
+
|
|
53
|
+
def publish_many(self, targets: list[str], kind: EventKind | str, payload: dict) -> None:
|
|
54
|
+
for target in targets:
|
|
55
|
+
self.publish(target, kind, payload)
|
|
56
|
+
|
|
57
|
+
def _deliver(self, target: str, event: dict) -> None:
|
|
58
|
+
for queue in list(self._subs.get(target, ())):
|
|
59
|
+
if queue.full():
|
|
60
|
+
# Slow consumer: drop the oldest so a stalled stream cannot wedge the hub.
|
|
61
|
+
with contextlib.suppress(asyncio.QueueEmpty):
|
|
62
|
+
queue.get_nowait()
|
|
63
|
+
with contextlib.suppress(asyncio.QueueFull):
|
|
64
|
+
queue.put_nowait(event)
|
|
65
|
+
|
|
66
|
+
def subscriber_count(self, agent: str) -> int:
|
|
67
|
+
return len(self._subs.get(agent, ()))
|
|
68
|
+
|
|
69
|
+
@contextlib.asynccontextmanager
|
|
70
|
+
async def subscribe(self, agent: str) -> AsyncIterator[asyncio.Queue]:
|
|
71
|
+
queue: asyncio.Queue = asyncio.Queue(maxsize=MAX_QUEUE)
|
|
72
|
+
self._subs[agent].add(queue)
|
|
73
|
+
try:
|
|
74
|
+
yield queue
|
|
75
|
+
finally:
|
|
76
|
+
self._subs[agent].discard(queue)
|
|
77
|
+
if not self._subs[agent]:
|
|
78
|
+
self._subs.pop(agent, None)
|