@assuredloop/orca-bot-kit 0.21.0 → 0.23.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/README.md +26 -2
- package/package.json +1 -1
- package/skills/obk-bot-building/SKILL.md +3 -2
- package/src/book.js +2 -1
- package/src/bot.js +78 -1
- package/src/clear.js +455 -0
- package/src/cli.js +112 -6
- package/src/health.js +16 -8
- package/src/hooks.js +70 -32
- package/src/lines.js +7 -4
- package/src/message.js +81 -4
- package/src/orca-reload.applescript +1 -1
- package/src/orca-runtime.cjs +1 -1
- package/src/orca.js +18 -14
- package/src/record.js +5 -5
- package/src/temp.js +156 -32
- package/src/up.js +7 -6
package/src/orca.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Every call the kit makes to Orca goes through here (ADR
|
|
1
|
+
// Every call the kit makes to Orca goes through here (ADR 0034). Orca's CLI
|
|
2
2
|
// changes often, so the kit reads `--json` and never the human text, and keeps
|
|
3
3
|
// the parsing in one place.
|
|
4
4
|
//
|
|
@@ -216,7 +216,7 @@ const CLIENT_KILL_MS = 3000;
|
|
|
216
216
|
*
|
|
217
217
|
* Orca's window re-reads only when its runtime says the projects changed, and
|
|
218
218
|
* `setup-update` and `setup-delete` do not say so. `project.update` with no
|
|
219
|
-
* changes does, and Orca's CLI does not offer it (ADR
|
|
219
|
+
* changes does, and Orca's CLI does not offer it (ADR 0034). The caller prints
|
|
220
220
|
* `RELOAD_LINE` either way. It does not take a removed project out of the
|
|
221
221
|
* sidebar: `reloadWindow` does.
|
|
222
222
|
*/
|
|
@@ -227,7 +227,7 @@ export const tellWindow = (projectId) => askRuntime('project.update', { projectI
|
|
|
227
227
|
* runtime's answer, or undefined when there is none to be had.
|
|
228
228
|
*
|
|
229
229
|
* It goes through Orca's own runtime client out of the installed app, run by
|
|
230
|
-
* Orca's binary the way its `bin/orca` runs its CLI (ADR
|
|
230
|
+
* Orca's binary the way its `bin/orca` runs its CLI (ADR 0034). None of that is
|
|
231
231
|
* Orca's published interface, so anything that goes wrong is a quiet
|
|
232
232
|
* undefined: it never throws and is never tried twice.
|
|
233
233
|
*/
|
|
@@ -264,7 +264,7 @@ const RELOAD_KILL_MS = 5000;
|
|
|
264
264
|
|
|
265
265
|
/**
|
|
266
266
|
* Have Orca's window force-reload itself, after a project was removed: true
|
|
267
|
-
* only when it did (#343, ADR
|
|
267
|
+
* only when it did (#343, ADR 0034).
|
|
268
268
|
*
|
|
269
269
|
* The window keeps a removed project in its sidebar until it is rebuilt, and
|
|
270
270
|
* `tellWindow` only relabels the row "Unknown" (stablyai/orca#23224; once a
|
|
@@ -377,14 +377,14 @@ export const QUESTION_ON_SCREEN = 'question-on-screen';
|
|
|
377
377
|
|
|
378
378
|
/**
|
|
379
379
|
* What the tab `handle` renders: `{ question }`, true when it shows a question
|
|
380
|
-
* of its harness's own, or `{ screenUnreadable: <why> }`. Orca refusing is an
|
|
380
|
+
* of its harness's own, with the `rows` it read, or `{ screenUnreadable: <why> }`. Orca refusing is an
|
|
381
381
|
* answer that cannot be read, not an error, and so is anything that is not the
|
|
382
382
|
* rendered screen: accumulated output comes back in fragments (tech notes,
|
|
383
383
|
* section 1).
|
|
384
384
|
*/
|
|
385
385
|
function screenOf(handle, readMs) {
|
|
386
386
|
const read = screenRows(handle, readMs);
|
|
387
|
-
return read.rows === undefined ? { screenUnreadable: read.unreadable } : { question: questionIn(read.rows) };
|
|
387
|
+
return read.rows === undefined ? { screenUnreadable: read.unreadable } : { question: questionIn(read.rows), rows: read.rows };
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
/**
|
|
@@ -418,7 +418,7 @@ const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
|
|
|
418
418
|
* Whether the rows of a rendered screen hold a question of the harness's own.
|
|
419
419
|
*
|
|
420
420
|
* Every one seen is a numbered list of choices with the harness's pointer on
|
|
421
|
-
* one (tech notes, section 1; ADR
|
|
421
|
+
* one (tech notes, section 1; ADR 0034). The same pointer starts the harness's
|
|
422
422
|
* input line and its echo of the user's past turns, and the input line is the
|
|
423
423
|
* lowest of them whenever it is on screen, so only the lowest pointer row is
|
|
424
424
|
* asked about: a question counts while it stands in the input line's place.
|
|
@@ -445,11 +445,14 @@ export function questionIn(rows) {
|
|
|
445
445
|
* Whether the kit may type a line into `tabId`, the tab the book holds for a
|
|
446
446
|
* session in the Orca project at `home`, and the handle to type into when it
|
|
447
447
|
* may: `{ handle, agent }`, with the agent Orca names there. Otherwise nothing
|
|
448
|
-
* is typed, and the answer says why
|
|
448
|
+
* is typed, and the answer says why. `idle` says whether Orca's `tui-idle` wait
|
|
449
|
+
* answered ok, and `rows` is the screen the gate read, for a caller that must
|
|
450
|
+
* not type into a busy harness (#391):
|
|
449
451
|
* `{}` for a tab with no harness in it (none in the book, none Orca lists, or
|
|
450
452
|
* the shell in front), `{ blocked }` for one with something on screen waiting
|
|
451
453
|
* to be answered, and `{ unsure }`, a sentence, for one the kit cannot tell
|
|
452
|
-
* about
|
|
454
|
+
* about, with `psUnread` where `ps` could not read the tab (#350). Orca
|
|
455
|
+
* refusing throws, for the caller to report.
|
|
453
456
|
*
|
|
454
457
|
* The one gate for every line the kit types into a running session: the mail
|
|
455
458
|
* nudge and the skills reload. A busy harness passes it, since both harnesses
|
|
@@ -497,7 +500,8 @@ export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
|
497
500
|
const why = seen.front === undefined
|
|
498
501
|
? seen.unreadable
|
|
499
502
|
: `${seen.command} holds its terminal, and Orca names ${seen.agent ?? 'no agent'} in it`;
|
|
500
|
-
|
|
503
|
+
// Where `ps` could not read the tab, a reader that can may yet tell (#350).
|
|
504
|
+
return { unsure: `the kit could not tell whether a harness is running in it (${why}), so nothing was typed`, ...(seen.psUnread ? { psUnread: true } : {}) };
|
|
501
505
|
}
|
|
502
506
|
// Orca's reason does not cover every question: it called Codex's update
|
|
503
507
|
// offer idle, and a return typed into it took "Update now" (#329). So the
|
|
@@ -506,7 +510,7 @@ export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
|
506
510
|
if (seen.question === undefined) {
|
|
507
511
|
return { unsure: `the kit could not tell whether a question is waiting on its screen (${seen.screenUnreadable}), so nothing was typed` };
|
|
508
512
|
}
|
|
509
|
-
return { handle: live.handle, agent: seen.agent };
|
|
513
|
+
return { handle: live.handle, agent: seen.agent, idle: seen.answered, rows: seen.rows };
|
|
510
514
|
}
|
|
511
515
|
|
|
512
516
|
/**
|
|
@@ -554,13 +558,13 @@ const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
|
554
558
|
*
|
|
555
559
|
* `ps` is asked first. Where it cannot read the tab, as inside Codex's
|
|
556
560
|
* sandbox, where it does not start at all, Orca's runtime is asked instead
|
|
557
|
-
* (#298, ADR
|
|
561
|
+
* (#298, ADR 0034).
|
|
558
562
|
*/
|
|
559
563
|
function frontOf(handle, ptyId, readMs) {
|
|
560
564
|
const read = frontByPs(ptyId, readMs);
|
|
561
565
|
if (read.unreadable === undefined) return read;
|
|
562
566
|
const asked = frontByOrca(handle);
|
|
563
|
-
return asked.unreadable === undefined ? asked : { unreadable: `${read.unreadable}, and ${asked.unreadable}` };
|
|
567
|
+
return { ...(asked.unreadable === undefined ? asked : { unreadable: `${read.unreadable}, and ${asked.unreadable}` }), psUnread: true };
|
|
564
568
|
}
|
|
565
569
|
|
|
566
570
|
/**
|
|
@@ -590,7 +594,7 @@ function frontByOrca(handle) {
|
|
|
590
594
|
* Who holds the terminal of the pane `ptyId`, as `ps` says it.
|
|
591
595
|
*
|
|
592
596
|
* Orca gives the pane's pid in `diagnostics memory` and nowhere else, and `ps`
|
|
593
|
-
* gives that pid's terminal's foreground process group (ADR
|
|
597
|
+
* gives that pid's terminal's foreground process group (ADR 0034).
|
|
594
598
|
* On macOS the pane is `login` with the shell as its child, so the shell is in
|
|
595
599
|
* front when the group is the pane's own or that of a child of a `login` pane.
|
|
596
600
|
* `diagnostics memory` is a diagnostics command and may change, so everything
|
package/src/record.js
CHANGED
|
@@ -80,11 +80,10 @@ export async function recordSession(bots, name, said, tabId, shellPid) {
|
|
|
80
80
|
// this bot's folder, which every session of the bot shares — so it can say
|
|
81
81
|
// that a conversation nobody claims exists, and never whose it is.
|
|
82
82
|
const unclaimed = unclaimedFor(readBook(home), home, bot, tabId, id);
|
|
83
|
-
// A clear
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
const rules = said.source === CLEARED ? rulesStamp(home) : undefined;
|
|
83
|
+
// A clear reads the bot's instructions again, as a start does, so the book
|
|
84
|
+
// notes which ones it read: Claude Code's `/clear` (tech notes, section 2),
|
|
85
|
+
// and Codex's `/new`, seen here as a new id (#391).
|
|
86
|
+
const stamp = rulesStamp(home);
|
|
88
87
|
|
|
89
88
|
let cleared = false;
|
|
90
89
|
let told;
|
|
@@ -106,6 +105,7 @@ export async function recordSession(bots, name, said, tabId, shellPid) {
|
|
|
106
105
|
const uncertain = was.session === undefined && unclaimed.length > 0;
|
|
107
106
|
told = session;
|
|
108
107
|
|
|
108
|
+
const rules = cleared ? stamp : undefined;
|
|
109
109
|
const now = rememberSession(was, id, said.source);
|
|
110
110
|
book.sessions[session] = withUnclaimed(rules === undefined ? now : { ...now, rules }, unclaimed);
|
|
111
111
|
forgetClaimed(book);
|
package/src/temp.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// Any long-lived session can make a temporary session of its own bot for a
|
|
6
6
|
// piece of work, and retire it when the work is done, without Bot Father, who
|
|
7
7
|
// stays the manager of long-lived sessions. A temporary session can make one
|
|
8
|
-
// of its own, one level deep and one at a time (#464, ADR 0033).
|
|
8
|
+
// of its own, one level deep and one at a time (#464, ADR 0033). It can be
|
|
9
|
+
// made in a role of its bot's `temp_roles`, which gives its settings, and
|
|
10
|
+
// whose cap limits how many are open (#465). A temporary session is an ordinary
|
|
9
11
|
// session in every other way: in bot.yaml, brought up with a tab and a mailbox,
|
|
10
12
|
// its conversations kept in the book. What makes it temporary is one field of
|
|
11
13
|
// its book entry, `temporary: { maker, made }`, and the book is the one place
|
|
@@ -27,8 +29,8 @@ import path from 'node:path';
|
|
|
27
29
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
28
30
|
|
|
29
31
|
import { readBook, updateBook } from './book.js';
|
|
30
|
-
import { addSession, dropSession, NAME, readBot } from './bot.js';
|
|
31
|
-
import { harnessOf, isShortPrompt, ownCli, shellWord, startPrompt, workDirOf } from './launch.js';
|
|
32
|
+
import { addSession, dropSession, NAME, readBot, tempRoles } from './bot.js';
|
|
33
|
+
import { DEFAULT_APPROVAL, harnessOf, isShortPrompt, ownCli, shellWord, startPrompt, workDirOf } from './launch.js';
|
|
32
34
|
import { sessionInTab } from './message.js';
|
|
33
35
|
import { orca, screenRows, tabs } from './orca.js';
|
|
34
36
|
import { retireSession } from './retire.js';
|
|
@@ -39,45 +41,43 @@ const INHERITED = ['model', 'effort', 'context', 'approval'];
|
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Make a temporary session of the caller's own bot and bring it up. `given`
|
|
42
|
-
* holds what the caller said: `name`, `prompt` or `prompt_file`,
|
|
43
|
-
* `harness` and the inherited settings
|
|
44
|
-
*
|
|
44
|
+
* holds what the caller said: `name`, `prompt` or `prompt_file`, any of
|
|
45
|
+
* `harness` and the inherited settings, and `role`, as `role` or
|
|
46
|
+
* `role:option`, to take the settings that role's option gives in its bot's
|
|
47
|
+
* `temp_roles` (#465). Returns `{ bot, session, maker, settings, chosen,
|
|
48
|
+
* role, up }`: `chosen` says where each setting came from, `role` is there
|
|
49
|
+
* when one was asked for, and `up` is what bringing it up answered.
|
|
45
50
|
*/
|
|
46
51
|
export async function makeTemp(bots, { tab, ...given }) {
|
|
47
52
|
const caller = callerIn(bots, tab, 'make');
|
|
48
53
|
// Checked here, before anything is written, and again under the book's lock
|
|
49
54
|
// below, where two makes at once cannot both pass it.
|
|
50
55
|
refuseNested(bots, caller, readBook(caller.home));
|
|
56
|
+
const bot = readBot(caller.home, caller.bot);
|
|
57
|
+
const role = given.role === undefined ? undefined : roleAsked(caller, bot, given.role);
|
|
58
|
+
const name = role === undefined ? given.name : roleName(caller, bot, role, given.name);
|
|
51
59
|
// The name is a folder under work/ and a file beside the bots folder, so it
|
|
52
60
|
// is held to the rule a bot's name is: one plain name, never a path.
|
|
53
|
-
if (!NAME.test(
|
|
54
|
-
throw new Error(`${
|
|
55
|
-
}
|
|
56
|
-
if (given.prompt === undefined && given.prompt_file === undefined) {
|
|
57
|
-
throw new Error(`temp make needs the task: --prompt <text> or --prompt-file <path>. Nothing was made.`);
|
|
61
|
+
if (!NAME.test(name)) {
|
|
62
|
+
throw new Error(`${name} cannot be a session's name: a name is lower-case letters, digits and single hyphens, such as review-250. Nothing was made.`);
|
|
58
63
|
}
|
|
59
64
|
if (given.prompt !== undefined && given.prompt_file !== undefined) {
|
|
60
65
|
throw new Error(`temp make was given the task twice, as --prompt and as --prompt-file, and a session is told its duty once. Give it one way. Nothing was made.`);
|
|
61
66
|
}
|
|
67
|
+
if (given.prompt === undefined && given.prompt_file === undefined && role?.prompt_file === undefined) {
|
|
68
|
+
throw new Error(`temp make needs the task: --prompt <text> or --prompt-file <path>${role === undefined ? '' : `, since the role ${role.name} has no prompt file`}. Nothing was made.`);
|
|
69
|
+
}
|
|
70
|
+
if (role !== undefined) refuseCap(caller, role, readBook(caller.home));
|
|
62
71
|
|
|
63
|
-
const bot = readBot(caller.home, caller.bot);
|
|
64
72
|
const maker = bot.sessions.find((session) => session.name === caller.session);
|
|
65
|
-
const settings =
|
|
66
|
-
|
|
67
|
-
if (harness !== undefined) settings.harness = harness;
|
|
68
|
-
// On another harness than its maker's, the model, the effort and the context
|
|
69
|
-
// are that harness's own, and only the approval carries over (#238, PRD 6.4).
|
|
70
|
-
const same = (harness ?? bot.harness) === harnessOf(maker ?? {}, bot.harness);
|
|
71
|
-
for (const field of INHERITED) {
|
|
72
|
-
const value = given[field] ?? (same || field === 'approval' ? maker?.[field] : undefined);
|
|
73
|
-
if (value !== undefined) settings[field] = value;
|
|
74
|
-
}
|
|
73
|
+
const { settings, chosen } = settingsFor(bot, maker, role?.option ?? {}, given);
|
|
74
|
+
settings.name = name;
|
|
75
75
|
if (given.extra_args !== undefined) settings.extra_args = given.extra_args;
|
|
76
76
|
if (given.prompt !== undefined) settings.prompt = given.prompt;
|
|
77
|
-
else settings.prompt_file = given.prompt_file;
|
|
78
|
-
settings.work_dir = `work/${
|
|
77
|
+
else settings.prompt_file = given.prompt_file ?? role.prompt_file;
|
|
78
|
+
settings.work_dir = `work/${name}`;
|
|
79
79
|
|
|
80
|
-
const written = bot.sessions.some((session) => session.name ===
|
|
80
|
+
const written = bot.sessions.some((session) => session.name === name) ? undefined : writePromptFirst(bots, caller, settings);
|
|
81
81
|
// bot.yaml and the book are written under the book's lock, so that a second
|
|
82
82
|
// make from the same maker waits, and then sees this one (#464). addSession
|
|
83
83
|
// refuses a name the bot already has, and a setting that will not work,
|
|
@@ -89,21 +89,23 @@ export async function makeTemp(bots, { tab, ...given }) {
|
|
|
89
89
|
await updateBook(caller.home, (book) => {
|
|
90
90
|
locked = true;
|
|
91
91
|
refuseNested(bots, caller, book);
|
|
92
|
+
if (role !== undefined) refuseCap(caller, role, book);
|
|
92
93
|
added = addSession(bots, caller.bot, settings);
|
|
93
|
-
|
|
94
|
+
const kind = role === undefined ? {} : { role: role.name, option: role.option.name };
|
|
95
|
+
book.sessions[name] = { ...book.sessions[name], temporary: { maker: caller.session, made, ...kind } };
|
|
94
96
|
});
|
|
95
97
|
} catch (error) {
|
|
96
98
|
// A refusal under the lock has written nothing but the prompt file.
|
|
97
99
|
const refused = locked && added === undefined;
|
|
98
|
-
const failed = `${caller.bot}/${
|
|
100
|
+
const failed = `${caller.bot}/${name} could not be recorded in the book as temporary (${error.message})`;
|
|
99
101
|
// A session in bot.yaml that the book does not call temporary would be a
|
|
100
102
|
// long-lived one to everything that reads it, its maker's retire included,
|
|
101
103
|
// so it is taken back off rather than left that way.
|
|
102
104
|
if (added !== undefined) {
|
|
103
105
|
try {
|
|
104
|
-
dropSession(bots, caller.bot,
|
|
106
|
+
dropSession(bots, caller.bot, name);
|
|
105
107
|
} catch (undo) {
|
|
106
|
-
throw new Error(`${failed}, and taking it back off bot.yaml failed too (${undo.message}). It is in bot.yaml as a session the book does not call temporary. Take it off with ${shellWord(ownCli())} retire --bots ${shellWord(bots)} --bot ${caller.bot} --session ${
|
|
108
|
+
throw new Error(`${failed}, and taking it back off bot.yaml failed too (${undo.message}). It is in bot.yaml as a session the book does not call temporary. Take it off with ${shellWord(ownCli())} retire --bots ${shellWord(bots)} --bot ${caller.bot} --session ${name}`);
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
// Off bot.yaml, nothing is made; a prompt file that stays is only a file,
|
|
@@ -120,12 +122,134 @@ export async function makeTemp(bots, { tab, ...given }) {
|
|
|
120
122
|
|
|
121
123
|
let up;
|
|
122
124
|
try {
|
|
123
|
-
up = await bringUp(bots, { bot: caller.bot, session:
|
|
125
|
+
up = await bringUp(bots, { bot: caller.bot, session: name });
|
|
124
126
|
} catch (error) {
|
|
125
127
|
const cli = shellWord(ownCli());
|
|
126
|
-
throw new Error(`${caller.bot}/${
|
|
128
|
+
throw new Error(`${caller.bot}/${name} is made, in bot.yaml and in the book as temporary with its maker ${caller.session}, and could not be brought up: ${error.message} Bring it up with ${cli} up --bots ${shellWord(bots)} --bot ${caller.bot} --session ${name} or retire it with ${cli} temp retire --bots ${shellWord(bots)} --name ${name}`);
|
|
129
|
+
}
|
|
130
|
+
return { bot: caller.bot, session: name, maker: caller.session, settings: added.session, chosen, ...(role === undefined ? {} : { role: roleAnswer(role) }), up };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The settings a temporary session is made with, and for each of the ones
|
|
135
|
+
* that can be inherited, what it got and where from: `chosen` (#465). A flag
|
|
136
|
+
* wins; then the role's option, on the option's harness, which is the maker's
|
|
137
|
+
* when the option names none; then the maker, on the maker's harness; and
|
|
138
|
+
* otherwise the harness's own default. On another harness than its maker's,
|
|
139
|
+
* only the approval carries over (#238, PRD 6.4).
|
|
140
|
+
*/
|
|
141
|
+
function settingsFor(bot, maker, option, given) {
|
|
142
|
+
const makers = harnessOf(maker ?? {}, bot.harness);
|
|
143
|
+
const settings = {};
|
|
144
|
+
const chosen = {};
|
|
145
|
+
const pick = (field, ...sources) => {
|
|
146
|
+
// YAML reads a setting written with no value as null, which gives nothing.
|
|
147
|
+
const found = sources.find(([, value]) => value !== undefined && value !== null);
|
|
148
|
+
if (found === undefined) {
|
|
149
|
+
chosen[field] = { from: 'harness default' };
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
chosen[field] = { value: found[1], from: found[0] };
|
|
153
|
+
if (found[0] !== 'maker' || field !== 'harness' || maker?.harness !== undefined) settings[field] = found[1];
|
|
154
|
+
};
|
|
155
|
+
pick('harness', ['flag', given.harness], ['role', option.harness], ['maker', makers]);
|
|
156
|
+
const harness = chosen.harness.value;
|
|
157
|
+
const fromRole = harness === (option.harness ?? makers);
|
|
158
|
+
const same = harness === makers;
|
|
159
|
+
for (const field of INHERITED) {
|
|
160
|
+
pick(
|
|
161
|
+
field,
|
|
162
|
+
['flag', given[field]],
|
|
163
|
+
['role', fromRole && field !== 'approval' ? option[field] : undefined],
|
|
164
|
+
['maker', same || field === 'approval' ? maker?.[field] : undefined],
|
|
165
|
+
// A session's approval is never left to the harness (ADR 0015).
|
|
166
|
+
['kit default', field === 'approval' ? DEFAULT_APPROVAL : undefined],
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return { settings, chosen };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The role and option `asked` names, as `role` or `role:option`, from the
|
|
174
|
+
* bot's roles, with the option it takes; or a refusal that names what the bot
|
|
175
|
+
* has to choose from.
|
|
176
|
+
*/
|
|
177
|
+
function roleAsked(caller, bot, asked) {
|
|
178
|
+
const [name, optionName, ...more] = asked.split(':');
|
|
179
|
+
const roles = tempRoles(caller.home, bot);
|
|
180
|
+
if (roles.length === 0) {
|
|
181
|
+
throw new Error(`${caller.bot} has no roles for temporary sessions: its bot.yaml has no temp_roles. Make it without --role, or give the bot its roles first. Nothing was made.`);
|
|
127
182
|
}
|
|
128
|
-
|
|
183
|
+
const role = roles.find((one) => one.name === name);
|
|
184
|
+
if (role === undefined || more.length > 0) {
|
|
185
|
+
throw new Error(`${caller.bot} has no role ${asked}. Its roles are ${roles.map((one) => one.name).join(', ')}; ask for one as --role <role> or --role <role>:<option>. Nothing was made.`);
|
|
186
|
+
}
|
|
187
|
+
if (role.trouble.length > 0) {
|
|
188
|
+
throw new Error(`the role ${name} cannot be used as it is written: ${role.trouble.join(' ')} Nothing was made.`);
|
|
189
|
+
}
|
|
190
|
+
const option = optionName === undefined ? role.options[0] : role.options.find((one) => one.name === optionName);
|
|
191
|
+
if (option === undefined) {
|
|
192
|
+
throw new Error(`the role ${name} has no option ${optionName}. Its options are ${optionsLine(optionsOf(role))}. Nothing was made.`);
|
|
193
|
+
}
|
|
194
|
+
return { ...role, option };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The name of a session made in `role`: `--name` as given when it already
|
|
199
|
+
* begins with the role, or with the role put in front. A name that begins with
|
|
200
|
+
* another of the bot's roles would say the wrong one, and is refused.
|
|
201
|
+
*/
|
|
202
|
+
function roleName(caller, bot, role, given) {
|
|
203
|
+
if (given.startsWith(`${role.name}-`)) return given;
|
|
204
|
+
const other = tempRoles(caller.home, bot).find((one) => given.startsWith(`${one.name}-`));
|
|
205
|
+
if (other !== undefined) {
|
|
206
|
+
throw new Error(`${given} begins with ${other.name}-, the name of another of ${caller.bot}'s roles, and a session made in ${role.name} is named for ${role.name}. Give --name without it, such as --name ${given.slice(other.name.length + 1)}, to make ${role.name}-${given.slice(other.name.length + 1)}. Nothing was made.`);
|
|
207
|
+
}
|
|
208
|
+
return `${role.name}-${given}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Refuse a make in a role whose cap of open sessions is reached, against `book` (#465). */
|
|
212
|
+
function refuseCap(caller, role, book) {
|
|
213
|
+
if (role.cap === undefined) return;
|
|
214
|
+
const open = Object.keys(book.sessions).filter((name) => book.sessions[name]?.temporary?.role === role.name);
|
|
215
|
+
if (open.length >= role.cap) {
|
|
216
|
+
throw new Error(`the role ${role.name} has a cap of ${role.cap} open temporary sessions, and ${caller.bot} has ${open.length} open: ${open.join(', ')}. Retire one whose work is done first. Nothing was made.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A role's options as an answer gives them: each one's name, what it is for, and whether it is the default. */
|
|
221
|
+
const optionsOf = (role) => role.options.map((option, at) => ({ name: option.name, ...(option.for === undefined ? {} : { for: option.for }), default: at === 0 }));
|
|
222
|
+
|
|
223
|
+
/** Options as `optionsOf` gives them, on one line. */
|
|
224
|
+
export const optionsLine = (options) => options
|
|
225
|
+
.map((option) => `${option.name}${option.default ? ' (the default)' : ''}${option.for === undefined ? '' : `, for ${option.for}`}`)
|
|
226
|
+
.join('; ');
|
|
227
|
+
|
|
228
|
+
/** A role as an answer gives it: the option used, and every option the role has. */
|
|
229
|
+
const roleAnswer = (role) => ({
|
|
230
|
+
name: role.name,
|
|
231
|
+
option: role.option.name,
|
|
232
|
+
...(role.option.for === undefined ? {} : { for: role.option.for }),
|
|
233
|
+
options: optionsOf(role),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The roles of the caller's bot, each with its cap, its open temporary
|
|
238
|
+
* sessions, its prompt file and its options, the first the default (#465).
|
|
239
|
+
* Returns `{ bot, roles }`.
|
|
240
|
+
*/
|
|
241
|
+
export function listRoles(bots, { tab }) {
|
|
242
|
+
const caller = callerIn(bots, tab, 'roles', 'list the roles');
|
|
243
|
+
const book = readBook(caller.home);
|
|
244
|
+
const roles = tempRoles(caller.home, readBot(caller.home, caller.bot)).map((role) => ({
|
|
245
|
+
name: role.name,
|
|
246
|
+
...(role.cap === undefined ? {} : { cap: role.cap }),
|
|
247
|
+
open: Object.keys(book.sessions).filter((name) => book.sessions[name]?.temporary?.role === role.name),
|
|
248
|
+
...(role.prompt_file === undefined ? {} : { prompt_file: role.prompt_file }),
|
|
249
|
+
options: role.options.map((option, at) => ({ ...option, default: at === 0 })),
|
|
250
|
+
...(role.trouble.length === 0 ? {} : { trouble: role.trouble }),
|
|
251
|
+
}));
|
|
252
|
+
return { bot: caller.bot, roles };
|
|
129
253
|
}
|
|
130
254
|
|
|
131
255
|
/**
|
package/src/up.js
CHANGED
|
@@ -408,12 +408,13 @@ const LIST_LOOK_MS = 2000;
|
|
|
408
408
|
const LIST_ASK_MS = 500;
|
|
409
409
|
|
|
410
410
|
/**
|
|
411
|
-
* Type LIST_LINE into the tab `tabId` once the
|
|
412
|
-
* through lets it: `{ typed: true }`, or
|
|
413
|
-
* gate's last reason once LIST_WAIT_MS is up.
|
|
414
|
-
* form or a menu (#329, #416), nor into a tab
|
|
411
|
+
* Type `line`, LIST_LINE unless told otherwise, into the tab `tabId` once the
|
|
412
|
+
* gate every typed line goes through lets it: `{ typed: true }`, or
|
|
413
|
+
* `{ typed: false, why }` with the gate's last reason once LIST_WAIT_MS is up.
|
|
414
|
+
* Nothing goes into a question, a form or a menu (#329, #416), nor into a tab
|
|
415
|
+
* the kit cannot tell about.
|
|
415
416
|
*/
|
|
416
|
-
async function typeListLine(home, tabId) {
|
|
417
|
+
export async function typeListLine(home, tabId, line = LIST_LINE) {
|
|
417
418
|
const until = Date.now() + LIST_WAIT_MS;
|
|
418
419
|
for (;;) {
|
|
419
420
|
let found;
|
|
@@ -430,7 +431,7 @@ async function typeListLine(home, tabId) {
|
|
|
430
431
|
}
|
|
431
432
|
if (found.handle !== undefined) {
|
|
432
433
|
try {
|
|
433
|
-
typeIntoTab(found.handle,
|
|
434
|
+
typeIntoTab(found.handle, line);
|
|
434
435
|
return { typed: true };
|
|
435
436
|
} catch (error) {
|
|
436
437
|
return { typed: false, why: `Orca refused the line: ${error.message}` };
|