great-cto 3.58.0 → 3.58.2
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/board/.claude-plugin/plugin.json +1 -1
- package/board/packages/board/lib/config.mjs +10 -1
- package/board/packages/board/lib/routes.mjs +11 -3
- package/board/packages/board/public/index.html +17 -5
- package/board/scripts/lib/session-usage.mjs +17 -2
- package/dist/statusline.js +3 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
3
|
"description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 72 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
|
|
4
|
-
"version": "3.58.
|
|
4
|
+
"version": "3.58.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -4,7 +4,16 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
|
|
7
|
+
// `--port N` (or `--port=N`) first — server.mjs's usage line documents it, and
|
|
8
|
+
// ignoring it put a test board started with `--port 3177` onto :3141 (3.58.1).
|
|
9
|
+
// Then BOARD_PORT, PORT, and 3141. A value that is not a port is skipped.
|
|
10
|
+
export function portFrom(argv = process.argv, env = process.env) {
|
|
11
|
+
const ok = (v) => { const n = Number(v); return Number.isInteger(n) && n > 0 && n < 65536 ? n : null; };
|
|
12
|
+
const i = argv.indexOf('--port');
|
|
13
|
+
const eq = argv.find((a) => typeof a === 'string' && a.startsWith('--port='));
|
|
14
|
+
return ok(i > -1 ? argv[i + 1] : eq ? eq.slice(7) : null) ?? ok(env.BOARD_PORT) ?? ok(env.PORT) ?? 3141;
|
|
15
|
+
}
|
|
16
|
+
const PORT = portFrom();
|
|
8
17
|
const PUBLIC = path.join(__dirname, '..', 'public');
|
|
9
18
|
// Build version — read the plugin manifest this board ships inside (packages/board → ../../.claude-plugin).
|
|
10
19
|
const BUILD_VERSION = (() => {
|
|
@@ -13,7 +13,8 @@ import { sseClients, notifHistory } from './state.mjs';
|
|
|
13
13
|
import { autoRegisterProject, listProjects, resolveProjectCwd, resolveProjectInfo, getChangeTier, readProjectsRegistry, getRegistryDegradation } from './projects.mjs';
|
|
14
14
|
import { readVerdictsWithHealth } from './verdicts.mjs';
|
|
15
15
|
import { agentUsage, usageSnapshot } from '../../../scripts/lib/agent-usage.mjs';
|
|
16
|
-
import {
|
|
16
|
+
import { isProjectState } from '../../../scripts/lib/great-cto-scope.mjs';
|
|
17
|
+
import { usageIndexSnapshot, summarizeUsage, readCodexTitles, readClaudeLimits, claudeRecorder } from '../../../scripts/lib/session-usage.mjs';
|
|
17
18
|
import { outcomes as computeOutcomes } from '../../../scripts/lib/outcomes.mjs';
|
|
18
19
|
import { reviewerStatus } from '../../../scripts/lib/required-reviewers.mjs';
|
|
19
20
|
import { readSessionStatus } from '../../../scripts/lib/session-status.mjs';
|
|
@@ -276,6 +277,13 @@ async function dispatch(req, res, url, cwd) {
|
|
|
276
277
|
return;
|
|
277
278
|
}
|
|
278
279
|
const view = String(parsed.view || '');
|
|
280
|
+
// Only into a project. The board's cwd can be a directory that is not one —
|
|
281
|
+
// a release worktree, deleted since — and mkdir here recreated it (3.58.1).
|
|
282
|
+
if (!isProjectState(path.join(c, '.great_cto'))) {
|
|
283
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
284
|
+
res.end(JSON.stringify({ ok: true, view, recorded: false, why: 'not a project' }));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
279
287
|
try {
|
|
280
288
|
recordView({ root: c, view });
|
|
281
289
|
} catch (e) {
|
|
@@ -284,7 +292,7 @@ async function dispatch(req, res, url, cwd) {
|
|
|
284
292
|
return;
|
|
285
293
|
}
|
|
286
294
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
287
|
-
res.end(JSON.stringify({ ok: true, view }));
|
|
295
|
+
res.end(JSON.stringify({ ok: true, view, recorded: true }));
|
|
288
296
|
});
|
|
289
297
|
return true;
|
|
290
298
|
}
|
|
@@ -1507,7 +1515,7 @@ async function dispatch(req, res, url, cwd) {
|
|
|
1507
1515
|
if (snap.state !== 'ready') {
|
|
1508
1516
|
body = { state: snap.state, why: snap.why };
|
|
1509
1517
|
} else {
|
|
1510
|
-
const sum = summarizeUsage(snap.index, { days, codexTitles: readCodexTitles(), claudeReadings: readClaudeLimits() });
|
|
1518
|
+
const sum = summarizeUsage(snap.index, { days, codexTitles: readCodexTitles(), claudeReadings: readClaudeLimits(), claudeRecorderState: claudeRecorder() });
|
|
1511
1519
|
const ours = new Set(boardAgentNames());
|
|
1512
1520
|
for (const host of Object.keys(sum.lists)) {
|
|
1513
1521
|
sum.lists[host].agents = sum.lists[host].agents.map((a) => ({ ...a, ours: ours.has(a.name) }));
|
|
@@ -6091,8 +6091,8 @@ function usageHostCard(d, host) {
|
|
|
6091
6091
|
const meters = P ? meter(P.fiveHour) + meter(P.weekly) : '';
|
|
6092
6092
|
limit = meters
|
|
6093
6093
|
? `<div class="usage-limit">${meters}<div class="usage-sub">as Claude Code reported it to the status line, ${esc(relTime(new Date(Math.max(...Object.values(P).map((l) => (l.current && l.current.readAt) || 0))).toISOString()))} ago · ${hits.plan} refused at a limit in ${d.window.days} days</div></div>`
|
|
6094
|
-
: `<div class="usage-limit"><div class="usage-limit-head"><span>Plan limit</span>${absent('unloaded',
|
|
6095
|
-
<div class="usage-sub">${hits.plan} refused at the plan limit · ${hits.server} server-side throttles (not your limit) in ${d.window.days} days · plan percentage: <code>great-cto statusline install</code
|
|
6094
|
+
: `<div class="usage-limit"><div class="usage-limit-head"><span>Plan limit</span>${absent('unloaded', usageRecorderHint(d).short)}</div>
|
|
6095
|
+
<div class="usage-sub">${hits.plan} refused at the plan limit · ${hits.server} server-side throttles (not your limit) in ${d.window.days} days · plan percentage: ${d.limits && d.limits.claude && d.limits.claude.recorder === 'on' ? 'from <code>claude</code> in a terminal' : '<code>great-cto statusline install</code>'}</div></div>`;
|
|
6096
6096
|
}
|
|
6097
6097
|
return `<section class="usage-card" data-host="${host}">
|
|
6098
6098
|
<h3><span>${name}</span><span class="muted">${esc(d.window.from)} → ${esc(d.window.to)}</span></h3>
|
|
@@ -6205,9 +6205,21 @@ function usageLineSvg({ from, to, points, max, marks = [], ceiling = null, label
|
|
|
6205
6205
|
<line class="grid" x1="0" x2="${W}" y1="${H}" y2="${H}"></line>${ceil}${ticks}<path class="ln" d="${path}"></path></svg>`;
|
|
6206
6206
|
}
|
|
6207
6207
|
|
|
6208
|
+
// Plan percentage reaches only a status line Claude Code draws: the terminal
|
|
6209
|
+
// `claude` draws one, the desktop app's Code tab does not. So "recorder on, no
|
|
6210
|
+
// reading yet" gets its own words — sending the operator to install what they
|
|
6211
|
+
// already installed is the hint that misled on the first day.
|
|
6212
|
+
function usageRecorderHint(d) {
|
|
6213
|
+
const on = d.limits && d.limits.claude && d.limits.claude.recorder === 'on';
|
|
6214
|
+
return on
|
|
6215
|
+
? { short: 'recorder on, no reading yet — readings come from claude in a terminal', long: 'The recorder is on and has no reading yet. Claude Code passes its plan percentage only to a status line it draws: the terminal <code>claude</code> does, the desktop app\'s Code tab does not. One terminal session fills this in.' }
|
|
6216
|
+
: { short: 'Claude Code reports plan use only to the status line — turn on the recorder: great-cto statusline install', long: 'Claude Code hands its plan percentage to the status line and to nothing else. <code>great-cto statusline install</code> records it here and keeps your own status line; readings come from <code>claude</code> in a terminal.' };
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6208
6219
|
function usageClaudeLimits(d) {
|
|
6209
|
-
const
|
|
6210
|
-
const
|
|
6220
|
+
const L0 = d.limits && d.limits.claude;
|
|
6221
|
+
const plan = L0 && L0.plan;
|
|
6222
|
+
const head = `<h3><span>Claude Code limits</span><span class="muted">${plan ? 'as Claude Code reported them to the status line' : L0 && L0.recorder === 'on' ? 'from refusals — plan percentages arrive from claude in a terminal' : 'from refusals — turn on the status line recorder for plan percentages'}</span></h3>`;
|
|
6211
6223
|
if (!d.seen || !d.seen.claude) return `<section class="usage-card" data-limits="claude">${head}<p class="usage-note">No Claude Code logs on this machine.</p></section>`;
|
|
6212
6224
|
const L = d.limits && d.limits.claude;
|
|
6213
6225
|
if (!L) return `<section class="usage-card" data-limits="claude">${head}<p class="usage-note">Not computed.</p></section>`;
|
|
@@ -6229,7 +6241,7 @@ function usageClaudeLimits(d) {
|
|
|
6229
6241
|
const CLAUDE_LANE = { fiveHour: '5-hour limit', weekly: 'Weekly limit', seven_day_opus: 'Weekly limit · Opus', seven_day_sonnet: 'Weekly limit · Sonnet', seven_day_oauth_apps: 'Weekly limit · apps' };
|
|
6230
6242
|
const lanes = plan
|
|
6231
6243
|
? ['fiveHour', 'weekly', ...Object.keys(plan).filter((k) => k !== 'fiveHour' && k !== 'weekly')].filter((k) => plan[k]).map((k) => usageCodexLane(k, plan[k], d, CLAUDE_LANE[k] || k)).join('')
|
|
6232
|
-
: `<p class="usage-note"
|
|
6244
|
+
: `<p class="usage-note">${usageRecorderHint(d).long}</p>`;
|
|
6233
6245
|
return `<section class="usage-card" data-limits="claude">${head}
|
|
6234
6246
|
${lanes}
|
|
6235
6247
|
<div class="usage-stats">${stat(esc(fmtUsdEq(L.fiveHour.usd)), 'last 5 hours, $ list')}${stat(esc(fmtUsdEq(L.sevenDay.usd)), 'last 7 days, $ list')}${stat(esc(String(hits.plan || 0)), `refusals in ${d.window.days} days`)}</div>
|
|
@@ -744,6 +744,21 @@ export function readClaudeLimits(file = path.join(home(), '.great_cto', 'claude-
|
|
|
744
744
|
return out;
|
|
745
745
|
}
|
|
746
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Is the plan-use recorder on? `great-cto statusline install` points Claude Code's
|
|
749
|
+
* statusLine at ~/.great_cto/statusline.mjs. On with no reading yet is not the
|
|
750
|
+
* same as never installed: only a status line Claude Code DRAWS gets the plan
|
|
751
|
+
* percentage — the terminal `claude` draws one, the desktop app's Code tab does
|
|
752
|
+
* not — so the board must say which of the two the operator is in.
|
|
753
|
+
*/
|
|
754
|
+
export function claudeRecorder(homeDir = home()) {
|
|
755
|
+
try {
|
|
756
|
+
const s = JSON.parse(fs.readFileSync(path.join(homeDir, '.claude', 'settings.json'), 'utf8'));
|
|
757
|
+
const cmd = s && s.statusLine && s.statusLine.command;
|
|
758
|
+
return typeof cmd === 'string' && /\.great_cto[\\/]statusline\.mjs/.test(cmd) ? 'on' : 'off';
|
|
759
|
+
} catch { return 'off'; }
|
|
760
|
+
}
|
|
761
|
+
|
|
747
762
|
/**
|
|
748
763
|
* Plan windows from readings [{t, lane, minutes, used, resets}]: every window the
|
|
749
764
|
* period touched with its peak and when it filled, the readings to draw, and —
|
|
@@ -808,7 +823,7 @@ function laneDetail(samples, { fromMs, now }) {
|
|
|
808
823
|
* @param {object} index from scanUsage
|
|
809
824
|
* @param {{days?:number, now?:number, codexTitles?:Record<string,string>, prices?:object, top?:number}} opts
|
|
810
825
|
*/
|
|
811
|
-
export function summarizeUsage(index, { days = 30, now = Date.now(), codexTitles = {}, prices = effectivePrices(), top = 10, claudeReadings = [] } = {}) {
|
|
826
|
+
export function summarizeUsage(index, { days = 30, now = Date.now(), codexTitles = {}, prices = effectivePrices(), top = 10, claudeReadings = [], claudeRecorderState = 'off' } = {}) {
|
|
812
827
|
const span = Math.max(1, Math.min(365, Math.floor(days) || 30));
|
|
813
828
|
const dates = [];
|
|
814
829
|
for (let i = span - 1; i >= 0; i--) dates.push(dayOf(now - i * 86400000));
|
|
@@ -943,7 +958,7 @@ export function summarizeUsage(index, { days = 30, now = Date.now(), codexTitles
|
|
|
943
958
|
}])),
|
|
944
959
|
limits: {
|
|
945
960
|
codex: codexLimits ? { ...codexLimits, plan: codexLimits.plan || codexPlan?.plan || null, detail: codexLimitDetail(codexPoints, { fromMs, now }) } : null,
|
|
946
|
-
claude: { ...claudeLimits(hourly.claude, { fromMs, now, lastHit }), plan: claudePlanDetail(claudeReadings, { fromMs, now }) },
|
|
961
|
+
claude: { ...claudeLimits(hourly.claude, { fromMs, now, lastHit }), plan: claudePlanDetail(claudeReadings, { fromMs, now }), recorder: claudeRecorderState },
|
|
947
962
|
},
|
|
948
963
|
unpricedModels: [...unpriced].sort(),
|
|
949
964
|
// Claude Code only: Codex does not write hook outcomes to its session logs.
|
package/dist/statusline.js
CHANGED
|
@@ -117,6 +117,7 @@ export function runStatuslineCommand(argv) {
|
|
|
117
117
|
}
|
|
118
118
|
success(`${r.message} ${dim(r.backup ? `(settings backup: ${r.backup})` : "")}`);
|
|
119
119
|
console.log(" Claude's plan use is recorded on the next status line refresh; the board shows it under Usage → Limits.");
|
|
120
|
+
console.log(" Readings come from `claude` in a terminal: the desktop app's Code tab does not draw a status line.");
|
|
120
121
|
console.log(` Undo: ${dim("great-cto statusline uninstall")}`);
|
|
121
122
|
return 0;
|
|
122
123
|
}
|
|
@@ -135,6 +136,8 @@ export function runStatuslineCommand(argv) {
|
|
|
135
136
|
if (st.chain)
|
|
136
137
|
console.log(`your status line (shown through it): ${st.chain}`);
|
|
137
138
|
console.log(`last reading: ${st.lastReading ? JSON.stringify(st.lastReading) : "none yet"}`);
|
|
139
|
+
if (st.installed && !st.lastReading)
|
|
140
|
+
console.log(" (readings come from `claude` in a terminal; the desktop app's Code tab does not draw a status line)");
|
|
138
141
|
return 0;
|
|
139
142
|
}
|
|
140
143
|
warn(`unknown: great-cto statusline ${sub} — use install, uninstall or status`);
|
package/package.json
CHANGED