team 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/dist/approve/approval.d.ts +38 -0
- package/dist/approve/approval.js +78 -0
- package/dist/approve/diff.d.ts +13 -0
- package/dist/approve/diff.js +47 -0
- package/dist/approve/fingerprint.d.ts +44 -0
- package/dist/approve/fingerprint.js +109 -0
- package/dist/args.d.ts +7 -0
- package/dist/args.js +22 -0
- package/dist/budgets/checks.d.ts +35 -0
- package/dist/budgets/checks.js +77 -0
- package/dist/budgets/gate.d.ts +20 -0
- package/dist/budgets/gate.js +96 -0
- package/dist/budgets/readings.d.ts +47 -0
- package/dist/budgets/readings.js +119 -0
- package/dist/budgets/run.d.ts +66 -0
- package/dist/budgets/run.js +119 -0
- package/dist/budgets/table.d.ts +24 -0
- package/dist/budgets/table.js +90 -0
- package/dist/caller.d.ts +48 -0
- package/dist/caller.js +126 -0
- package/dist/check/config.d.ts +51 -0
- package/dist/check/config.js +50 -0
- package/dist/check/git.d.ts +27 -0
- package/dist/check/git.js +97 -0
- package/dist/check/message.d.ts +27 -0
- package/dist/check/message.js +101 -0
- package/dist/check/run.d.ts +31 -0
- package/dist/check/run.js +77 -0
- package/dist/check/signature.d.ts +10 -0
- package/dist/check/signature.js +22 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +76 -0
- package/dist/clis.d.ts +7 -0
- package/dist/clis.js +9 -0
- package/dist/commands/add.d.ts +23 -0
- package/dist/commands/add.js +445 -0
- package/dist/commands/approve.d.ts +11 -0
- package/dist/commands/approve.js +146 -0
- package/dist/commands/check.d.ts +17 -0
- package/dist/commands/check.js +103 -0
- package/dist/commands/doctor.d.ts +38 -0
- package/dist/commands/doctor.js +232 -0
- package/dist/commands/down.d.ts +37 -0
- package/dist/commands/down.js +258 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +138 -0
- package/dist/commands/remove.d.ts +23 -0
- package/dist/commands/remove.js +235 -0
- package/dist/commands/status.d.ts +41 -0
- package/dist/commands/status.js +147 -0
- package/dist/commands/up.d.ts +49 -0
- package/dist/commands/up.js +361 -0
- package/dist/commands/watch.d.ts +35 -0
- package/dist/commands/watch.js +302 -0
- package/dist/commands/worktree.d.ts +12 -0
- package/dist/commands/worktree.js +388 -0
- package/dist/end/condition.d.ts +28 -0
- package/dist/end/condition.js +77 -0
- package/dist/file/current.d.ts +13 -0
- package/dist/file/current.js +53 -0
- package/dist/file/lines.d.ts +25 -0
- package/dist/file/lines.js +277 -0
- package/dist/file/load.d.ts +7 -0
- package/dist/file/load.js +90 -0
- package/dist/file/paths.d.ts +6 -0
- package/dist/file/paths.js +77 -0
- package/dist/file/secrets.d.ts +6 -0
- package/dist/file/secrets.js +54 -0
- package/dist/file/signature.d.ts +13 -0
- package/dist/file/signature.js +36 -0
- package/dist/file/types.d.ts +130 -0
- package/dist/file/types.js +1 -0
- package/dist/file/validate.d.ts +13 -0
- package/dist/file/validate.js +702 -0
- package/dist/file/write.d.ts +11 -0
- package/dist/file/write.js +13 -0
- package/dist/herdr.d.ts +38 -0
- package/dist/herdr.js +227 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/io.d.ts +10 -0
- package/dist/io.js +1 -0
- package/dist/launch/deliver.d.ts +10 -0
- package/dist/launch/deliver.js +39 -0
- package/dist/launch/execute.d.ts +46 -0
- package/dist/launch/execute.js +284 -0
- package/dist/launch/plan.d.ts +185 -0
- package/dist/launch/plan.js +280 -0
- package/dist/launch/rules.d.ts +25 -0
- package/dist/launch/rules.js +36 -0
- package/dist/log.d.ts +1 -0
- package/dist/log.js +24 -0
- package/dist/profiles/antigravity.yaml +66 -0
- package/dist/profiles/claude-code.yaml +69 -0
- package/dist/profiles/codex.yaml +77 -0
- package/dist/profiles/cursor.yaml +68 -0
- package/dist/profiles/index.d.ts +1 -0
- package/dist/profiles/index.js +1 -0
- package/dist/profiles/profile.d.ts +60 -0
- package/dist/profiles/profile.js +313 -0
- package/dist/profiles/quota.d.ts +22 -0
- package/dist/profiles/quota.js +56 -0
- package/dist/state.d.ts +59 -0
- package/dist/state.js +127 -0
- package/dist/status/compare.d.ts +26 -0
- package/dist/status/compare.js +110 -0
- package/dist/status/statusline.d.ts +9 -0
- package/dist/status/statusline.js +26 -0
- package/dist/store/store.d.ts +62 -0
- package/dist/store/store.js +116 -0
- package/dist/watch/check.d.ts +82 -0
- package/dist/watch/check.js +29 -0
- package/dist/watch/checks/approval.d.ts +2 -0
- package/dist/watch/checks/approval.js +16 -0
- package/dist/watch/checks/attention.d.ts +2 -0
- package/dist/watch/checks/attention.js +23 -0
- package/dist/watch/checks/budget.d.ts +2 -0
- package/dist/watch/checks/budget.js +141 -0
- package/dist/watch/checks/disk.d.ts +2 -0
- package/dist/watch/checks/disk.js +13 -0
- package/dist/watch/checks/extra.d.ts +2 -0
- package/dist/watch/checks/extra.js +18 -0
- package/dist/watch/checks/idle.d.ts +2 -0
- package/dist/watch/checks/idle.js +32 -0
- package/dist/watch/checks/load.d.ts +2 -0
- package/dist/watch/checks/load.js +11 -0
- package/dist/watch/checks/memory.d.ts +2 -0
- package/dist/watch/checks/memory.js +11 -0
- package/dist/watch/checks/missing.d.ts +2 -0
- package/dist/watch/checks/missing.js +12 -0
- package/dist/watch/checks/model-drift.d.ts +2 -0
- package/dist/watch/checks/model-drift.js +14 -0
- package/dist/watch/checks/swap-free.d.ts +2 -0
- package/dist/watch/checks/swap-free.js +12 -0
- package/dist/watch/checks/swap-growth.d.ts +2 -0
- package/dist/watch/checks/swap-growth.js +18 -0
- package/dist/watch/checks/team-idle.d.ts +2 -0
- package/dist/watch/checks/team-idle.js +18 -0
- package/dist/watch/checks/unsent.d.ts +2 -0
- package/dist/watch/checks/unsent.js +18 -0
- package/dist/watch/close.d.ts +19 -0
- package/dist/watch/close.js +40 -0
- package/dist/watch/dialect.d.ts +5 -0
- package/dist/watch/dialect.js +430 -0
- package/dist/watch/end.d.ts +11 -0
- package/dist/watch/end.js +17 -0
- package/dist/watch/machine.d.ts +38 -0
- package/dist/watch/machine.js +137 -0
- package/dist/watch/notify.d.ts +1 -0
- package/dist/watch/notify.js +15 -0
- package/dist/watch/pass.d.ts +30 -0
- package/dist/watch/pass.js +237 -0
- package/dist/watch/screen-core.d.ts +14 -0
- package/dist/watch/screen-core.js +320 -0
- package/dist/watch/screen-data.d.ts +65 -0
- package/dist/watch/screen-data.js +1 -0
- package/dist/watch/screen-file.d.ts +2 -0
- package/dist/watch/screen-file.js +269 -0
- package/dist/watch/screen.d.ts +20 -0
- package/dist/watch/screen.js +38 -0
- package/dist/worktree/place.d.ts +18 -0
- package/dist/worktree/place.js +126 -0
- package/dist/yaml.d.ts +30 -0
- package/dist/yaml.js +380 -0
- package/examples/checks/codex-quota +217 -0
- package/examples/team.yaml +76 -0
- package/package.json +29 -10
package/dist/yaml.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// A parser for the YAML subset a team file is written in: block maps and sequences, flow maps
|
|
2
|
+
// and sequences on one line, plain and quoted scalars, comments. Anything else is refused with
|
|
3
|
+
// its line number, since this reads a file that runs commands.
|
|
4
|
+
export class YamlError extends Error {
|
|
5
|
+
line;
|
|
6
|
+
constructor(line, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'YamlError';
|
|
9
|
+
this.line = line;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function parseYaml(text) {
|
|
13
|
+
const lines = readLines(text);
|
|
14
|
+
if (!lines.length)
|
|
15
|
+
throw new YamlError(1, 'the file is empty');
|
|
16
|
+
const state = { lines, at: 0 };
|
|
17
|
+
const first = lines[0];
|
|
18
|
+
if (first.indent !== 0)
|
|
19
|
+
throw new YamlError(first.n, 'the first line must not be indented');
|
|
20
|
+
const node = parseBlock(state, 0);
|
|
21
|
+
const rest = state.lines[state.at];
|
|
22
|
+
if (rest)
|
|
23
|
+
throw new YamlError(rest.n, 'unexpected indentation');
|
|
24
|
+
return node;
|
|
25
|
+
}
|
|
26
|
+
export function toValue(node) {
|
|
27
|
+
if (node.kind === 'scalar')
|
|
28
|
+
return node.value;
|
|
29
|
+
if (node.kind === 'seq')
|
|
30
|
+
return node.items.map(toValue);
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const entry of node.entries)
|
|
33
|
+
out[entry.key] = toValue(entry.value);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function readLines(text) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
const source = text.replace(/^/, '').split(/\r?\n/);
|
|
39
|
+
for (let i = 0; i < source.length; i++) {
|
|
40
|
+
const n = i + 1;
|
|
41
|
+
const full = source[i];
|
|
42
|
+
const indent = full.length - full.trimStart().length;
|
|
43
|
+
if (full.slice(0, indent).includes('\t'))
|
|
44
|
+
throw new YamlError(n, 'a tab in the indentation: use spaces');
|
|
45
|
+
const body = stripComment(full.slice(indent), n).trimEnd();
|
|
46
|
+
if (!body)
|
|
47
|
+
continue;
|
|
48
|
+
if (body === '---' || body === '...' || body.startsWith('--- ')) {
|
|
49
|
+
throw new YamlError(n, 'document markers are not supported: one document per file');
|
|
50
|
+
}
|
|
51
|
+
if (body.startsWith('%'))
|
|
52
|
+
throw new YamlError(n, 'directives are not supported');
|
|
53
|
+
lines.push({ n, indent, text: body });
|
|
54
|
+
}
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
// A comment starts at a `#` that begins the line or follows a space, outside quotes.
|
|
58
|
+
function stripComment(text, n) {
|
|
59
|
+
let quote = '';
|
|
60
|
+
for (let i = 0; i < text.length; i++) {
|
|
61
|
+
const c = text[i];
|
|
62
|
+
if (quote) {
|
|
63
|
+
if (quote === '"' && c === '\\')
|
|
64
|
+
i++;
|
|
65
|
+
else if (c === quote) {
|
|
66
|
+
if (quote === "'" && text[i + 1] === "'")
|
|
67
|
+
i++;
|
|
68
|
+
else
|
|
69
|
+
quote = '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if ((c === '"' || c === "'") && startsScalar(text, i))
|
|
73
|
+
quote = c;
|
|
74
|
+
else if (c === '#' && (i === 0 || text[i - 1] === ' ' || text[i - 1] === '\t'))
|
|
75
|
+
return text.slice(0, i);
|
|
76
|
+
}
|
|
77
|
+
if (quote)
|
|
78
|
+
throw new YamlError(n, 'a quoted string is not closed on its line');
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
// A quote opens a string only where a scalar can start; inside a plain scalar it is a character.
|
|
82
|
+
function startsScalar(text, i) {
|
|
83
|
+
const before = text.slice(0, i).trimEnd();
|
|
84
|
+
return before === '' || /[:,\[{-]$/.test(before);
|
|
85
|
+
}
|
|
86
|
+
function parseBlock(state, indent) {
|
|
87
|
+
const line = state.lines[state.at];
|
|
88
|
+
return isItem(line.text) ? parseSeq(state, indent) : parseMap(state, indent);
|
|
89
|
+
}
|
|
90
|
+
function isItem(text) {
|
|
91
|
+
return text === '-' || text.startsWith('- ');
|
|
92
|
+
}
|
|
93
|
+
function parseMap(state, indent) {
|
|
94
|
+
const start = state.lines[state.at];
|
|
95
|
+
const entries = [];
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (;;) {
|
|
98
|
+
const line = state.lines[state.at];
|
|
99
|
+
if (!line || line.indent < indent)
|
|
100
|
+
break;
|
|
101
|
+
if (line.indent > indent)
|
|
102
|
+
throw new YamlError(line.n, 'unexpected indentation');
|
|
103
|
+
if (isItem(line.text))
|
|
104
|
+
throw new YamlError(line.n, 'a list item where a key was expected');
|
|
105
|
+
const split = splitKey(line.text, line.n);
|
|
106
|
+
if (!split)
|
|
107
|
+
throw new YamlError(line.n, `expected "key: value", found "${clip(line.text)}"`);
|
|
108
|
+
if (seen.has(split.key))
|
|
109
|
+
throw new YamlError(line.n, `duplicate key "${split.key}"`);
|
|
110
|
+
seen.add(split.key);
|
|
111
|
+
state.at++;
|
|
112
|
+
entries.push({ key: split.key, line: line.n, value: valueAfterKey(state, indent, split.rest, line.n) });
|
|
113
|
+
}
|
|
114
|
+
return { kind: 'map', line: start.n, entries };
|
|
115
|
+
}
|
|
116
|
+
function valueAfterKey(state, indent, rest, n) {
|
|
117
|
+
if (rest)
|
|
118
|
+
return parseInline(rest, n);
|
|
119
|
+
const next = state.lines[state.at];
|
|
120
|
+
if (next && next.indent > indent)
|
|
121
|
+
return parseBlock(state, next.indent);
|
|
122
|
+
// A list may sit at its key's own indentation.
|
|
123
|
+
if (next && next.indent === indent && isItem(next.text))
|
|
124
|
+
return parseSeq(state, indent);
|
|
125
|
+
return { kind: 'scalar', line: n, value: null, quoted: false, raw: '' };
|
|
126
|
+
}
|
|
127
|
+
function parseSeq(state, indent) {
|
|
128
|
+
const start = state.lines[state.at];
|
|
129
|
+
const items = [];
|
|
130
|
+
for (;;) {
|
|
131
|
+
const line = state.lines[state.at];
|
|
132
|
+
if (!line || line.indent < indent)
|
|
133
|
+
break;
|
|
134
|
+
if (line.indent > indent)
|
|
135
|
+
throw new YamlError(line.n, 'unexpected indentation');
|
|
136
|
+
if (!isItem(line.text))
|
|
137
|
+
break;
|
|
138
|
+
const after = line.text.slice(1);
|
|
139
|
+
const rest = after.trimStart();
|
|
140
|
+
if (!rest) {
|
|
141
|
+
state.at++;
|
|
142
|
+
const next = state.lines[state.at];
|
|
143
|
+
if (next && next.indent > indent)
|
|
144
|
+
items.push(parseBlock(state, next.indent));
|
|
145
|
+
else
|
|
146
|
+
items.push({ kind: 'scalar', line: line.n, value: null, quoted: false, raw: '' });
|
|
147
|
+
}
|
|
148
|
+
else if (isItem(rest)) {
|
|
149
|
+
throw new YamlError(line.n, 'a list directly inside a list item is not supported');
|
|
150
|
+
}
|
|
151
|
+
else if (splitKey(rest, line.n)) {
|
|
152
|
+
// "- key: value" opens a map whose keys align with the first one.
|
|
153
|
+
const inner = indent + 1 + (after.length - rest.length);
|
|
154
|
+
state.lines[state.at] = { n: line.n, indent: inner, text: rest };
|
|
155
|
+
items.push(parseMap(state, inner));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
state.at++;
|
|
159
|
+
items.push(parseInline(rest, line.n));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { kind: 'seq', line: start.n, items };
|
|
163
|
+
}
|
|
164
|
+
// Splits "key: rest" or "key:". Returns null when the text is not a map entry.
|
|
165
|
+
function splitKey(text, n) {
|
|
166
|
+
const c = text[0];
|
|
167
|
+
if (c === '"' || c === "'") {
|
|
168
|
+
const read = readQuoted(text, 0, n);
|
|
169
|
+
const tail = text.slice(read.end);
|
|
170
|
+
if (tail === ':')
|
|
171
|
+
return { key: read.value, rest: '' };
|
|
172
|
+
if (tail.startsWith(': '))
|
|
173
|
+
return { key: read.value, rest: tail.slice(2).trim() };
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (c === '{' || c === '[')
|
|
177
|
+
return null;
|
|
178
|
+
if (c === '?' && (text.length === 1 || text[1] === ' '))
|
|
179
|
+
throw new YamlError(n, 'complex keys ("? ") are not supported');
|
|
180
|
+
for (let i = 0; i < text.length; i++) {
|
|
181
|
+
if (text[i] !== ':')
|
|
182
|
+
continue;
|
|
183
|
+
if (i === text.length - 1)
|
|
184
|
+
return checkKey(text.slice(0, i), '', n);
|
|
185
|
+
if (text[i + 1] === ' ')
|
|
186
|
+
return checkKey(text.slice(0, i), text.slice(i + 2).trim(), n);
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function checkKey(key, rest, n) {
|
|
191
|
+
const trimmed = key.trimEnd();
|
|
192
|
+
if (!trimmed)
|
|
193
|
+
throw new YamlError(n, 'an empty key');
|
|
194
|
+
refuseIndicator(trimmed, n);
|
|
195
|
+
return { key: trimmed, rest };
|
|
196
|
+
}
|
|
197
|
+
function parseInline(text, n) {
|
|
198
|
+
const c = text[0];
|
|
199
|
+
// "a: b: c" and "a: - x" are not values in YAML; a full parser refuses them too.
|
|
200
|
+
if (c !== '"' && c !== "'" && c !== '{' && c !== '[') {
|
|
201
|
+
if (/: |:$/.test(text))
|
|
202
|
+
throw new YamlError(n, 'a value that contains ": " must be quoted');
|
|
203
|
+
if (isItem(text))
|
|
204
|
+
throw new YamlError(n, 'a list can\'t start on its key\'s line: put each "- item" on a line of its own');
|
|
205
|
+
}
|
|
206
|
+
if (c === '{' || c === '[') {
|
|
207
|
+
const read = readFlow(text, 0, n);
|
|
208
|
+
if (text.slice(read.end).trim())
|
|
209
|
+
throw new YamlError(n, `unexpected text after "${c === '{' ? '}' : ']'}"`);
|
|
210
|
+
return read.node;
|
|
211
|
+
}
|
|
212
|
+
if (c === '"' || c === "'") {
|
|
213
|
+
const read = readQuoted(text, 0, n);
|
|
214
|
+
if (text.slice(read.end).trim())
|
|
215
|
+
throw new YamlError(n, 'unexpected text after a quoted string');
|
|
216
|
+
return { kind: 'scalar', line: n, value: read.value, quoted: true, raw: text.slice(0, read.end) };
|
|
217
|
+
}
|
|
218
|
+
return plain(text, n);
|
|
219
|
+
}
|
|
220
|
+
function plain(text, n) {
|
|
221
|
+
refuseIndicator(text, n);
|
|
222
|
+
return { kind: 'scalar', line: n, value: resolve(text), quoted: false, raw: text };
|
|
223
|
+
}
|
|
224
|
+
function refuseIndicator(text, n) {
|
|
225
|
+
const c = text[0];
|
|
226
|
+
if (c === '&')
|
|
227
|
+
throw new YamlError(n, 'anchors ("&") are not supported');
|
|
228
|
+
if (c === '*')
|
|
229
|
+
throw new YamlError(n, 'aliases ("*") are not supported: quote a value that starts with "*"');
|
|
230
|
+
if (c === '!')
|
|
231
|
+
throw new YamlError(n, 'tags ("!") are not supported');
|
|
232
|
+
if (c === '|' || c === '>')
|
|
233
|
+
throw new YamlError(n, 'block scalars ("|", ">") are not supported: write the value on one line');
|
|
234
|
+
if (c === '@' || c === '`' || c === '%')
|
|
235
|
+
throw new YamlError(n, `a value can't start with "${c}": quote it`);
|
|
236
|
+
if (text === '<<')
|
|
237
|
+
throw new YamlError(n, 'merge keys ("<<") are not supported');
|
|
238
|
+
}
|
|
239
|
+
// YAML 1.2 core schema.
|
|
240
|
+
function resolve(text) {
|
|
241
|
+
if (text === '~' || /^(null|Null|NULL)$/.test(text))
|
|
242
|
+
return null;
|
|
243
|
+
if (/^(true|True|TRUE)$/.test(text))
|
|
244
|
+
return true;
|
|
245
|
+
if (/^(false|False|FALSE)$/.test(text))
|
|
246
|
+
return false;
|
|
247
|
+
if (/^[-+]?[0-9]+$/.test(text))
|
|
248
|
+
return Number(text);
|
|
249
|
+
if (/^0o[0-7]+$/.test(text))
|
|
250
|
+
return parseInt(text.slice(2), 8);
|
|
251
|
+
if (/^0x[0-9a-fA-F]+$/.test(text))
|
|
252
|
+
return parseInt(text.slice(2), 16);
|
|
253
|
+
if (/^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$/.test(text))
|
|
254
|
+
return Number(text);
|
|
255
|
+
if (/^[-+]?\.(inf|Inf|INF)$/.test(text))
|
|
256
|
+
return text.startsWith('-') ? -Infinity : Infinity;
|
|
257
|
+
if (/^\.(nan|NaN|NAN)$/.test(text))
|
|
258
|
+
return NaN;
|
|
259
|
+
return text;
|
|
260
|
+
}
|
|
261
|
+
const ESCAPES = {
|
|
262
|
+
'0': '\0', a: '\x07', b: '\b', t: '\t', n: '\n', v: '\v', f: '\f', r: '\r', e: '\x1b',
|
|
263
|
+
' ': ' ', '"': '"', '/': '/', '\\': '\\', N: '\x85', _: '\xa0', L: '
', P: '
',
|
|
264
|
+
};
|
|
265
|
+
function readQuoted(text, at, n) {
|
|
266
|
+
const quote = text[at];
|
|
267
|
+
let out = '';
|
|
268
|
+
for (let i = at + 1; i < text.length; i++) {
|
|
269
|
+
const c = text[i];
|
|
270
|
+
if (quote === "'") {
|
|
271
|
+
if (c !== "'")
|
|
272
|
+
out += c;
|
|
273
|
+
else if (text[i + 1] === "'") {
|
|
274
|
+
out += "'";
|
|
275
|
+
i++;
|
|
276
|
+
}
|
|
277
|
+
else
|
|
278
|
+
return { value: out, end: i + 1 };
|
|
279
|
+
}
|
|
280
|
+
else if (c === '"') {
|
|
281
|
+
return { value: out, end: i + 1 };
|
|
282
|
+
}
|
|
283
|
+
else if (c === '\\') {
|
|
284
|
+
const e = text[++i];
|
|
285
|
+
const width = e === 'x' ? 2 : e === 'u' ? 4 : e === 'U' ? 8 : 0;
|
|
286
|
+
if (width) {
|
|
287
|
+
const hex = text.slice(i + 1, i + 1 + width);
|
|
288
|
+
if (!new RegExp(`^[0-9a-fA-F]{${width}}$`).test(hex))
|
|
289
|
+
throw new YamlError(n, `a bad "\\${e}" escape`);
|
|
290
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
291
|
+
i += width;
|
|
292
|
+
}
|
|
293
|
+
else if (e !== undefined && e in ESCAPES)
|
|
294
|
+
out += ESCAPES[e];
|
|
295
|
+
else
|
|
296
|
+
throw new YamlError(n, `an unknown escape "\\${e ?? ''}" in a double-quoted string`);
|
|
297
|
+
}
|
|
298
|
+
else
|
|
299
|
+
out += c;
|
|
300
|
+
}
|
|
301
|
+
throw new YamlError(n, 'a quoted string is not closed on its line');
|
|
302
|
+
}
|
|
303
|
+
function readFlow(text, at, n) {
|
|
304
|
+
const open = text[at];
|
|
305
|
+
const close = open === '{' ? '}' : ']';
|
|
306
|
+
const entries = [];
|
|
307
|
+
const items = [];
|
|
308
|
+
const seen = new Set();
|
|
309
|
+
let i = skipSpaces(text, at + 1);
|
|
310
|
+
for (;;) {
|
|
311
|
+
if (i >= text.length)
|
|
312
|
+
throw new YamlError(n, `"${open}" is not closed on its line`);
|
|
313
|
+
if (text[i] === close) {
|
|
314
|
+
const node = open === '{' ? { kind: 'map', line: n, entries } : { kind: 'seq', line: n, items };
|
|
315
|
+
return { node, end: i + 1 };
|
|
316
|
+
}
|
|
317
|
+
if (open === '{') {
|
|
318
|
+
const key = readFlowScalar(text, i, n, true);
|
|
319
|
+
if (key.node.kind !== 'scalar')
|
|
320
|
+
throw new YamlError(n, 'a key in "{ }" must be a plain or quoted word');
|
|
321
|
+
i = skipSpaces(text, key.end);
|
|
322
|
+
if (text[i] !== ':')
|
|
323
|
+
throw new YamlError(n, 'expected "key: value" inside "{ }"');
|
|
324
|
+
const name = key.node.quoted ? String(key.node.value) : key.node.raw;
|
|
325
|
+
if (seen.has(name))
|
|
326
|
+
throw new YamlError(n, `duplicate key "${name}"`);
|
|
327
|
+
seen.add(name);
|
|
328
|
+
i = skipSpaces(text, i + 1);
|
|
329
|
+
const value = readFlowScalar(text, i, n, false);
|
|
330
|
+
entries.push({ key: name, line: n, value: value.node });
|
|
331
|
+
i = skipSpaces(text, value.end);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const value = readFlowScalar(text, i, n, false);
|
|
335
|
+
items.push(value.node);
|
|
336
|
+
i = skipSpaces(text, value.end);
|
|
337
|
+
}
|
|
338
|
+
if (text[i] === ',')
|
|
339
|
+
i = skipSpaces(text, i + 1);
|
|
340
|
+
else if (text[i] !== close)
|
|
341
|
+
throw new YamlError(n, `expected "," or "${close}"`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function readFlowScalar(text, at, n, key) {
|
|
345
|
+
const c = text[at];
|
|
346
|
+
if (c === '{' || c === '[') {
|
|
347
|
+
if (key)
|
|
348
|
+
throw new YamlError(n, 'a key in "{ }" must be a plain or quoted word');
|
|
349
|
+
return readFlow(text, at, n);
|
|
350
|
+
}
|
|
351
|
+
if (c === '"' || c === "'") {
|
|
352
|
+
const read = readQuoted(text, at, n);
|
|
353
|
+
return { node: { kind: 'scalar', line: n, value: read.value, quoted: true, raw: text.slice(at, read.end) }, end: read.end };
|
|
354
|
+
}
|
|
355
|
+
let end = at;
|
|
356
|
+
while (end < text.length) {
|
|
357
|
+
const d = text[end];
|
|
358
|
+
if (d === ',' || d === ']' || d === '}' || d === '[' || d === '{')
|
|
359
|
+
break;
|
|
360
|
+
if (d === ':' && (key || text[end + 1] === ' ' || end + 1 === text.length)) {
|
|
361
|
+
if (!key)
|
|
362
|
+
throw new YamlError(n, 'a ": " inside a plain value in "[ ]" or "{ }": quote it');
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
end++;
|
|
366
|
+
}
|
|
367
|
+
const raw = text.slice(at, end).trim();
|
|
368
|
+
if (!raw)
|
|
369
|
+
throw new YamlError(n, 'an empty value inside "[ ]" or "{ }"');
|
|
370
|
+
return { node: plain(raw, n), end };
|
|
371
|
+
}
|
|
372
|
+
function skipSpaces(text, at) {
|
|
373
|
+
let i = at;
|
|
374
|
+
while (text[i] === ' ')
|
|
375
|
+
i++;
|
|
376
|
+
return i;
|
|
377
|
+
}
|
|
378
|
+
function clip(text) {
|
|
379
|
+
return text.length > 40 ? `${text.slice(0, 40)}…` : text;
|
|
380
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Prints one subscription line from a Codex session rollout.
|
|
3
|
+
//
|
|
4
|
+
// Bun runs this. The check is one JSONL scan, and the owner already has Bun on
|
|
5
|
+
// PATH to run team. It uses only the built-in file modules, so there is no
|
|
6
|
+
// package to install beside the script.
|
|
7
|
+
//
|
|
8
|
+
// team runs a check with an empty environment plus PATH and HOME. CODEX_HOME
|
|
9
|
+
// from the owner's shell never reaches this process, so sessions are read from
|
|
10
|
+
// $HOME/.codex/sessions. When Codex's home is somewhere else, install a two-line
|
|
11
|
+
// wrapper, name that wrapper as the account's check, and let it set CODEX_HOME
|
|
12
|
+
// before it execs this script:
|
|
13
|
+
//
|
|
14
|
+
// #!/bin/sh
|
|
15
|
+
// CODEX_HOME=/path/to/codex exec /path/to/codex-quota
|
|
16
|
+
//
|
|
17
|
+
// When that variable is set, sessions are $CODEX_HOME/sessions
|
|
18
|
+
// (codex-rs/utils/home-dir and rollout's SESSIONS_SUBDIR, tag rust-v0.157.0).
|
|
19
|
+
//
|
|
20
|
+
// Files named rollout-*.jsonl are taken newest first. The first that has a
|
|
21
|
+
// token_count event with a primary window is the one used, and at most ten
|
|
22
|
+
// files are opened. A newer session that has only session_meta does not hide
|
|
23
|
+
// the last figure. From that file the last such event is kept. The line's at
|
|
24
|
+
// time is that event's timestamp, so an older figure stays dated. The fields
|
|
25
|
+
// used are that window's used_percent, window_minutes and resets_at, plus the
|
|
26
|
+
// event's timestamp. Nothing else from the file is printed.
|
|
27
|
+
//
|
|
28
|
+
// window_minutes maps to a contract name: 300 session, 1440 daily, 10080 weekly.
|
|
29
|
+
// Any other length has no name, and inventing one would be a false figure.
|
|
30
|
+
// used_percent is rounded to the nearest integer. Below 0 or above 100 cannot
|
|
31
|
+
// be written. A reset in the past, or none, is left off the line.
|
|
32
|
+
//
|
|
33
|
+
// primary is the first line. When it cannot be written, nothing is printed, and
|
|
34
|
+
// an older file is not used in its place.
|
|
35
|
+
// RateLimitSnapshot.secondary (protocol.rs, tag rust-v0.157.0) is a second
|
|
36
|
+
// window in the same event. It is a second line when its window_minutes maps to
|
|
37
|
+
// a different one of the three names. The same window is left off: a repeated
|
|
38
|
+
// window makes the whole reading unknown. A secondary figure that cannot be
|
|
39
|
+
// written is left off, and the primary line still prints. Both lines share the
|
|
40
|
+
// event's timestamp.
|
|
41
|
+
|
|
42
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
43
|
+
import { join } from 'node:path';
|
|
44
|
+
|
|
45
|
+
const WINDOWS = { 300: 'session', 1440: 'daily', 10080: 'weekly' };
|
|
46
|
+
const MAX_ROLLOUTS = 10;
|
|
47
|
+
const LINE =
|
|
48
|
+
/^(session|daily|weekly) (100|[0-9]{1,2})% used( resets (?:[0-9]+h(?:[0-9]+m)?|[0-9]+m))? at [0-9]{10}$/;
|
|
49
|
+
|
|
50
|
+
function main() {
|
|
51
|
+
for (const path of newestRollouts(sessionsDir(), MAX_ROLLOUTS)) {
|
|
52
|
+
const line = lineFromRollout(readFileSync(path, 'utf8'));
|
|
53
|
+
if (line === undefined) continue;
|
|
54
|
+
if (line) process.stdout.write(`${line}\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sessionsDir() {
|
|
60
|
+
const codex = process.env.CODEX_HOME;
|
|
61
|
+
if (codex) return join(codex, 'sessions');
|
|
62
|
+
const home = process.env.HOME;
|
|
63
|
+
if (!home) return null;
|
|
64
|
+
return join(home, '.codex', 'sessions');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {string | null} root @param {number} limit */
|
|
68
|
+
function newestRollouts(root, limit) {
|
|
69
|
+
if (!root) return [];
|
|
70
|
+
/** @type {{ path: string, mtimeMs: number, name: string }[]} */
|
|
71
|
+
const found = [];
|
|
72
|
+
walk(root, found);
|
|
73
|
+
found.sort((a, b) => b.mtimeMs - a.mtimeMs || (a.name < b.name ? 1 : a.name > b.name ? -1 : 0));
|
|
74
|
+
return found.slice(0, limit).map((file) => file.path);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @param {string} dir @param {{ path: string, mtimeMs: number, name: string }[]} found */
|
|
78
|
+
function walk(dir, found) {
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
if (entry.isSymbolicLink()) continue;
|
|
87
|
+
const path = join(dir, entry.name);
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
walk(path, found);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!entry.isFile() || !entry.name.startsWith('rollout-') || !entry.name.endsWith('.jsonl')) continue;
|
|
93
|
+
try {
|
|
94
|
+
found.push({ path, mtimeMs: statSync(path).mtimeMs, name: entry.name });
|
|
95
|
+
} catch {
|
|
96
|
+
// A file that disappears between listing and stat is not the newest.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The contract line, null when a primary window is present but cannot be
|
|
103
|
+
* written, or undefined when this file has no token_count with a primary.
|
|
104
|
+
* A null result stops the scan: this file already has a primary window, and an
|
|
105
|
+
* older file must not stand in for it.
|
|
106
|
+
* @param {string} text
|
|
107
|
+
* @returns {string | null | undefined}
|
|
108
|
+
*/
|
|
109
|
+
function lineFromRollout(text) {
|
|
110
|
+
const lines = text.split('\n');
|
|
111
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
112
|
+
const raw = lines[index]?.trim();
|
|
113
|
+
if (!raw) continue;
|
|
114
|
+
const event = tokenCount(raw);
|
|
115
|
+
if (event === undefined) continue;
|
|
116
|
+
return event;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* undefined: not a token_count with a primary window; keep looking.
|
|
123
|
+
* null: the latest primary window cannot be written; stop.
|
|
124
|
+
* string: one contract line, or two when secondary is another window.
|
|
125
|
+
* @param {string} raw
|
|
126
|
+
* @returns {string | null | undefined}
|
|
127
|
+
*/
|
|
128
|
+
function tokenCount(raw) {
|
|
129
|
+
let row;
|
|
130
|
+
try {
|
|
131
|
+
row = JSON.parse(raw);
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const payload = row?.payload;
|
|
136
|
+
if (!payload || payload.type !== 'token_count') return undefined;
|
|
137
|
+
const limits = payload.rate_limits;
|
|
138
|
+
const primary = limits?.primary;
|
|
139
|
+
if (primary == null) return undefined;
|
|
140
|
+
if (typeof primary !== 'object') return null;
|
|
141
|
+
const line = formatWindow(primary, row.timestamp);
|
|
142
|
+
if (line === null) return null;
|
|
143
|
+
const extra = secondaryLine(limits.secondary, row.timestamp, windowName(primary.window_minutes));
|
|
144
|
+
return extra ? `${line}\n${extra}` : line;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The second line, or '' when secondary is absent, the same window, or cannot
|
|
149
|
+
* be written. A bad secondary never drops the primary line.
|
|
150
|
+
* @param {unknown} secondary
|
|
151
|
+
* @param {unknown} timestamp
|
|
152
|
+
* @param {string | null} primaryWindow
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
function secondaryLine(secondary, timestamp, primaryWindow) {
|
|
156
|
+
if (secondary == null || typeof secondary !== 'object') return '';
|
|
157
|
+
const record = /** @type {Record<string, unknown>} */ (secondary);
|
|
158
|
+
const window = windowName(record.window_minutes);
|
|
159
|
+
if (window === null || window === primaryWindow) return '';
|
|
160
|
+
return formatWindow(record, timestamp) ?? '';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @param {Record<string, unknown>} window @param {unknown} timestamp */
|
|
164
|
+
function formatWindow(window, timestamp) {
|
|
165
|
+
const used = roundedPercent(window.used_percent);
|
|
166
|
+
const name = windowName(window.window_minutes);
|
|
167
|
+
const at = unixSeconds(timestamp);
|
|
168
|
+
if (used === null || name === null || at === null) return null;
|
|
169
|
+
const resets = resetPhrase(window.resets_at);
|
|
170
|
+
if (resets === null) return null;
|
|
171
|
+
const line = `${name} ${used}% used${resets} at ${at}`;
|
|
172
|
+
return LINE.test(line) ? line : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** @param {unknown} value */
|
|
176
|
+
function roundedPercent(value) {
|
|
177
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 100) return null;
|
|
178
|
+
return Math.round(value);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** @param {unknown} minutes */
|
|
182
|
+
function windowName(minutes) {
|
|
183
|
+
if (typeof minutes !== 'number' || !Number.isFinite(minutes)) return null;
|
|
184
|
+
return WINDOWS[minutes] ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @param {unknown} timestamp */
|
|
188
|
+
function unixSeconds(timestamp) {
|
|
189
|
+
const ms = typeof timestamp === 'string' ? Date.parse(timestamp) : NaN;
|
|
190
|
+
if (!Number.isFinite(ms)) return null;
|
|
191
|
+
const seconds = Math.floor(ms / 1000);
|
|
192
|
+
return seconds >= 1_000_000_000 && seconds <= 9_999_999_999 ? String(seconds) : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* '' when the reset is absent or already past. null when the field is present
|
|
197
|
+
* and not a finite number of seconds.
|
|
198
|
+
* @param {unknown} resetsAt
|
|
199
|
+
*/
|
|
200
|
+
function resetPhrase(resetsAt) {
|
|
201
|
+
if (resetsAt == null) return '';
|
|
202
|
+
if (typeof resetsAt !== 'number' || !Number.isFinite(resetsAt)) return null;
|
|
203
|
+
const remaining = Math.floor(resetsAt) - Math.floor(Date.now() / 1000);
|
|
204
|
+
if (remaining <= 0) return '';
|
|
205
|
+
const minutes = Math.floor(remaining / 60);
|
|
206
|
+
const hours = Math.floor(minutes / 60);
|
|
207
|
+
const rest = minutes % 60;
|
|
208
|
+
if (hours > 0 && rest > 0) return ` resets ${hours}h${rest}m`;
|
|
209
|
+
if (hours > 0) return ` resets ${hours}h`;
|
|
210
|
+
return ` resets ${rest}m`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
main();
|
|
215
|
+
} catch {
|
|
216
|
+
// No file, a bad path, or a read error is an unknown figure. Exit 0, no text.
|
|
217
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# beacon's team, in format 1: a fictional example. Every project, seat, model and
|
|
2
|
+
# command in it is invented; nothing of a real roster is here.
|
|
3
|
+
|
|
4
|
+
format: 1
|
|
5
|
+
project: beacon
|
|
6
|
+
coordinator: claude-keeper # dispatches work
|
|
7
|
+
operator: claude-signal # receives the watch's reports and nudges
|
|
8
|
+
|
|
9
|
+
identity:
|
|
10
|
+
signature:
|
|
11
|
+
commits:
|
|
12
|
+
position: trailer # last-line | trailer | anywhere
|
|
13
|
+
exempt: [merge]
|
|
14
|
+
pull_requests:
|
|
15
|
+
position: last-line
|
|
16
|
+
template: "**Agent:** {display} · {role}"
|
|
17
|
+
|
|
18
|
+
workspace:
|
|
19
|
+
mode: shared # every seat works in the project root
|
|
20
|
+
|
|
21
|
+
machine: # checked before each seat is launched, and by the watch
|
|
22
|
+
load_start: 2.0 # 1-minute load per core above which `up` and `add` refuse
|
|
23
|
+
load_max: 8.0 # per core, above which the watch reports
|
|
24
|
+
memory_start: 30% # free memory below which `up` and `add` refuse
|
|
25
|
+
memory_min: 10% # below which the watch reports
|
|
26
|
+
disk_min: 20GB # free on the project's volume; both refuse and report
|
|
27
|
+
|
|
28
|
+
seats:
|
|
29
|
+
- role: coordinator
|
|
30
|
+
name: claude-keeper
|
|
31
|
+
label: coordinator
|
|
32
|
+
cli: claude-code
|
|
33
|
+
vendor: anthropic
|
|
34
|
+
model: Claude Nova
|
|
35
|
+
version: "2"
|
|
36
|
+
launch: claude
|
|
37
|
+
|
|
38
|
+
- role: operator
|
|
39
|
+
name: claude-signal
|
|
40
|
+
label: operator
|
|
41
|
+
cli: claude-code
|
|
42
|
+
vendor: anthropic
|
|
43
|
+
model: Claude Nova
|
|
44
|
+
version: "2"
|
|
45
|
+
launch: claude
|
|
46
|
+
|
|
47
|
+
- role: implementer
|
|
48
|
+
name: codex-beacon
|
|
49
|
+
label: codex
|
|
50
|
+
cli: codex
|
|
51
|
+
vendor: openai
|
|
52
|
+
model: GPT Comet
|
|
53
|
+
version: "3"
|
|
54
|
+
display: GPT-3 Comet # the vendor's spelling, for the signature
|
|
55
|
+
launch: codex -m gpt-comet-3
|
|
56
|
+
parked: true # running, and not reported while idle
|
|
57
|
+
|
|
58
|
+
- role: implementer
|
|
59
|
+
name: cursor-beacon
|
|
60
|
+
label: cursor
|
|
61
|
+
cli: cursor
|
|
62
|
+
vendor: meridian
|
|
63
|
+
model: Meridian
|
|
64
|
+
version: "1"
|
|
65
|
+
launch: cursor-agent # the model is chosen inside Cursor
|
|
66
|
+
|
|
67
|
+
# three seats from one entry: nimbus-beacon, nimbus-beacon-2 and -3
|
|
68
|
+
- role: implementer
|
|
69
|
+
name: nimbus-beacon
|
|
70
|
+
label: nimbus
|
|
71
|
+
cli: claude-code # the vendor's model, run by Claude Code
|
|
72
|
+
vendor: nimbus
|
|
73
|
+
model: Nimbus
|
|
74
|
+
version: "1"
|
|
75
|
+
launch: nimbus-claude # a launcher on the PATH, holding the account's key and endpoint
|
|
76
|
+
count: 3
|