@lmctl-ai/lmctl 0.2.37 → 0.2.39
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 +16 -0
- package/THIRD_PARTY_NOTICES.md +35 -0
- package/bin/lmbee +2 -0
- package/bin/lmscript +3 -0
- package/build-info.json +1 -0
- package/dist/cli/index.js +569 -494
- package/dist/cli/schema.sql +0 -169
- package/dist/cli/script.js +273 -266
- package/dist/lmbee.js +960 -0
- package/dist/lmscript.js +388 -0
- package/dist/public/app.js +379 -0
- package/dist/public/index.html +78 -0
- package/dist/public/styles.css +160 -0
- package/dist/schema.sql +455 -0
- package/dist/vendor-webui/index.html +18 -0
- package/dist/vendor-webui/local.js +231 -0
- package/dist/vendor-webui/styles.css +1852 -0
- package/package.json +12 -7
- package/dist/cli/batch.d.ts +0 -2
- package/dist/cli/busy-evidence.d.ts +0 -27
- package/dist/cli/chat-cycle.d.ts +0 -59
- package/dist/cli/chat-exec.d.ts +0 -91
- package/dist/cli/chat-policy.d.ts +0 -22
- package/dist/cli/chat.d.ts +0 -14
- package/dist/cli/code-resolver.d.ts +0 -8
- package/dist/cli/common.d.ts +0 -57
- package/dist/cli/context.d.ts +0 -12
- package/dist/cli/cost.d.ts +0 -2
- package/dist/cli/export.d.ts +0 -2
- package/dist/cli/hello.d.ts +0 -10
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/infra.d.ts +0 -78
- package/dist/cli/init.d.ts +0 -21
- package/dist/cli/intent-input.d.ts +0 -16
- package/dist/cli/intent-match.d.ts +0 -30
- package/dist/cli/intent-sessions.d.ts +0 -24
- package/dist/cli/intent-team.d.ts +0 -25
- package/dist/cli/intent.d.ts +0 -22
- package/dist/cli/ls.d.ts +0 -2
- package/dist/cli/mailbox-cli.d.ts +0 -6
- package/dist/cli/managed.d.ts +0 -62
- package/dist/cli/mcp-install.d.ts +0 -23
- package/dist/cli/mcp.d.ts +0 -2
- package/dist/cli/model.d.ts +0 -21
- package/dist/cli/paths.d.ts +0 -50
- package/dist/cli/plan-wizard.d.ts +0 -42
- package/dist/cli/plan.d.ts +0 -20
- package/dist/cli/queue-delivery.d.ts +0 -122
- package/dist/cli/quickrun.d.ts +0 -14
- package/dist/cli/ratelimit.d.ts +0 -2
- package/dist/cli/remote-fetch.d.ts +0 -7
- package/dist/cli/run-queries.d.ts +0 -82
- package/dist/cli/running.d.ts +0 -5
- package/dist/cli/save-script.d.ts +0 -41
- package/dist/cli/script.d.ts +0 -6
- package/dist/cli/seed-flight.d.ts +0 -2
- package/dist/cli/self-identity.d.ts +0 -25
- package/dist/cli/session-idle.d.ts +0 -118
- package/dist/cli/status.d.ts +0 -22
- package/dist/cli/tail.d.ts +0 -18
- package/dist/cli/target-resolver.d.ts +0 -139
- package/dist/cli/team-status.d.ts +0 -91
- package/dist/cli/teamfile-compat.d.ts +0 -116
- package/dist/cli/teamfile-store.d.ts +0 -18
- package/dist/cli/teamfile-template.d.ts +0 -47
- package/dist/cli/template-env.d.ts +0 -25
- package/dist/cli/terminal-lock.d.ts +0 -25
- package/dist/cli/terminal.d.ts +0 -23
- package/dist/cli/time-filter.d.ts +0 -10
- package/dist/cli/update-notifier.d.ts +0 -11
- package/dist/cli/version-util.d.ts +0 -23
- package/dist/config/seed_templates.d.ts +0 -1
- package/dist/db/backfill-event-log.d.ts +0 -26
- package/dist/db/delegation.d.ts +0 -37
- package/dist/db/escalation_vocab.d.ts +0 -45
- package/dist/db/event-log.d.ts +0 -40
- package/dist/db/index.d.ts +0 -1213
- package/dist/db/inflight-acquire.d.ts +0 -70
- package/dist/db/invocation-event-projections.d.ts +0 -65
- package/dist/db/ledger-identity.test.d.ts +0 -1
- package/dist/db/live-event-projections.d.ts +0 -92
- package/dist/db/migrate.d.ts +0 -51
- package/dist/db/open-readonly.d.ts +0 -2
- package/dist/db/process-liveness.d.ts +0 -37
- package/dist/db/sqlite.d.ts +0 -53
- package/dist/db/sqlite.test.d.ts +0 -1
- package/dist/db/terminal-lock.d.ts +0 -14
- package/dist/db/tracked-invocation-reconcile.d.ts +0 -52
- package/dist/db/tracked-invocation.d.ts +0 -116
- package/dist/mcp/dispatch.d.ts +0 -45
- package/dist/mcp/mediation.d.ts +0 -128
- package/dist/mcp/path-warning.d.ts +0 -7
- package/dist/mcp/server.d.ts +0 -63
- package/dist/mcp/tools.d.ts +0 -24
- package/dist/providers/adapter.d.ts +0 -271
- package/dist/providers/adapters/agy-adapter.d.ts +0 -18
- package/dist/providers/adapters/claude-adapter.d.ts +0 -20
- package/dist/providers/adapters/codex-adapter.d.ts +0 -22
- package/dist/providers/adapters/copilot-adapter.d.ts +0 -16
- package/dist/providers/adapters/dsh-adapter.d.ts +0 -89
- package/dist/providers/adapters/gemini-adapter.d.ts +0 -44
- package/dist/providers/adapters/hermes-adapter.d.ts +0 -71
- package/dist/providers/adapters/index.d.ts +0 -35
- package/dist/providers/adapters/kimi-adapter.d.ts +0 -16
- package/dist/providers/adapters/lmplayer-adapter.d.ts +0 -14
- package/dist/providers/adapters/opencode-adapter.d.ts +0 -11
- package/dist/providers/adapters/pi-adapter.d.ts +0 -17
- package/dist/providers/adapters/print-turn.d.ts +0 -29
- package/dist/providers/adapters/qwen-adapter.d.ts +0 -11
- package/dist/providers/agy.d.ts +0 -32
- package/dist/providers/base-adapter.d.ts +0 -110
- package/dist/providers/base.d.ts +0 -12
- package/dist/providers/binary-check.d.ts +0 -102
- package/dist/providers/bundle.d.ts +0 -124
- package/dist/providers/claude-mock.d.ts +0 -90
- package/dist/providers/claude.d.ts +0 -21
- package/dist/providers/codex-config.d.ts +0 -15
- package/dist/providers/codex.d.ts +0 -25
- package/dist/providers/config/init-config.d.ts +0 -43
- package/dist/providers/config/model-selection-lint.d.ts +0 -70
- package/dist/providers/config/team-config.d.ts +0 -184
- package/dist/providers/config/types.d.ts +0 -146
- package/dist/providers/config/user-config.d.ts +0 -88
- package/dist/providers/copilot.d.ts +0 -21
- package/dist/providers/debug.d.ts +0 -128
- package/dist/providers/dsh-sidecar.d.ts +0 -65
- package/dist/providers/dsh-zstd.d.ts +0 -27
- package/dist/providers/dsh.d.ts +0 -37
- package/dist/providers/frontmatter.d.ts +0 -46
- package/dist/providers/gemini-session-reader.d.ts +0 -151
- package/dist/providers/gemini.d.ts +0 -39
- package/dist/providers/hermes.d.ts +0 -90
- package/dist/providers/index.d.ts +0 -423
- package/dist/providers/kimi.d.ts +0 -37
- package/dist/providers/model-catalog.d.ts +0 -145
- package/dist/providers/model-suggestions.d.ts +0 -4
- package/dist/providers/one-shot-timeout.d.ts +0 -1
- package/dist/providers/opencode-config.d.ts +0 -62
- package/dist/providers/opencode.d.ts +0 -40
- package/dist/providers/parsers/acp.d.ts +0 -71
- package/dist/providers/parsers/codex-app-server.d.ts +0 -60
- package/dist/providers/parsers/stream-json.d.ts +0 -57
- package/dist/providers/passive-readers/agy.d.ts +0 -155
- package/dist/providers/passive-readers/copilot.d.ts +0 -173
- package/dist/providers/passive-readers/dsh.d.ts +0 -34
- package/dist/providers/passive-readers/kimi.d.ts +0 -44
- package/dist/providers/passive-readers/opencode.d.ts +0 -89
- package/dist/providers/passive-readers/qwen.d.ts +0 -151
- package/dist/providers/passive-readers/shared.d.ts +0 -138
- package/dist/providers/passive-session-reader.d.ts +0 -188
- package/dist/providers/path-utils.d.ts +0 -1
- package/dist/providers/pi.d.ts +0 -32
- package/dist/providers/pricing.d.ts +0 -107
- package/dist/providers/qwen.d.ts +0 -22
- package/dist/providers/ratelimit/agy.d.ts +0 -22
- package/dist/providers/ratelimit/claude-auth.d.ts +0 -13
- package/dist/providers/ratelimit/claude.d.ts +0 -6
- package/dist/providers/ratelimit/codex.d.ts +0 -7
- package/dist/providers/ratelimit/kimi-auth.d.ts +0 -15
- package/dist/providers/ratelimit/kimi.d.ts +0 -6
- package/dist/providers/ratelimit/schema.d.ts +0 -28
- package/dist/providers/ratelimit-readings.d.ts +0 -19
- package/dist/providers/registry.d.ts +0 -31
- package/dist/providers/resume.d.ts +0 -43
- package/dist/providers/seed-instruction.d.ts +0 -3
- package/dist/providers/self-session-env.d.ts +0 -21
- package/dist/providers/session-limit.d.ts +0 -9
- package/dist/providers/strip-ansi.d.ts +0 -5
- package/dist/providers/transport-errors.d.ts +0 -94
- package/dist/providers/transport-lifecycle.d.ts +0 -63
- package/dist/providers/transport-wrappers.d.ts +0 -110
- package/dist/providers/transports/acp-profiles/config-option-step.d.ts +0 -24
- package/dist/providers/transports/acp-profiles/copilot.d.ts +0 -12
- package/dist/providers/transports/acp-profiles/gemini.d.ts +0 -22
- package/dist/providers/transports/acp-profiles/hermes.d.ts +0 -27
- package/dist/providers/transports/acp-profiles/index.d.ts +0 -19
- package/dist/providers/transports/acp-profiles/kimi.d.ts +0 -24
- package/dist/providers/transports/acp-profiles/opencode.d.ts +0 -26
- package/dist/providers/transports/acp-profiles/types.d.ts +0 -258
- package/dist/providers/transports/acp-rpc.d.ts +0 -18
- package/dist/providers/transports/acp.d.ts +0 -217
- package/dist/providers/transports/codex-app-server.d.ts +0 -190
- package/dist/providers/transports/hold-open.d.ts +0 -120
- package/dist/providers/transports/manager.d.ts +0 -21
- package/dist/providers/transports/stream-json.d.ts +0 -162
- package/dist/providers/transports/types.d.ts +0 -56
- package/dist/providers/types.d.ts +0 -154
- package/dist/providers/yolo-contract.d.ts +0 -131
- package/dist/script/adapter.d.ts +0 -104
- package/dist/script/batch-script.d.ts +0 -7
- package/dist/script/index.d.ts +0 -3
- package/dist/script/lmbee-module.d.ts +0 -2
- package/dist/script/lmscript.d.ts +0 -616
- package/dist/shared/policy.d.ts +0 -74
- package/dist/shared-lib/db.d.ts +0 -25
- package/dist/shared-lib/passive-session-reader.d.ts +0 -2
- package/dist/shared-lib/provider-parse.d.ts +0 -6
- package/dist/templates/basedir.d.ts +0 -52
- package/dist/uri/lmctl_uri.d.ts +0 -38
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
const $ = (selector) => document.querySelector(selector);
|
|
2
|
+
|
|
3
|
+
function escapeHtml(value) {
|
|
4
|
+
return String(value ?? '').replace(/[&<>"']/g, (ch) => ({
|
|
5
|
+
'&': '&',
|
|
6
|
+
'<': '<',
|
|
7
|
+
'>': '>',
|
|
8
|
+
'"': '"',
|
|
9
|
+
"'": ''',
|
|
10
|
+
}[ch]));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function fetchJson(url, options) {
|
|
14
|
+
const res = await fetch(url, options);
|
|
15
|
+
const body = await res.json().catch(() => ({}));
|
|
16
|
+
if (!res.ok) throw new Error(body.error || `request failed: ${res.status}`);
|
|
17
|
+
return body;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderTable(headers, rows) {
|
|
21
|
+
if (rows.length === 0) return '<p class="muted">(none)</p>';
|
|
22
|
+
const headHtml = headers.map((h) => `<th>${escapeHtml(h)}</th>`).join('');
|
|
23
|
+
const bodyHtml = rows
|
|
24
|
+
.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join('')}</tr>`)
|
|
25
|
+
.join('');
|
|
26
|
+
return `<table><thead><tr>${headHtml}</tr></thead><tbody>${bodyHtml}</tbody></table>`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function statusClass(status) {
|
|
30
|
+
if (!status) return '';
|
|
31
|
+
if (status === 'BLOCKED') return 'status-BLOCKED';
|
|
32
|
+
if (status === 'DONE') return 'status-DONE';
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Phase 2c (2026-08-31, tools.lmctl's design cue): `/api/liveness`'s `remoteOutbound` is `null` for a
|
|
37
|
+
// RemoteTeam this site has never sent anything -- genuinely nothing observed yet, not an error.
|
|
38
|
+
function remoteOutboundLabel(remoteOutbound) {
|
|
39
|
+
if (!remoteOutbound) return 'remote -- delivery only';
|
|
40
|
+
const ts = remoteOutbound.repliedTs ?? remoteOutbound.ackedTs ?? remoteOutbound.sentTs;
|
|
41
|
+
if (remoteOutbound.state === 'replied') return `remote -- replied ${ts}`;
|
|
42
|
+
if (remoteOutbound.state === 'acked') return `remote -- acked ${ts}`;
|
|
43
|
+
return `remote -- sent ${ts}, no ack yet`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Populated by renderBoard, read by the enqueue/session-link flows so they know each team's lead
|
|
47
|
+
// alias without a second round-trip.
|
|
48
|
+
let teamsCache = [];
|
|
49
|
+
|
|
50
|
+
async function renderBoard() {
|
|
51
|
+
const el = $('#board-table');
|
|
52
|
+
try {
|
|
53
|
+
const [teamsBody, stateBody, liveBody, stalledBody] = await Promise.all([
|
|
54
|
+
fetchJson('api/teams'),
|
|
55
|
+
fetchJson('api/state'),
|
|
56
|
+
fetchJson('api/liveness').catch(() => ({ teams: [] })),
|
|
57
|
+
fetchJson('api/stalled').catch(() => ({ stalled: [], remoteStalled: [] })),
|
|
58
|
+
]);
|
|
59
|
+
teamsCache = teamsBody.teams;
|
|
60
|
+
const stateByTeam = new Map(stateBody.teams.map((t) => [t.team, t.state]));
|
|
61
|
+
const progressByTeam = new Map(stateBody.teams.map((t) => [t.team, t.progress]));
|
|
62
|
+
const liveByTeam = new Map(liveBody.teams.map((t) => [t.team, t]));
|
|
63
|
+
const stalledSet = new Set(stalledBody.stalled);
|
|
64
|
+
const remoteStalledSet = new Set(stalledBody.remoteStalled ?? []);
|
|
65
|
+
|
|
66
|
+
const rows = teamsBody.teams.map((team) => {
|
|
67
|
+
// 2026-08-31, Phase 2b3: a RemoteTeam (channel=lmmail) has no leadAlias/promptName and no `lctl`
|
|
68
|
+
// liveness concept. Phase 2c (tools.lmctl's design cue): once this site has actually SENT the
|
|
69
|
+
// team something, `/api/liveness`'s `remoteOutbound` gives real ack/reply observation --
|
|
70
|
+
// "remote -- delivery+observe" with the actual state, not just the static placeholder. Before
|
|
71
|
+
// that (never sent anything yet), there's genuinely nothing to observe -- stays "delivery only",
|
|
72
|
+
// honest about the absence of evidence rather than implying observation that hasn't happened.
|
|
73
|
+
const isRemote = team.channel === 'lmmail';
|
|
74
|
+
const state = stateByTeam.get(team.name);
|
|
75
|
+
const progress = progressByTeam.get(team.name);
|
|
76
|
+
const live = liveByTeam.get(team.name);
|
|
77
|
+
const liveLabel = isRemote
|
|
78
|
+
? remoteOutboundLabel(live?.remoteOutbound)
|
|
79
|
+
: live
|
|
80
|
+
? live.running
|
|
81
|
+
? 'Running'
|
|
82
|
+
: live.deadReclaimable
|
|
83
|
+
? 'Dead(reclaimable)'
|
|
84
|
+
: 'Idle'
|
|
85
|
+
: '?';
|
|
86
|
+
const status = state?.status ?? '?';
|
|
87
|
+
// Objective, git/file-write-evidence-based signal (2026-08-30 status-engine redesign) -- `status`
|
|
88
|
+
// above is self-report only, kept as a hint; this is the ground truth anti-churn actually uses.
|
|
89
|
+
const progressLabel = progress?.confirmed_done_ts ? 'DONE(confirmed)' : (progress?.progress_status ?? '?');
|
|
90
|
+
// Surfaced for the LLM/operator to decide on -- the cron itself never auto-nudges a stalled
|
|
91
|
+
// team (see cron/scheduler.ts); this badge is the whole point of that surfacing. Phase 2c: a
|
|
92
|
+
// remote team's stalled signal is a DIFFERENT classification mechanism (ack/reply observation,
|
|
93
|
+
// not lctl/progress-scan) -- labeled distinctly so it's never confused with the local one.
|
|
94
|
+
const attention = stalledSet.has(team.name)
|
|
95
|
+
? '<span class="status-BLOCKED">STALLED</span>'
|
|
96
|
+
: remoteStalledSet.has(team.name)
|
|
97
|
+
? '<span class="status-BLOCKED">STALLED (remote)</span>'
|
|
98
|
+
: '';
|
|
99
|
+
return [
|
|
100
|
+
isRemote
|
|
101
|
+
? escapeHtml(team.name)
|
|
102
|
+
: `<a href="#" class="team-link" data-team="${escapeHtml(team.name)}" data-alias="${escapeHtml(team.leadAlias)}">${escapeHtml(team.name)}</a>`,
|
|
103
|
+
isRemote ? '(remote)' : escapeHtml(team.leadAlias),
|
|
104
|
+
isRemote ? '--' : escapeHtml(team.promptName),
|
|
105
|
+
`<span class="${statusClass(status)}">${escapeHtml(status)}</span>`,
|
|
106
|
+
escapeHtml(progressLabel),
|
|
107
|
+
escapeHtml(state?.changed_ts ?? '-'),
|
|
108
|
+
escapeHtml(liveLabel),
|
|
109
|
+
attention,
|
|
110
|
+
];
|
|
111
|
+
});
|
|
112
|
+
el.innerHTML = renderTable(
|
|
113
|
+
['Team', 'Lead', 'Prompt', 'Status(self-report)', 'Progress', 'Changed', 'Live', 'Attention'],
|
|
114
|
+
rows,
|
|
115
|
+
);
|
|
116
|
+
el.querySelectorAll('.team-link').forEach((link) => {
|
|
117
|
+
link.addEventListener('click', (event) => {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
$('#session-form [name="team"]').value = link.dataset.team;
|
|
120
|
+
$('#session-form [name="alias"]').value = link.dataset.alias;
|
|
121
|
+
$('#session-form').requestSubmit();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function renderQueue() {
|
|
130
|
+
const el = $('#queue-table');
|
|
131
|
+
try {
|
|
132
|
+
const body = await fetchJson('api/queue');
|
|
133
|
+
const rows = body.queue.map((row) => [
|
|
134
|
+
String(row.id),
|
|
135
|
+
escapeHtml(row.from_id),
|
|
136
|
+
escapeHtml(row.to_id),
|
|
137
|
+
escapeHtml(row.status),
|
|
138
|
+
escapeHtml(row.created_ts),
|
|
139
|
+
escapeHtml(row.sent_ts ?? row.retrieved_ts ?? '-'),
|
|
140
|
+
]);
|
|
141
|
+
el.innerHTML = renderTable(['ID', 'From', 'To', 'Status', 'Created', 'Sent/Retrieved'], rows);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function renderHistory() {
|
|
148
|
+
const el = $('#history-table');
|
|
149
|
+
const team = $('#history-team-filter').value.trim();
|
|
150
|
+
try {
|
|
151
|
+
const query = team ? `?team=${encodeURIComponent(team)}` : '';
|
|
152
|
+
const body = await fetchJson(`api/history${query}`);
|
|
153
|
+
const rows = body.events.map((row) => [
|
|
154
|
+
String(row.id),
|
|
155
|
+
escapeHtml(row.ts),
|
|
156
|
+
escapeHtml(row.team),
|
|
157
|
+
`<span class="${statusClass(row.status)}">${escapeHtml(row.status)}</span>`,
|
|
158
|
+
escapeHtml(row.note ?? ''),
|
|
159
|
+
]);
|
|
160
|
+
el.innerHTML = renderTable(['ID', 'TS', 'Team', 'Status', 'Note'], rows);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function renderSources() {
|
|
167
|
+
const el = $('#sources-table');
|
|
168
|
+
try {
|
|
169
|
+
const body = await fetchJson('api/sources');
|
|
170
|
+
const rows = [
|
|
171
|
+
['-', escapeHtml(body.defaultTeamsConfPath), '(default, always scanned)', ''],
|
|
172
|
+
...body.sources.map((s) => [
|
|
173
|
+
String(s.id),
|
|
174
|
+
escapeHtml(s.path),
|
|
175
|
+
escapeHtml(s.label ?? ''),
|
|
176
|
+
`<button data-remove-path="${escapeHtml(s.path)}">Remove</button>`,
|
|
177
|
+
]),
|
|
178
|
+
];
|
|
179
|
+
el.innerHTML = renderTable(['ID', 'Path', 'Label', ''], rows);
|
|
180
|
+
el.querySelectorAll('[data-remove-path]').forEach((button) => {
|
|
181
|
+
button.addEventListener('click', async () => {
|
|
182
|
+
await fetchJson(`api/sources?path=${encodeURIComponent(button.dataset.removePath)}`, {
|
|
183
|
+
method: 'DELETE',
|
|
184
|
+
});
|
|
185
|
+
renderSources();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
} catch (err) {
|
|
189
|
+
el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function refreshAll() {
|
|
194
|
+
$('#generated-at').textContent = `updated ${new Date().toISOString()}`;
|
|
195
|
+
await Promise.all([renderBoard(), renderQueue(), renderHistory(), renderSources()]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
$('#refresh-all').addEventListener('click', refreshAll);
|
|
199
|
+
$('#history-team-filter').addEventListener('change', renderHistory);
|
|
200
|
+
|
|
201
|
+
$('#send-form').addEventListener('submit', async (event) => {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
const form = new FormData(event.target);
|
|
204
|
+
const resultEl = $('#send-result');
|
|
205
|
+
resultEl.textContent = 'sending...';
|
|
206
|
+
try {
|
|
207
|
+
const body = await fetchJson('api/send', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'content-type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({ team: form.get('team'), prompt: form.get('prompt') }),
|
|
211
|
+
});
|
|
212
|
+
resultEl.textContent = JSON.stringify(body, null, 2);
|
|
213
|
+
renderBoard();
|
|
214
|
+
renderHistory();
|
|
215
|
+
} catch (err) {
|
|
216
|
+
resultEl.textContent = `error: ${err.message}`;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
$('#enqueue-form').addEventListener('submit', async (event) => {
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
const form = new FormData(event.target);
|
|
223
|
+
const team = form.get('to');
|
|
224
|
+
try {
|
|
225
|
+
await fetchJson('api/enqueue', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: { 'content-type': 'application/json' },
|
|
228
|
+
body: JSON.stringify({ to: team, prompt: form.get('prompt'), from: 'operator' }),
|
|
229
|
+
});
|
|
230
|
+
event.target.reset();
|
|
231
|
+
renderQueue();
|
|
232
|
+
|
|
233
|
+
const teamInfo = teamsCache.find((t) => t.name === team);
|
|
234
|
+
const alias = teamInfo?.leadAlias ?? 'Lead';
|
|
235
|
+
$('#session-form [name="team"]').value = team;
|
|
236
|
+
$('#session-form [name="alias"]').value = alias;
|
|
237
|
+
pollForReply(team, alias);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
alert(`enqueue failed: ${err.message}`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
$('#deliver-btn').addEventListener('click', async () => {
|
|
244
|
+
try {
|
|
245
|
+
const body = await fetchJson('api/queue/deliver', { method: 'POST' });
|
|
246
|
+
renderQueue();
|
|
247
|
+
renderBoard();
|
|
248
|
+
alert(`dispatched ${body.delivered} pending prompt(s)`);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
alert(`deliver failed: ${err.message}`);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
$('#source-form').addEventListener('submit', async (event) => {
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
const form = new FormData(event.target);
|
|
257
|
+
try {
|
|
258
|
+
await fetchJson('api/sources', {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: { 'content-type': 'application/json' },
|
|
261
|
+
body: JSON.stringify({ path: form.get('path'), label: form.get('label') || undefined }),
|
|
262
|
+
});
|
|
263
|
+
event.target.reset();
|
|
264
|
+
renderSources();
|
|
265
|
+
renderBoard();
|
|
266
|
+
} catch (err) {
|
|
267
|
+
alert(`add source failed: ${err.message}`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
async function loadSessionSummary(team, alias) {
|
|
272
|
+
// Best-effort: `lmctl session` loads a team's ENTIRE session before slicing, which can exceed our
|
|
273
|
+
// exec maxBuffer for a long-running team (live-confirmed against math.lmctl) -- a failure here must
|
|
274
|
+
// never block the transcript view (loadTranscript, below), which is the reliable one.
|
|
275
|
+
const el = $('#session-detail');
|
|
276
|
+
el.innerHTML = '<p class="muted">loading...</p>';
|
|
277
|
+
try {
|
|
278
|
+
const body = await fetchJson(
|
|
279
|
+
`api/session/${encodeURIComponent(team)}/${encodeURIComponent(alias)}?limit=10`,
|
|
280
|
+
);
|
|
281
|
+
const tokens = body.tokens?.aggregate ?? {};
|
|
282
|
+
const summaryRows = [
|
|
283
|
+
['Provider', escapeHtml(body.provider ?? '?')],
|
|
284
|
+
['Session ID', escapeHtml(body.sessionId ?? '?')],
|
|
285
|
+
['Modified', escapeHtml(body.modifiedAt ?? '?')],
|
|
286
|
+
['Handled', `${body.handled} (${escapeHtml(body.handledSignal ?? '?')})`],
|
|
287
|
+
['Tokens (aggregate)', `in ${tokens.input ?? 0} / out ${tokens.output ?? 0}`],
|
|
288
|
+
['Tool calls', String((body.toolCalls ?? []).length)],
|
|
289
|
+
['Tracked invocations', String((body.invocations ?? []).length)],
|
|
290
|
+
['Last prompt', escapeHtml((body.lastPrompt ?? '').slice(0, 200))],
|
|
291
|
+
];
|
|
292
|
+
el.innerHTML = renderTable(['Field', 'Value'], summaryRows);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function renderTranscriptTurns(turns, newFromIndex) {
|
|
299
|
+
if (turns.length === 0) return '<p class="muted">(no turns)</p>';
|
|
300
|
+
return (
|
|
301
|
+
'<div class="transcript">' +
|
|
302
|
+
turns
|
|
303
|
+
.map((turn, i) => {
|
|
304
|
+
const cls = i >= newFromIndex ? 'transcript-turn turn-new' : 'transcript-turn';
|
|
305
|
+
return (
|
|
306
|
+
`<div class="${cls}">` +
|
|
307
|
+
`<div class="turn-head"><span>${escapeHtml(turn.role)}</span><span>${escapeHtml(turn.ts ?? '')}</span></div>` +
|
|
308
|
+
`<div class="turn-text">${escapeHtml(turn.text)}</div>` +
|
|
309
|
+
'</div>'
|
|
310
|
+
);
|
|
311
|
+
})
|
|
312
|
+
.join('') +
|
|
313
|
+
'</div>'
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Fetches + renders the real request/response transcript for one team member. Returns the fetched
|
|
318
|
+
* turns (or null on error) so callers -- e.g. the enqueue reply-poller -- can detect new turns. */
|
|
319
|
+
async function loadTranscript(team, alias, lines, newFromIndex = Infinity) {
|
|
320
|
+
const el = $('#transcript-view');
|
|
321
|
+
try {
|
|
322
|
+
const body = await fetchJson(
|
|
323
|
+
`api/transcript?team=${encodeURIComponent(team)}&alias=${encodeURIComponent(alias)}&lines=${lines || 40}`,
|
|
324
|
+
);
|
|
325
|
+
el.innerHTML = renderTranscriptTurns(body.turns, newFromIndex);
|
|
326
|
+
return body.turns;
|
|
327
|
+
} catch (err) {
|
|
328
|
+
el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let activeReplyPoll = null;
|
|
334
|
+
|
|
335
|
+
/** After an operator enqueue, watch that team's transcript for the reply to land (delivered by the
|
|
336
|
+
* in-server cron, then answered by the target's own LLM turn) and highlight it on the page --
|
|
337
|
+
* the "operator can drive a team from the web app" loop the mission brief asks for (function 3). */
|
|
338
|
+
function pollForReply(team, alias) {
|
|
339
|
+
if (activeReplyPoll) clearInterval(activeReplyPoll.timer);
|
|
340
|
+
const statusEl = $('#enqueue-poll-status');
|
|
341
|
+
const POLL_MS = 5000;
|
|
342
|
+
const MAX_ATTEMPTS = 60; // ~5 minutes
|
|
343
|
+
|
|
344
|
+
loadTranscript(team, alias, 40).then((baseline) => {
|
|
345
|
+
if (baseline === null) return; // error already rendered by loadTranscript
|
|
346
|
+
const baselineLength = baseline.length;
|
|
347
|
+
let attempts = 0;
|
|
348
|
+
statusEl.textContent = `queued to ${team} -- watching for a new reply...`;
|
|
349
|
+
const timer = setInterval(async () => {
|
|
350
|
+
attempts++;
|
|
351
|
+
const turns = await loadTranscript(team, alias, 40, baselineLength);
|
|
352
|
+
if (turns === null) return;
|
|
353
|
+
if (turns.length > baselineLength) {
|
|
354
|
+
clearInterval(timer);
|
|
355
|
+
activeReplyPoll = null;
|
|
356
|
+
statusEl.textContent = `new reply from ${team} landed (see the transcript below).`;
|
|
357
|
+
} else if (attempts >= MAX_ATTEMPTS) {
|
|
358
|
+
clearInterval(timer);
|
|
359
|
+
activeReplyPoll = null;
|
|
360
|
+
statusEl.textContent =
|
|
361
|
+
`still waiting on ${team} after 5 minutes -- stopped polling (it may still be running; ` +
|
|
362
|
+
`click "Inspect" in Active session review to check manually).`;
|
|
363
|
+
}
|
|
364
|
+
}, POLL_MS);
|
|
365
|
+
activeReplyPoll = { team, timer };
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
$('#session-form').addEventListener('submit', async (event) => {
|
|
370
|
+
event.preventDefault();
|
|
371
|
+
const form = new FormData(event.target);
|
|
372
|
+
const team = form.get('team');
|
|
373
|
+
const alias = form.get('alias');
|
|
374
|
+
const lines = Number(form.get('lines')) || 40;
|
|
375
|
+
$('#transcript-status').textContent = '';
|
|
376
|
+
await Promise.all([loadSessionSummary(team, alias), loadTranscript(team, alias, lines)]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
refreshAll();
|
|
@@ -0,0 +1,78 @@
|
|
|
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>lmauto</title>
|
|
7
|
+
<link rel="stylesheet" href="styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="page-shell">
|
|
11
|
+
<header class="toolbar">
|
|
12
|
+
<h1>lmauto</h1>
|
|
13
|
+
<button id="refresh-all">Refresh</button>
|
|
14
|
+
<span id="generated-at" class="muted"></span>
|
|
15
|
+
</header>
|
|
16
|
+
|
|
17
|
+
<main class="layout">
|
|
18
|
+
<section class="panel" id="panel-board">
|
|
19
|
+
<h2>Team board (S1↔S2)</h2>
|
|
20
|
+
<div id="board-table"></div>
|
|
21
|
+
</section>
|
|
22
|
+
|
|
23
|
+
<section class="panel" id="panel-send">
|
|
24
|
+
<h2>Send as operator</h2>
|
|
25
|
+
<form id="send-form">
|
|
26
|
+
<label>Team <input type="text" name="team" required /></label>
|
|
27
|
+
<label>Prompt <textarea name="prompt" rows="4" required></textarea></label>
|
|
28
|
+
<button type="submit">Send</button>
|
|
29
|
+
</form>
|
|
30
|
+
<pre id="send-result" class="result"></pre>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
<section class="panel" id="panel-queue">
|
|
34
|
+
<h2>Queue — drive a team (e.g. <code>math</code>, the meta-lead) via the outbox</h2>
|
|
35
|
+
<form id="enqueue-form">
|
|
36
|
+
<label>To <input type="text" name="to" required /></label>
|
|
37
|
+
<label>Prompt <textarea name="prompt" rows="2" required></textarea></label>
|
|
38
|
+
<button type="submit">Enqueue</button>
|
|
39
|
+
<button type="button" id="deliver-btn">Deliver pending</button>
|
|
40
|
+
</form>
|
|
41
|
+
<p id="enqueue-poll-status" class="muted"></p>
|
|
42
|
+
<div id="queue-table"></div>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<section class="panel" id="panel-history">
|
|
46
|
+
<h2>History</h2>
|
|
47
|
+
<label>Team filter <input type="text" id="history-team-filter" placeholder="(all)" /></label>
|
|
48
|
+
<div id="history-table"></div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<section class="panel" id="panel-sources">
|
|
52
|
+
<h2>Registry sources</h2>
|
|
53
|
+
<form id="source-form">
|
|
54
|
+
<label>Path <input type="text" name="path" required /></label>
|
|
55
|
+
<label>Label <input type="text" name="label" /></label>
|
|
56
|
+
<button type="submit">Add source</button>
|
|
57
|
+
</form>
|
|
58
|
+
<div id="sources-table"></div>
|
|
59
|
+
</section>
|
|
60
|
+
|
|
61
|
+
<section class="panel" id="panel-session">
|
|
62
|
+
<h2>Active session review</h2>
|
|
63
|
+
<form id="session-form">
|
|
64
|
+
<label>Team <input type="text" name="team" required /></label>
|
|
65
|
+
<label>Alias <input type="text" name="alias" value="Lead" required /></label>
|
|
66
|
+
<label>Lines <input type="number" name="lines" value="40" min="1" style="width: 5em" /></label>
|
|
67
|
+
<button type="submit">Inspect</button>
|
|
68
|
+
</form>
|
|
69
|
+
<div id="session-detail"></div>
|
|
70
|
+
<h3>Transcript (prompts + replies, real request/response history)</h3>
|
|
71
|
+
<p id="transcript-status" class="muted"></p>
|
|
72
|
+
<div id="transcript-view"></div>
|
|
73
|
+
</section>
|
|
74
|
+
</main>
|
|
75
|
+
</div>
|
|
76
|
+
<script src="app.js"></script>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
--border: #d0d0d0;
|
|
4
|
+
--muted: #666;
|
|
5
|
+
--bg-panel: rgba(127, 127, 127, 0.06);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 1rem;
|
|
16
|
+
line-height: 1.4;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.page-shell {
|
|
20
|
+
max-width: 1400px;
|
|
21
|
+
margin: 0 auto;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.toolbar {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 1rem;
|
|
28
|
+
margin-bottom: 1rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.toolbar h1 {
|
|
32
|
+
margin: 0;
|
|
33
|
+
font-size: 1.25rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.muted {
|
|
37
|
+
color: var(--muted);
|
|
38
|
+
font-size: 0.85rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.layout {
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
|
44
|
+
gap: 1rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.panel {
|
|
48
|
+
border: 1px solid var(--border);
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
padding: 1rem;
|
|
51
|
+
background: var(--bg-panel);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.panel h2 {
|
|
55
|
+
margin-top: 0;
|
|
56
|
+
font-size: 1rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
table {
|
|
60
|
+
border-collapse: collapse;
|
|
61
|
+
width: 100%;
|
|
62
|
+
font-size: 0.85rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
th,
|
|
66
|
+
td {
|
|
67
|
+
border-bottom: 1px solid var(--border);
|
|
68
|
+
padding: 0.35rem 0.5rem;
|
|
69
|
+
text-align: left;
|
|
70
|
+
vertical-align: top;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
form {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: 0.5rem;
|
|
77
|
+
margin-bottom: 0.75rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
form label {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
gap: 0.25rem;
|
|
84
|
+
font-size: 0.85rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
input,
|
|
88
|
+
textarea,
|
|
89
|
+
button {
|
|
90
|
+
font: inherit;
|
|
91
|
+
padding: 0.35rem 0.5rem;
|
|
92
|
+
border: 1px solid var(--border);
|
|
93
|
+
border-radius: 4px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
button {
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
width: fit-content;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.result {
|
|
102
|
+
white-space: pre-wrap;
|
|
103
|
+
background: rgba(127, 127, 127, 0.1);
|
|
104
|
+
padding: 0.5rem;
|
|
105
|
+
border-radius: 4px;
|
|
106
|
+
font-size: 0.8rem;
|
|
107
|
+
max-height: 200px;
|
|
108
|
+
overflow: auto;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.error {
|
|
112
|
+
color: #c0392b;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.status-Running {
|
|
116
|
+
color: #2c7be5;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.status-BLOCKED {
|
|
120
|
+
color: #c0392b;
|
|
121
|
+
font-weight: bold;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.status-DONE {
|
|
125
|
+
color: #1a9850;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.transcript {
|
|
129
|
+
max-height: 420px;
|
|
130
|
+
overflow: auto;
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
gap: 0.5rem;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.transcript-turn {
|
|
137
|
+
border: 1px solid var(--border);
|
|
138
|
+
border-radius: 4px;
|
|
139
|
+
padding: 0.4rem 0.6rem;
|
|
140
|
+
font-size: 0.82rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.transcript-turn.turn-new {
|
|
144
|
+
border-color: #1a9850;
|
|
145
|
+
background: rgba(26, 152, 80, 0.08);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.transcript-turn .turn-head {
|
|
149
|
+
display: flex;
|
|
150
|
+
justify-content: space-between;
|
|
151
|
+
gap: 0.5rem;
|
|
152
|
+
font-size: 0.75rem;
|
|
153
|
+
color: var(--muted);
|
|
154
|
+
margin-bottom: 0.25rem;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.transcript-turn .turn-text {
|
|
158
|
+
white-space: pre-wrap;
|
|
159
|
+
word-break: break-word;
|
|
160
|
+
}
|