team 0.1.0 → 0.1.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/README.md +20 -5
- package/dist/ansi.d.ts +13 -0
- package/dist/ansi.js +75 -0
- package/dist/approve/approval.d.ts +11 -0
- package/dist/approve/approval.js +75 -8
- package/dist/approve/fingerprint.d.ts +15 -4
- package/dist/approve/fingerprint.js +37 -12
- package/dist/budgets/gate.d.ts +12 -7
- package/dist/budgets/gate.js +52 -17
- package/dist/budgets/readings.d.ts +71 -5
- package/dist/budgets/readings.js +136 -9
- package/dist/budgets/table.d.ts +2 -2
- package/dist/budgets/table.js +24 -11
- package/dist/commands/add.js +12 -5
- package/dist/commands/doctor.d.ts +3 -1
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/down.d.ts +2 -2
- package/dist/commands/down.js +8 -1
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +21 -2
- package/dist/commands/status.js +3 -2
- package/dist/commands/up.d.ts +3 -1
- package/dist/commands/up.js +12 -7
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +94 -16
- package/dist/file/types.d.ts +5 -0
- package/dist/file/validate.js +31 -11
- package/dist/herdr.d.ts +4 -0
- package/dist/herdr.js +40 -11
- package/dist/launch/agent.d.ts +1 -0
- package/dist/launch/agent.js +12 -0
- package/dist/launch/deliver.d.ts +3 -1
- package/dist/launch/deliver.js +10 -1
- package/dist/launch/execute.d.ts +2 -2
- package/dist/launch/execute.js +15 -2
- package/dist/launch/rules.d.ts +4 -2
- package/dist/launch/rules.js +17 -3
- package/dist/profiles/antigravity.yaml +13 -0
- package/dist/profiles/claude-code.yaml +30 -0
- package/dist/profiles/cursor.yaml +4 -1
- package/dist/profiles/quota.d.ts +7 -2
- package/dist/profiles/quota.js +15 -14
- package/dist/state.d.ts +8 -3
- package/dist/state.js +37 -0
- package/dist/status/statusline.js +3 -1
- package/dist/watch/check.d.ts +1 -0
- package/dist/watch/checks/budget.js +4 -1
- package/dist/watch/pass.d.ts +14 -1
- package/dist/watch/pass.js +64 -28
- package/dist/watch/screen-core.d.ts +7 -1
- package/dist/watch/screen-core.js +146 -54
- package/dist/watch/screen-data.d.ts +9 -0
- package/dist/watch/screen-file.js +25 -7
- package/dist/watch/screen.d.ts +5 -0
- package/dist/watch/screen.js +16 -2
- package/examples/team.yaml +8 -0
- package/package.json +2 -2
|
@@ -1,13 +1,61 @@
|
|
|
1
|
+
// The screen core. Stage order, dialog primitives, the four composer modes and the
|
|
2
|
+
// safety floor live here, never in a profile. A profile is data: it can add a
|
|
3
|
+
// pattern, and it cannot move a stage or turn the floor off.
|
|
4
|
+
import { allDimAfter, hasSgr, stripSgr } from "../ansi.js";
|
|
1
5
|
const BUDGET_MS = 100;
|
|
2
6
|
// The rule a `below_last_rule` pattern has to sit under. It is the core's, not the profile's.
|
|
3
7
|
const RULE_LINE = /^\s*[─━]{8,}\s*$/;
|
|
4
|
-
|
|
8
|
+
// The safety floor's markers, matched without case: a dialog's wording is not always title-case
|
|
9
|
+
// (`Press enter to confirm or esc to cancel`). RFC 0002 § 4.1 permits it.
|
|
10
|
+
const FLOOR_PHRASES = ['do you want to', 'esc to cancel', 'enter confirm', 'enter continue', 'esc quit', 'esc skip', '↑/↓'];
|
|
11
|
+
/**
|
|
12
|
+
* The index of the composer's status line in the window, or -1. A `status-last` composer pins it
|
|
13
|
+
* as the last non-blank line; a `status-then-one` composer allows one chrome line below it. The
|
|
14
|
+
* classification and the quota read both take their line from here, so the two cannot disagree
|
|
15
|
+
* about which line the composer's status line is.
|
|
16
|
+
*/
|
|
17
|
+
function statusIndex(composer, lines, allowOneTrailing, tick) {
|
|
18
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
19
|
+
if (tick())
|
|
20
|
+
return 'stop';
|
|
21
|
+
if (!composer.statusLine.test(lines[i] ?? ''))
|
|
22
|
+
continue;
|
|
23
|
+
const trailing = lines.slice(i + 1).filter((line) => line.trim());
|
|
24
|
+
if (allowOneTrailing) {
|
|
25
|
+
if (trailing.length > 0 && (trailing.length > 1 || composer.prompt.test(trailing[0] ?? '')))
|
|
26
|
+
return -1;
|
|
27
|
+
}
|
|
28
|
+
else if (trailing.length > 0)
|
|
29
|
+
return -1;
|
|
30
|
+
return i;
|
|
31
|
+
}
|
|
32
|
+
return -1;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The one line a quota figure may be read from: the composer's own status row, or null when the
|
|
36
|
+
* window shows none. A dialog, a transcript line, the input box — anything the seat printed or
|
|
37
|
+
* typed — is never the row. The caller still has to know the screen is a composer screen.
|
|
38
|
+
*/
|
|
39
|
+
export function statusRowOf(data, lines) {
|
|
40
|
+
const composer = data.composer;
|
|
41
|
+
if (composer.mode !== 'status-last' && composer.mode !== 'status-then-one')
|
|
42
|
+
return null;
|
|
43
|
+
const at = statusIndex(composer, lines, composer.mode === 'status-then-one', () => false);
|
|
44
|
+
return at === 'stop' || at < 0 ? null : (lines[at] ?? null);
|
|
45
|
+
}
|
|
46
|
+
/** `lines` is already the window: the last 20 lines, each keeping any ANSI styling. Matching
|
|
47
|
+
* runs on each line's plain form, trimmed at the end; the styled form is kept for the one
|
|
48
|
+
* question styling can answer, whether the composer's input line is all dim. */
|
|
49
|
+
function plainLines(lines) {
|
|
50
|
+
return lines.map((line) => stripSgr(line).trimEnd());
|
|
51
|
+
}
|
|
5
52
|
/** `lines` is already the window: the last 20 lines, each trimmed at the end. */
|
|
6
53
|
export function classifyLines(data, lines, clock) {
|
|
7
54
|
const now = clock?.now ?? Date.now;
|
|
8
55
|
const budget = clock?.budgetMs ?? BUDGET_MS;
|
|
9
56
|
const start = now();
|
|
10
57
|
const tick = () => now() - start > budget;
|
|
58
|
+
const plain = plainLines(lines);
|
|
11
59
|
const order = [
|
|
12
60
|
['unknown', data.unknown],
|
|
13
61
|
['trust', data.trust],
|
|
@@ -19,19 +67,19 @@ export function classifyLines(data, lines, clock) {
|
|
|
19
67
|
if (!stage)
|
|
20
68
|
continue;
|
|
21
69
|
for (const rule of stage.rules) {
|
|
22
|
-
const hit = ruleMatches(
|
|
70
|
+
const hit = ruleMatches(data, plain, rule, tick);
|
|
23
71
|
if (hit === 'stop')
|
|
24
72
|
return { kind: 'unknown' };
|
|
25
73
|
if (hit)
|
|
26
74
|
return { kind };
|
|
27
75
|
}
|
|
28
76
|
}
|
|
29
|
-
const composed = compose(data, lines, tick);
|
|
77
|
+
const composed = compose(data, plain, lines, tick);
|
|
30
78
|
if (composed.kind === 'stop' || composed.kind === 'unknown')
|
|
31
79
|
return { kind: 'unknown' };
|
|
32
80
|
if (composed.kind !== 'idle' && composed.kind !== 'unsent')
|
|
33
81
|
return { kind: composed.kind };
|
|
34
|
-
const marked = floorHits(
|
|
82
|
+
const marked = floorHits(plain, composed.from, composed.input, data.chrome, tick);
|
|
35
83
|
if (marked === 'stop' || marked)
|
|
36
84
|
return { kind: 'unknown' };
|
|
37
85
|
return { kind: composed.kind };
|
|
@@ -45,28 +93,30 @@ export function composeLines(data, lines, clock) {
|
|
|
45
93
|
const budget = clock?.budgetMs ?? BUDGET_MS;
|
|
46
94
|
const start = now();
|
|
47
95
|
const tick = () => now() - start > budget;
|
|
96
|
+
const plain = plainLines(lines);
|
|
48
97
|
if (data.unknown) {
|
|
49
98
|
for (const rule of data.unknown.rules) {
|
|
50
|
-
const hit = ruleMatches(
|
|
99
|
+
const hit = ruleMatches(data, plain, rule, tick);
|
|
51
100
|
if (hit === 'stop')
|
|
52
101
|
return { kind: 'unknown' };
|
|
53
102
|
if (hit)
|
|
54
103
|
return { kind: 'unknown' };
|
|
55
104
|
}
|
|
56
105
|
}
|
|
57
|
-
const composed = compose(data, lines, tick);
|
|
106
|
+
const composed = compose(data, plain, lines, tick);
|
|
58
107
|
if (composed.kind === 'stop' || composed.kind === 'unknown')
|
|
59
108
|
return { kind: 'unknown' };
|
|
60
109
|
if (composed.kind !== 'idle' && composed.kind !== 'unsent')
|
|
61
110
|
return { kind: composed.kind };
|
|
62
|
-
const marked = floorHits(
|
|
111
|
+
const marked = floorHits(plain, composed.from, composed.input, data.chrome, tick);
|
|
63
112
|
if (marked === 'stop' || marked)
|
|
64
113
|
return { kind: 'unknown' };
|
|
65
114
|
return { kind: composed.kind };
|
|
66
115
|
}
|
|
67
|
-
function ruleMatches(lines, rule, tick) {
|
|
116
|
+
function ruleMatches(data, lines, rule, tick) {
|
|
117
|
+
const where = rule.onFooter ? [footerLine(data, lines)] : lines;
|
|
68
118
|
if (rule.any) {
|
|
69
|
-
const hit = anyLine(
|
|
119
|
+
const hit = anyLine(where, rule.any, tick);
|
|
70
120
|
if (hit === 'stop')
|
|
71
121
|
return 'stop';
|
|
72
122
|
if (!hit)
|
|
@@ -74,7 +124,7 @@ function ruleMatches(lines, rule, tick) {
|
|
|
74
124
|
}
|
|
75
125
|
if (rule.all) {
|
|
76
126
|
for (const pattern of rule.all) {
|
|
77
|
-
const hit = lineSomewhere(
|
|
127
|
+
const hit = lineSomewhere(where, pattern, tick);
|
|
78
128
|
if (hit === 'stop')
|
|
79
129
|
return 'stop';
|
|
80
130
|
if (!hit)
|
|
@@ -82,16 +132,23 @@ function ruleMatches(lines, rule, tick) {
|
|
|
82
132
|
}
|
|
83
133
|
}
|
|
84
134
|
if (rule.footer !== undefined) {
|
|
85
|
-
|
|
86
|
-
|
|
135
|
+
if (footerLine(data, lines) !== rule.footer)
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (rule.withoutRule) {
|
|
139
|
+
if (tick())
|
|
140
|
+
return 'stop';
|
|
141
|
+
if (findLast(lines, (line) => RULE_LINE.test(line)) >= 0)
|
|
87
142
|
return false;
|
|
88
143
|
}
|
|
89
144
|
if (rule.belowLastRule) {
|
|
90
145
|
if (tick())
|
|
91
146
|
return 'stop';
|
|
92
|
-
//
|
|
93
|
-
//
|
|
147
|
+
// A pattern sits below a composer rule. A window with no rule line is not
|
|
148
|
+
// below one: a dialog that fills the pane says so with `without_rule`.
|
|
94
149
|
const ruleAt = findLast(lines, (line) => RULE_LINE.test(line));
|
|
150
|
+
if (ruleAt < 0)
|
|
151
|
+
return false;
|
|
95
152
|
const at = findLast(lines, (line) => rule.belowLastRule?.test(line) ?? false);
|
|
96
153
|
if (at < 0 || at <= ruleAt)
|
|
97
154
|
return false;
|
|
@@ -137,6 +194,21 @@ function lineSomewhere(lines, pattern, tick) {
|
|
|
137
194
|
}
|
|
138
195
|
return false;
|
|
139
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* The line a rule's footer must be: the last non-blank one. The composer's own chrome is the one
|
|
199
|
+
* thing allowed below it — a `status-last` mode pins the status line to the pane's last line, and
|
|
200
|
+
* a dialog drawn above it is still the dialog its footer belongs to. Any other line below the
|
|
201
|
+
* footer means the footer is not the dialog's, and the rule doesn't match.
|
|
202
|
+
*/
|
|
203
|
+
function footerLine(data, lines) {
|
|
204
|
+
const nonBlank = lines.filter((line) => line.trim());
|
|
205
|
+
const last = nonBlank[nonBlank.length - 1] ?? '';
|
|
206
|
+
const composer = data.composer;
|
|
207
|
+
if (composer.mode === 'status-last' && composer.statusLine.test(last)) {
|
|
208
|
+
return (nonBlank[nonBlank.length - 2] ?? '').trim();
|
|
209
|
+
}
|
|
210
|
+
return last.trim();
|
|
211
|
+
}
|
|
140
212
|
function matches(line, pattern, tick) {
|
|
141
213
|
if (tick())
|
|
142
214
|
return 'stop';
|
|
@@ -150,17 +222,18 @@ function matches(line, pattern, tick) {
|
|
|
150
222
|
}
|
|
151
223
|
return true;
|
|
152
224
|
}
|
|
153
|
-
|
|
225
|
+
/** `plain` is the window's plain form; `styled` the window itself, for the input line's dim question. */
|
|
226
|
+
function compose(data, plain, styled, tick) {
|
|
154
227
|
const composer = data.composer;
|
|
155
228
|
if (composer.mode === 'box-to-rule')
|
|
156
|
-
return boxToRule(
|
|
229
|
+
return boxToRule(plain, styled, composer, tick);
|
|
157
230
|
if (composer.mode === 'status-last')
|
|
158
|
-
return statusLast(
|
|
231
|
+
return statusLast(plain, styled, composer, tick, false);
|
|
159
232
|
if (composer.mode === 'status-then-one')
|
|
160
|
-
return statusThenOne(
|
|
161
|
-
return twoRules(
|
|
233
|
+
return statusThenOne(plain, styled, composer, tick);
|
|
234
|
+
return twoRules(plain, styled, composer, tick);
|
|
162
235
|
}
|
|
163
|
-
function boxToRule(lines, composer, tick) {
|
|
236
|
+
function boxToRule(lines, styled, composer, tick) {
|
|
164
237
|
let input = -1;
|
|
165
238
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
166
239
|
if (tick())
|
|
@@ -172,39 +245,53 @@ function boxToRule(lines, composer, tick) {
|
|
|
172
245
|
}
|
|
173
246
|
if (input < 0)
|
|
174
247
|
return { kind: 'unknown' };
|
|
175
|
-
|
|
248
|
+
// The frame is the box itself: a closing rule under this prompt. A rule anywhere
|
|
249
|
+
// above, or a rule with shell output under it, is not the box. A shell's last
|
|
250
|
+
// prompt never has that closing rule.
|
|
251
|
+
let close = -1;
|
|
176
252
|
for (let i = input + 1; i < lines.length; i++) {
|
|
177
253
|
if (tick())
|
|
178
254
|
return { kind: 'stop' };
|
|
179
|
-
if (composer.rule.test(lines[i] ?? ''))
|
|
255
|
+
if (composer.rule.test(lines[i] ?? '')) {
|
|
256
|
+
close = i;
|
|
180
257
|
break;
|
|
181
|
-
|
|
182
|
-
return { kind: 'unsent', from, input };
|
|
258
|
+
}
|
|
183
259
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
let
|
|
189
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
260
|
+
if (close < 0)
|
|
261
|
+
return { kind: 'unknown' };
|
|
262
|
+
const above = input > 0 && composer.rule.test(lines[input - 1] ?? '');
|
|
263
|
+
let footer = false;
|
|
264
|
+
for (let j = close + 1; j < lines.length; j++) {
|
|
190
265
|
if (tick())
|
|
191
266
|
return { kind: 'stop' };
|
|
192
|
-
if (
|
|
193
|
-
|
|
267
|
+
if (statusFooter(lines[j] ?? '')) {
|
|
268
|
+
footer = true;
|
|
194
269
|
break;
|
|
195
270
|
}
|
|
196
271
|
}
|
|
197
|
-
|
|
272
|
+
// The opening rule on the line directly above, or the status footer when that
|
|
273
|
+
// rule has scrolled out of the window.
|
|
274
|
+
if (!above && !footer)
|
|
198
275
|
return { kind: 'unknown' };
|
|
199
|
-
const
|
|
200
|
-
for (let i =
|
|
276
|
+
const from = above ? input - 1 : input;
|
|
277
|
+
for (let i = input + 1; i < close; i++) {
|
|
201
278
|
if ((lines[i] ?? '').trim())
|
|
202
|
-
|
|
203
|
-
if (allowOneTrailing) {
|
|
204
|
-
if (trailing.length > 1 || trailing.some((line) => composer.prompt.test(line)))
|
|
205
|
-
return { kind: 'unknown' };
|
|
279
|
+
return { kind: 'unsent', from, input };
|
|
206
280
|
}
|
|
207
|
-
|
|
281
|
+
const typed = (lines[input] ?? '').replace(composer.prompt, '').trim();
|
|
282
|
+
return { kind: placeholder(typed, composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from, input };
|
|
283
|
+
}
|
|
284
|
+
// The composer footer, not a line of shell output under a stray rule. The model
|
|
285
|
+
// row uses a middot; the permissions line is the other footer.
|
|
286
|
+
function statusFooter(line) {
|
|
287
|
+
const text = line.trim();
|
|
288
|
+
return text.includes('·') || /bypass permissions/i.test(text);
|
|
289
|
+
}
|
|
290
|
+
function statusLast(lines, styled, composer, tick, allowOneTrailing) {
|
|
291
|
+
const status = statusIndex(composer, lines, allowOneTrailing, tick);
|
|
292
|
+
if (status === 'stop')
|
|
293
|
+
return { kind: 'stop' };
|
|
294
|
+
if (status < 0)
|
|
208
295
|
return { kind: 'unknown' };
|
|
209
296
|
let input = status - 1;
|
|
210
297
|
while (input >= 0 && !composer.prompt.test(lines[input] ?? ''))
|
|
@@ -214,10 +301,10 @@ function statusLast(lines, composer, tick, allowOneTrailing) {
|
|
|
214
301
|
for (let i = input + 1; i < status; i++)
|
|
215
302
|
if ((lines[i] ?? '').trim())
|
|
216
303
|
return { kind: 'unsent', from: input, input };
|
|
217
|
-
return { kind: placeholder(stripTyped(lines[input] ?? '', composer), composer
|
|
304
|
+
return { kind: placeholder(stripTyped(lines[input] ?? '', composer), composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from: input, input };
|
|
218
305
|
}
|
|
219
|
-
function statusThenOne(lines, composer, tick) {
|
|
220
|
-
const found = statusLast(lines, composer, tick, true);
|
|
306
|
+
function statusThenOne(lines, styled, composer, tick) {
|
|
307
|
+
const found = statusLast(lines, styled, composer, tick, true);
|
|
221
308
|
if (found.kind !== 'unknown')
|
|
222
309
|
return found;
|
|
223
310
|
// No status line: the fallback rules decide. Anything they don't name is unknown.
|
|
@@ -244,7 +331,7 @@ function statusThenOne(lines, composer, tick) {
|
|
|
244
331
|
}
|
|
245
332
|
return { kind: 'unknown' };
|
|
246
333
|
}
|
|
247
|
-
function twoRules(lines, composer, tick) {
|
|
334
|
+
function twoRules(lines, styled, composer, tick) {
|
|
248
335
|
let bottom = -1;
|
|
249
336
|
let top = -1;
|
|
250
337
|
let input = -1;
|
|
@@ -277,20 +364,24 @@ function twoRules(lines, composer, tick) {
|
|
|
277
364
|
if (lines.slice(input + 1, bottom).some((line) => line.trim()))
|
|
278
365
|
return { kind: 'unsent', from: top, input };
|
|
279
366
|
const typed = (lines[input] ?? '').replace(composer.prompt, '').trim();
|
|
280
|
-
return { kind: placeholder(typed, composer
|
|
281
|
-
}
|
|
282
|
-
function boundaryBefore(lines, input, rule) {
|
|
283
|
-
for (let i = input - 1; i >= 0; i--)
|
|
284
|
-
if (rule.test(lines[i] ?? ''))
|
|
285
|
-
return i;
|
|
286
|
-
return input;
|
|
367
|
+
return { kind: placeholder(typed, composer, lines[input] ?? '', styled[input]) ? 'idle' : 'unsent', from: top, input };
|
|
287
368
|
}
|
|
288
369
|
function stripTyped(line, composer) {
|
|
289
370
|
const stripped = composer.stripSuffix ? line.replace(composer.prompt, '').replace(composer.stripSuffix, '') : line.replace(composer.prompt, '');
|
|
290
371
|
return stripped.trim();
|
|
291
372
|
}
|
|
292
|
-
|
|
293
|
-
|
|
373
|
+
// A placeholder, not text. When the composer says its suggestions render dim and the input
|
|
374
|
+
// line carries any styling at all, the styling decides alone: all faint past the prompt is a
|
|
375
|
+
// greyed suggestion, anything else is text — the list is not asked, so no suggestion's
|
|
376
|
+
// styling can lend its words to typed characters after it. A line without styling (a plain
|
|
377
|
+
// read, an old herdr) leaves the list to decide, as does a composer without the style.
|
|
378
|
+
function placeholder(typed, composer, plainLine, styledLine) {
|
|
379
|
+
if (composer.placeholderStyle === 'dim' && styledLine !== undefined && hasSgr(styledLine)) {
|
|
380
|
+
const hit = composer.prompt.exec(plainLine);
|
|
381
|
+
const skip = hit === null ? 0 : plainLine.slice(0, hit.index + hit[0].length).replace(/\s/g, '').length;
|
|
382
|
+
return allDimAfter(styledLine, skip);
|
|
383
|
+
}
|
|
384
|
+
return composer.placeholders.some((entry) => ('equals' in entry ? typed === entry.equals : typed.startsWith(entry.prefix)));
|
|
294
385
|
}
|
|
295
386
|
function floorHits(lines, from, input, chrome, tick) {
|
|
296
387
|
for (let i = Math.max(0, from); i < lines.length; i++) {
|
|
@@ -301,7 +392,8 @@ function floorHits(lines, from, input, chrome, tick) {
|
|
|
301
392
|
continue;
|
|
302
393
|
if (i !== input && choiceLine(line) && lines.slice(i + 1).some((later) => twoLine(later)))
|
|
303
394
|
return true;
|
|
304
|
-
|
|
395
|
+
const lower = line.toLowerCase();
|
|
396
|
+
if (FLOOR_PHRASES.some((phrase) => lower.includes(phrase)))
|
|
305
397
|
return true;
|
|
306
398
|
}
|
|
307
399
|
return false;
|
|
@@ -7,7 +7,11 @@ export type Rule = {
|
|
|
7
7
|
any?: LinePattern[];
|
|
8
8
|
all?: LinePattern[];
|
|
9
9
|
footer?: string;
|
|
10
|
+
/** `any` and `all` are read on the dialog's last line, not on every line. */
|
|
11
|
+
onFooter?: boolean;
|
|
10
12
|
belowLastRule?: RegExp;
|
|
13
|
+
/** The window has no composer rule: the dialog is the pane. */
|
|
14
|
+
withoutRule?: boolean;
|
|
11
15
|
noneAfter?: {
|
|
12
16
|
anchor: LinePattern;
|
|
13
17
|
patterns: LinePattern[];
|
|
@@ -25,23 +29,27 @@ export type FallbackRule = {
|
|
|
25
29
|
all: LinePattern[];
|
|
26
30
|
kind: Screen['kind'];
|
|
27
31
|
};
|
|
32
|
+
export type PlaceholderStyle = 'dim';
|
|
28
33
|
type Box = {
|
|
29
34
|
mode: 'box-to-rule';
|
|
30
35
|
prompt: RegExp;
|
|
31
36
|
rule: RegExp;
|
|
32
37
|
placeholders: Placeholder[];
|
|
38
|
+
placeholderStyle?: PlaceholderStyle;
|
|
33
39
|
};
|
|
34
40
|
type StatusLast = {
|
|
35
41
|
mode: 'status-last';
|
|
36
42
|
statusLine: RegExp;
|
|
37
43
|
prompt: RegExp;
|
|
38
44
|
placeholders: Placeholder[];
|
|
45
|
+
placeholderStyle?: PlaceholderStyle;
|
|
39
46
|
};
|
|
40
47
|
type StatusThenOne = {
|
|
41
48
|
mode: 'status-then-one';
|
|
42
49
|
statusLine: RegExp;
|
|
43
50
|
prompt: RegExp;
|
|
44
51
|
placeholders: Placeholder[];
|
|
52
|
+
placeholderStyle?: PlaceholderStyle;
|
|
45
53
|
stripSuffix: RegExp | null;
|
|
46
54
|
fallback: FallbackRule[];
|
|
47
55
|
};
|
|
@@ -51,6 +59,7 @@ type TwoRules = {
|
|
|
51
59
|
rule: RegExp;
|
|
52
60
|
footers: RegExp[];
|
|
53
61
|
placeholders: Placeholder[];
|
|
62
|
+
placeholderStyle?: PlaceholderStyle;
|
|
54
63
|
};
|
|
55
64
|
export type Composer = Box | StatusLast | StatusThenOne | TwoRules;
|
|
56
65
|
export type ScreenData = {
|
|
@@ -50,14 +50,16 @@ function stageOf(node) {
|
|
|
50
50
|
}
|
|
51
51
|
function ruleOf(node, ignoreCase) {
|
|
52
52
|
const entries = mapping(node, 'a rule');
|
|
53
|
-
only(entries, ['any', 'all', 'footer', 'below_last_rule', 'none_after']);
|
|
53
|
+
only(entries, ['any', 'all', 'footer', 'on_footer', 'below_last_rule', 'without_rule', 'none_after']);
|
|
54
54
|
if (entries.length === 0)
|
|
55
55
|
fail(node.line, 'a rule has no primitive');
|
|
56
56
|
const rule = {};
|
|
57
57
|
const any = optional(entries, 'any');
|
|
58
58
|
const all = optional(entries, 'all');
|
|
59
59
|
const footer = optional(entries, 'footer');
|
|
60
|
+
const onFooter = optional(entries, 'on_footer');
|
|
60
61
|
const below = optional(entries, 'below_last_rule');
|
|
62
|
+
const withoutRule = optional(entries, 'without_rule');
|
|
61
63
|
const noneAfter = optional(entries, 'none_after');
|
|
62
64
|
if (any)
|
|
63
65
|
rule.any = patternsOf(any.value, 'any', ignoreCase);
|
|
@@ -65,8 +67,12 @@ function ruleOf(node, ignoreCase) {
|
|
|
65
67
|
rule.all = patternsOf(all.value, 'all', ignoreCase);
|
|
66
68
|
if (footer)
|
|
67
69
|
rule.footer = stringOf(footer.value) ?? fail(footer.line, '"footer" must be a string');
|
|
70
|
+
if (onFooter)
|
|
71
|
+
rule.onFooter = boolOf(onFooter.value, 'on_footer');
|
|
68
72
|
if (below)
|
|
69
73
|
rule.belowLastRule = patternOf(stringOf(below.value) ?? fail(below.line, '"below_last_rule" must be a string'), ignoreCase, below.line);
|
|
74
|
+
if (withoutRule)
|
|
75
|
+
rule.withoutRule = boolOf(withoutRule.value, 'without_rule');
|
|
70
76
|
if (noneAfter)
|
|
71
77
|
rule.noneAfter = noneAfterOf(noneAfter.value, ignoreCase);
|
|
72
78
|
return rule;
|
|
@@ -116,16 +122,20 @@ function composerOf(node) {
|
|
|
116
122
|
const entries = mapping(node, 'composer');
|
|
117
123
|
const mode = required(entries, 'mode', node.line);
|
|
118
124
|
const name = stringOf(mode.value);
|
|
125
|
+
// How the CLI renders its greyed suggestions, read off the input line's styling. Optional:
|
|
126
|
+
// a composer whose suggestions plain text already names needs none of it.
|
|
127
|
+
const style = optional(entries, 'placeholder_style');
|
|
128
|
+
const placeholderStyle = style ? placeholderStyleOf(style) : undefined;
|
|
119
129
|
if (name === 'box-to-rule') {
|
|
120
|
-
only(entries, ['mode', 'prompt', 'rule', 'placeholders']);
|
|
121
|
-
return { mode: name, prompt: regexField(entries, 'prompt', node.line), rule: regexField(entries, 'rule', node.line), placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value) };
|
|
130
|
+
only(entries, ['mode', 'prompt', 'rule', 'placeholders', 'placeholder_style']);
|
|
131
|
+
return { mode: name, prompt: regexField(entries, 'prompt', node.line), rule: regexField(entries, 'rule', node.line), placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value), placeholderStyle };
|
|
122
132
|
}
|
|
123
133
|
if (name === 'status-last') {
|
|
124
|
-
only(entries, ['mode', 'status_line', 'prompt', 'placeholders']);
|
|
125
|
-
return { mode: name, statusLine: regexField(entries, 'status_line', node.line), prompt: regexField(entries, 'prompt', node.line), placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value) };
|
|
134
|
+
only(entries, ['mode', 'status_line', 'prompt', 'placeholders', 'placeholder_style']);
|
|
135
|
+
return { mode: name, statusLine: regexField(entries, 'status_line', node.line), prompt: regexField(entries, 'prompt', node.line), placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value), placeholderStyle };
|
|
126
136
|
}
|
|
127
137
|
if (name === 'status-then-one') {
|
|
128
|
-
only(entries, ['mode', 'status_line', 'prompt', 'placeholders', 'strip_suffix', 'fallback']);
|
|
138
|
+
only(entries, ['mode', 'status_line', 'prompt', 'placeholders', 'placeholder_style', 'strip_suffix', 'fallback']);
|
|
129
139
|
const suffix = optional(entries, 'strip_suffix');
|
|
130
140
|
const fallback = required(entries, 'fallback', node.line);
|
|
131
141
|
return {
|
|
@@ -133,12 +143,13 @@ function composerOf(node) {
|
|
|
133
143
|
statusLine: regexField(entries, 'status_line', node.line),
|
|
134
144
|
prompt: regexField(entries, 'prompt', node.line),
|
|
135
145
|
placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value),
|
|
146
|
+
placeholderStyle,
|
|
136
147
|
stripSuffix: suffix ? patternOf(stringOf(suffix.value) ?? fail(suffix.line, '"strip_suffix" must be a string'), false, suffix.line) : null,
|
|
137
148
|
fallback: fallbackOf(fallback.value),
|
|
138
149
|
};
|
|
139
150
|
}
|
|
140
151
|
if (name === 'two-rules-footer-below') {
|
|
141
|
-
only(entries, ['mode', 'ignore_case', 'prompt', 'rule', 'footers', 'placeholders']);
|
|
152
|
+
only(entries, ['mode', 'ignore_case', 'prompt', 'rule', 'footers', 'placeholders', 'placeholder_style']);
|
|
142
153
|
const flag = optional(entries, 'ignore_case');
|
|
143
154
|
const ignoreCase = flag ? boolOf(flag.value, 'ignore_case') : false;
|
|
144
155
|
const footers = required(entries, 'footers', node.line);
|
|
@@ -155,10 +166,17 @@ function composerOf(node) {
|
|
|
155
166
|
return patternOf(text, ignoreCase, item.line);
|
|
156
167
|
}),
|
|
157
168
|
placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value),
|
|
169
|
+
placeholderStyle,
|
|
158
170
|
};
|
|
159
171
|
}
|
|
160
172
|
fail(mode.line, `"mode" must be box-to-rule, status-last, status-then-one or two-rules-footer-below`);
|
|
161
173
|
}
|
|
174
|
+
function placeholderStyleOf(entry) {
|
|
175
|
+
const text = stringOf(entry.value);
|
|
176
|
+
if (text !== 'dim')
|
|
177
|
+
fail(entry.line, '"placeholder_style" must be dim');
|
|
178
|
+
return text;
|
|
179
|
+
}
|
|
162
180
|
function fallbackOf(node) {
|
|
163
181
|
if (node.kind !== 'seq')
|
|
164
182
|
fail(node.line, '"fallback" must be a list');
|
package/dist/watch/screen.d.ts
CHANGED
|
@@ -18,3 +18,8 @@ export declare function classify(cli: string, lines: string[]): Screen;
|
|
|
18
18
|
/** The composer alone. A running turn would otherwise hide an empty input box. */
|
|
19
19
|
export declare function classifyComposer(cli: string, lines: string[]): Screen;
|
|
20
20
|
export declare function readScreen(cli: string, screen: string | undefined): Screen;
|
|
21
|
+
/** The one line a quota figure may come from: the composer's own status row, in the pane's last
|
|
22
|
+
* 20 lines, read plain — a figure is never styled. Null for a CLI with no status line, and for
|
|
23
|
+
* a window that shows no status row — a dialog, a question, a trust screen, a shell or an
|
|
24
|
+
* unknown one reads no figures. */
|
|
25
|
+
export declare function statusRow(cli: string, screen: string | undefined): string | null;
|
package/dist/watch/screen.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// What a pane's visible text shows, read against the shapes of its CLI. A screen that matches no
|
|
2
2
|
// shape is "unknown": never ready, never idle, and never grounds for typing anything.
|
|
3
|
+
// The text may keep its ANSI styling, as `herdr pane read --format ansi` reads it: matching
|
|
4
|
+
// runs on the plain form, and the styling tells a greyed suggestion from typed text.
|
|
3
5
|
import { readFileSync } from 'node:fs';
|
|
4
|
-
import {
|
|
6
|
+
import { stripSgr } from "../ansi.js";
|
|
7
|
+
import { classifyLines, composeLines, statusRowOf } from "./screen-core.js";
|
|
5
8
|
import { loadScreen } from "./screen-file.js";
|
|
6
9
|
function load(name) {
|
|
7
10
|
return loadScreen(readFileSync(new URL(`../profiles/${name}.yaml`, import.meta.url), 'utf8'));
|
|
@@ -13,7 +16,8 @@ const DATA = {
|
|
|
13
16
|
cursor: load('cursor'),
|
|
14
17
|
antigravity: load('antigravity'),
|
|
15
18
|
};
|
|
16
|
-
// The window every pattern sees: the pane's last 20 lines
|
|
19
|
+
// The window every pattern sees: the pane's last 20 lines. Styled lines are trimmed only past
|
|
20
|
+
// their last escape; the core trims each line's plain form for matching.
|
|
17
21
|
function windowOf(lines) {
|
|
18
22
|
return lines.map((line) => line.trimEnd()).slice(-20);
|
|
19
23
|
}
|
|
@@ -36,3 +40,13 @@ export function readScreen(cli, screen) {
|
|
|
36
40
|
return { kind: 'unknown' };
|
|
37
41
|
return classify(cli, screen.split('\n'));
|
|
38
42
|
}
|
|
43
|
+
/** The one line a quota figure may come from: the composer's own status row, in the pane's last
|
|
44
|
+
* 20 lines, read plain — a figure is never styled. Null for a CLI with no status line, and for
|
|
45
|
+
* a window that shows no status row — a dialog, a question, a trust screen, a shell or an
|
|
46
|
+
* unknown one reads no figures. */
|
|
47
|
+
export function statusRow(cli, screen) {
|
|
48
|
+
const data = DATA[cli];
|
|
49
|
+
if (data === undefined || screen === undefined)
|
|
50
|
+
return null;
|
|
51
|
+
return statusRowOf(data, windowOf(stripSgr(screen).split('\n')));
|
|
52
|
+
}
|
package/examples/team.yaml
CHANGED
|
@@ -49,6 +49,7 @@ seats:
|
|
|
49
49
|
label: codex
|
|
50
50
|
cli: codex
|
|
51
51
|
vendor: openai
|
|
52
|
+
account: openai-team # the seat's account, when one vendor has two; absent, its vendor
|
|
52
53
|
model: GPT Comet
|
|
53
54
|
version: "3"
|
|
54
55
|
display: GPT-3 Comet # the vendor's spelling, for the signature
|
|
@@ -74,3 +75,10 @@ seats:
|
|
|
74
75
|
version: "1"
|
|
75
76
|
launch: nimbus-claude # a launcher on the PATH, holding the account's key and endpoint
|
|
76
77
|
count: 3
|
|
78
|
+
|
|
79
|
+
budgets: # the owner's: what is budgeted, and how long a figure stays fresh
|
|
80
|
+
accounts:
|
|
81
|
+
openai-team: # the account codex-beacon spends
|
|
82
|
+
kind: subscription
|
|
83
|
+
reserve: 20% # refuse a launch on a figure inside it
|
|
84
|
+
sources: [status_line] # Codex's status line carries its weekly figure
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Set up, change and watch a project's team of AI agents from one file",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"typecheck": "tsc --noEmit",
|
|
24
24
|
"test": "bun test",
|
|
25
25
|
"e2e": "bun scripts/e2e.ts",
|
|
26
|
-
"build": "
|
|
26
|
+
"build": "bun scripts/build.ts",
|
|
27
27
|
"ci": "bun run typecheck && bun test && bun run build && node dist/cli.js --version"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|