@assuredloop/orca-bot-kit 0.26.1 → 0.28.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 +68 -41
- package/SETUP.md +26 -13
- package/package.json +1 -1
- package/rules/mail.md +8 -8
- package/skills/obk-bot-building/SKILL.md +24 -15
- package/skills/obk-grooming/SKILL.md +4 -3
- package/src/book.js +143 -23
- package/src/bot.js +246 -118
- package/src/clear.js +7 -3
- package/src/cli.js +129 -130
- package/src/commands.js +21 -14
- package/src/conversations.js +73 -40
- package/src/groom.js +27 -9
- package/src/health.js +79 -11
- package/src/hooks.js +42 -64
- package/src/init.js +36 -29
- package/src/launch.js +221 -20
- package/src/message.js +83 -335
- package/src/name.js +1 -1
- package/src/orca-reload.applescript +1 -1
- package/src/orca-runtime.cjs +1 -1
- package/src/orca.js +35 -42
- package/src/permissions.js +30 -16
- package/src/pi.js +55 -0
- package/src/processes.js +392 -0
- package/src/retire.js +43 -40
- package/src/rules.js +5 -2
- package/src/skills.js +51 -35
- package/src/sources.js +73 -4
- package/src/temp.js +65 -20
- package/src/up.js +80 -14
- package/src/unread.js +0 -160
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 0046). 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
|
//
|
|
@@ -106,6 +106,15 @@ export function orcaTrouble() {
|
|
|
106
106
|
if (asked.error) return `could not run Orca at ${orcaCli()}: ${asked.error.message}. ${where}`;
|
|
107
107
|
|
|
108
108
|
const answer = parse(asked.stdout);
|
|
109
|
+
// A CLI that dies with no answer has not reached the app at all: it can fail
|
|
110
|
+
// on its own, as on a current folder macOS will not let it read, while Orca
|
|
111
|
+
// runs (#556). So what it said is passed on, and nobody is sent to start Orca.
|
|
112
|
+
if (answer === undefined && asked.status !== 0) {
|
|
113
|
+
const exit = asked.status ?? `signal ${asked.signal}`;
|
|
114
|
+
return `the Orca CLI at ${orcaCli()} failed before it answered status (exit ${exit}; `
|
|
115
|
+
+ `stdout ${said(asked.stdout)}; stderr ${said(asked.stderr)}). `
|
|
116
|
+
+ 'Fix what it says and run the command again, or point OBK_ORCA at another Orca CLI.';
|
|
117
|
+
}
|
|
109
118
|
// Orca knowing what is wrong beats the kit guessing, so its own words go
|
|
110
119
|
// first when it gave any.
|
|
111
120
|
if (answer !== null && typeof answer === 'object' && answer.ok !== true && answer.error?.message) {
|
|
@@ -319,7 +328,7 @@ const CLIENT_KILL_MS = 3000;
|
|
|
319
328
|
*
|
|
320
329
|
* Orca's window re-reads only when its runtime says the projects changed, and
|
|
321
330
|
* `setup-update` and `setup-delete` do not say so. `project.update` with no
|
|
322
|
-
* changes does, and Orca's CLI does not offer it (ADR
|
|
331
|
+
* changes does, and Orca's CLI does not offer it (ADR 0046). The caller prints
|
|
323
332
|
* `RELOAD_LINE` either way. It does not take a removed project out of the
|
|
324
333
|
* sidebar: `reloadWindow` does.
|
|
325
334
|
*/
|
|
@@ -330,7 +339,7 @@ export const tellWindow = (projectId) => askRuntime('project.update', { projectI
|
|
|
330
339
|
* runtime's answer, or undefined when there is none to be had.
|
|
331
340
|
*
|
|
332
341
|
* It goes through Orca's own runtime client out of the installed app, run by
|
|
333
|
-
* Orca's binary the way its `bin/orca` runs its CLI (ADR
|
|
342
|
+
* Orca's binary the way its `bin/orca` runs its CLI (ADR 0046). None of that is
|
|
334
343
|
* Orca's published interface, so anything that goes wrong is a quiet
|
|
335
344
|
* undefined: it never throws and is never tried twice.
|
|
336
345
|
*/
|
|
@@ -367,7 +376,7 @@ const RELOAD_KILL_MS = 5000;
|
|
|
367
376
|
|
|
368
377
|
/**
|
|
369
378
|
* Have Orca's window force-reload itself, after a project was removed: true
|
|
370
|
-
* only when it did (#343, ADR
|
|
379
|
+
* only when it did (#343, ADR 0046).
|
|
371
380
|
*
|
|
372
381
|
* The window keeps a removed project in its sidebar until it is rebuilt, and
|
|
373
382
|
* `tellWindow` only relabels the row "Unknown" (stablyai/orca#23224; once a
|
|
@@ -426,23 +435,6 @@ export const openTab = (home, title) =>
|
|
|
426
435
|
export const retitleTab = (handle, title) =>
|
|
427
436
|
orca(['terminal', 'rename', '--terminal', handle, '--title', title]).rename;
|
|
428
437
|
|
|
429
|
-
/**
|
|
430
|
-
* One quick look at whether the harness in a tab is at rest, for a caller that
|
|
431
|
-
* already knows a harness is there and is watching for a turn to start (#509):
|
|
432
|
-
* `idle` when Orca's tui-idle answers ok, `busy` when it times out, and
|
|
433
|
-
* `unknown` when Orca refuses or does not answer within `waitMs` and a second
|
|
434
|
-
* more. Never throws.
|
|
435
|
-
*/
|
|
436
|
-
export function idleNow(handle, waitMs) {
|
|
437
|
-
try {
|
|
438
|
-
const answer = ask(['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', String(waitMs)], waitMs + 1000);
|
|
439
|
-
if (answer.ok === true) return answer.result?.wait?.blockedReason === undefined ? 'idle' : 'unknown';
|
|
440
|
-
return answer.error?.code === 'timeout' ? 'busy' : 'unknown';
|
|
441
|
-
} catch {
|
|
442
|
-
return 'unknown';
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
438
|
/**
|
|
447
439
|
* What can be learned about the harness in a tab: what Orca says, and who is
|
|
448
440
|
* in front of the tab's terminal.
|
|
@@ -541,7 +533,7 @@ const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
|
|
|
541
533
|
* Whether the rows of a rendered screen hold a question of the harness's own.
|
|
542
534
|
*
|
|
543
535
|
* Every one seen is a numbered list of choices with the harness's pointer on
|
|
544
|
-
* one (tech notes, section 1; ADR
|
|
536
|
+
* one (tech notes, section 1; ADR 0046). The same pointer starts the harness's
|
|
545
537
|
* input line and its echo of the user's past turns, and the input line is the
|
|
546
538
|
* lowest of them whenever it is on screen, so the lowest pointer row is asked
|
|
547
539
|
* about: a question counts while it stands in the input line's place, or
|
|
@@ -637,14 +629,14 @@ function pointedChoiceAt(rows, at) {
|
|
|
637
629
|
* is typed, and the answer says why. `idle` says whether Orca's `tui-idle` wait
|
|
638
630
|
* answered ok, and `rows` is the screen the gate read, for a caller that must
|
|
639
631
|
* not type into a busy harness (#391), with the `draft` Orca gave beside it (#510):
|
|
640
|
-
* `{}` for a tab with no harness in it (none in the book,
|
|
641
|
-
*
|
|
632
|
+
* `{}` for a tab with no harness in it (none in the book, or the shell in
|
|
633
|
+
* front), `{ gone: true }` for one Orca does not list, `{ blocked }` for one with something on screen waiting
|
|
642
634
|
* to be answered, and `{ unsure }`, a sentence, for one the kit cannot tell
|
|
643
635
|
* about, with `psUnread` where `ps` could not read the tab (#350). Orca
|
|
644
636
|
* refusing throws, for the caller to report.
|
|
645
637
|
*
|
|
646
638
|
* The one gate for every line the kit types into a running session: the mail
|
|
647
|
-
*
|
|
639
|
+
* interrupt and the skills reload. A busy harness passes it, since both harnesses
|
|
648
640
|
* take a typed line as their next turn.
|
|
649
641
|
*/
|
|
650
642
|
export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
@@ -655,7 +647,7 @@ export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
|
655
647
|
let seen;
|
|
656
648
|
try {
|
|
657
649
|
live = tabs(home, { timeoutMs: GATE_READ_MS }).find((tab) => tab.tabId === tabId);
|
|
658
|
-
if (live === undefined) return {};
|
|
650
|
+
if (live === undefined) return { gone: true };
|
|
659
651
|
seen = harnessInTab(live.handle, timeoutMs, GATE_READ_MS);
|
|
660
652
|
} catch (error) {
|
|
661
653
|
if (error.code !== TIMED_OUT) throw error;
|
|
@@ -737,7 +729,7 @@ export function frontOfTab(handle) {
|
|
|
737
729
|
}
|
|
738
730
|
|
|
739
731
|
/** The `ps` this run reads. OBK_PS overrides it, as OBK_ORCA does Orca. */
|
|
740
|
-
const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
732
|
+
export const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
741
733
|
|
|
742
734
|
/**
|
|
743
735
|
* Who holds the terminal of the tab `handle`, whose pane is `ptyId`:
|
|
@@ -747,7 +739,7 @@ const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
|
747
739
|
*
|
|
748
740
|
* `ps` is asked first. Where it cannot read the tab, as inside Codex's
|
|
749
741
|
* sandbox, where it does not start at all, Orca's runtime is asked instead
|
|
750
|
-
* (#298, ADR
|
|
742
|
+
* (#298, ADR 0046). `readMs` bounds each call: a number, or a function that
|
|
751
743
|
* says what is left of a wait, asked before each one (#498). A `ps` that runs
|
|
752
744
|
* out of it ends the look, `late`, with no runtime asked after it.
|
|
753
745
|
*/
|
|
@@ -787,7 +779,7 @@ function frontByOrca(handle, readMs) {
|
|
|
787
779
|
* Who holds the terminal of the pane `ptyId`, as `ps` says it.
|
|
788
780
|
*
|
|
789
781
|
* Orca gives the pane's pid in `diagnostics memory` and nowhere else, and `ps`
|
|
790
|
-
* gives that pid's terminal's foreground process group (ADR
|
|
782
|
+
* gives that pid's terminal's foreground process group (ADR 0046).
|
|
791
783
|
* On macOS the pane is `login` with the shell as its child, so the shell is in
|
|
792
784
|
* front when the group is the pane's own or that of a child of a `login` pane.
|
|
793
785
|
* `diagnostics memory` is a diagnostics command and may change, so everything
|
|
@@ -834,7 +826,7 @@ function panePid(ptyId, readMs) {
|
|
|
834
826
|
}
|
|
835
827
|
|
|
836
828
|
/** The `lsof` and `stty` this run asks. OBK_LSOF and OBK_STTY override them, as OBK_PS does `ps`. */
|
|
837
|
-
const lsofCli = () => process.env.OBK_LSOF || '/usr/sbin/lsof';
|
|
829
|
+
export const lsofCli = () => process.env.OBK_LSOF || '/usr/sbin/lsof';
|
|
838
830
|
const sttyCli = () => process.env.OBK_STTY || '/bin/stty';
|
|
839
831
|
|
|
840
832
|
/**
|
|
@@ -968,18 +960,12 @@ export const closeTerminal = (handle) =>
|
|
|
968
960
|
* Seen live on 2026-09-21 in a system test run; not reproduced since, including
|
|
969
961
|
* from a plain shell, long lines and lines sent while the agent was working.
|
|
970
962
|
*
|
|
971
|
-
* The wait is short
|
|
972
|
-
*
|
|
973
|
-
* than a command that hangs for half a minute.
|
|
974
|
-
*
|
|
975
|
-
* With `watch`, the line is sent with that wait from the start, and Orca's
|
|
976
|
-
* receipt says whether it saw the line start a turn (#394). It gives back
|
|
977
|
-
* Orca's `result`: the receipt is in its `send.prompt`, and Orca's words about
|
|
978
|
-
* a turn it did not see in its `warnings`.
|
|
963
|
+
* The wait is short: a line that has to wait is a smaller cost than a
|
|
964
|
+
* command that hangs for half a minute.
|
|
979
965
|
*/
|
|
980
|
-
export function typeIntoTab(handle, text
|
|
966
|
+
export function typeIntoTab(handle, text) {
|
|
981
967
|
const args = ['terminal', 'send', '--terminal', handle, '--text', text, '--enter'];
|
|
982
|
-
const answer = ask(
|
|
968
|
+
const answer = ask(args);
|
|
983
969
|
if (answer.ok === true) return answer.result;
|
|
984
970
|
|
|
985
971
|
const again = answer.error?.data?.orchestrationRequestId;
|
|
@@ -990,11 +976,18 @@ export function typeIntoTab(handle, text, { watch = false } = {}) {
|
|
|
990
976
|
}
|
|
991
977
|
|
|
992
978
|
/**
|
|
993
|
-
* How long a
|
|
994
|
-
*
|
|
979
|
+
* How long a re-issued line is given to be submitted before Orca gives up on
|
|
980
|
+
* it. An idle harness started its turn in about 2 s, live (#394).
|
|
995
981
|
*/
|
|
996
982
|
const SUBMIT_WAIT_S = 5;
|
|
997
983
|
|
|
984
|
+
/**
|
|
985
|
+
* Press Escape once in a tab, with no return after it: what `obk message send
|
|
986
|
+
* --interrupt` sends to a busy receiver (#555).
|
|
987
|
+
*/
|
|
988
|
+
export const pressEscape = (handle) =>
|
|
989
|
+
orca(['terminal', 'send', '--terminal', handle, '--text', '\x1b']);
|
|
990
|
+
|
|
998
991
|
/** Where Orca names the terminal a process runs in, in every pane it opens. */
|
|
999
992
|
export const TERMINAL_ENV = 'ORCA_TERMINAL_HANDLE';
|
|
1000
993
|
|
package/src/permissions.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// body beside the bots folder, commit. A matching allow rule is settled before
|
|
7
7
|
// that check (Claude Code's permissions docs), so the kit gives every bot a
|
|
8
8
|
// default set, spelled with its real CLI and bots folder, with nobody asked
|
|
9
|
-
// (#527, ADR
|
|
9
|
+
// (#527, ADR 0050). A rule beyond it is the user's yes, which
|
|
10
10
|
// `permission allow` keeps in bot.yaml's `allow`. The kit's code writes what
|
|
11
11
|
// `allow` holds into `.claude/settings.json` in the bot folder, beside its hook
|
|
12
12
|
// (ADR 0022). A model editing the file by hand may get the format wrong; code
|
|
@@ -24,7 +24,7 @@ import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync
|
|
|
24
24
|
import path from 'node:path';
|
|
25
25
|
|
|
26
26
|
import { allowRules, leadsOutside } from './bot.js';
|
|
27
|
-
import { DEFAULT_COMMANDS, NO_LONGER_DEFAULT } from './commands.js';
|
|
27
|
+
import { DEFAULT_COMMANDS, NO_LONGER_DEFAULT, REMOVED } from './commands.js';
|
|
28
28
|
import { readSettings } from './hooks.js';
|
|
29
29
|
import { harnessOf, ownCli, shellWord } from './launch.js';
|
|
30
30
|
|
|
@@ -36,11 +36,10 @@ const CODEX_RULES = '.codex/rules';
|
|
|
36
36
|
const CODEX_FILE = `${CODEX_RULES}/obk.rules`;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* The rules every bot is given (ADR
|
|
40
|
-
* and none kept back for the user's yes (ADR
|
|
39
|
+
* The rules every bot is given (ADR 0050): each of the kit's default commands,
|
|
40
|
+
* and none kept back for the user's yes (ADR 0051), narrowed to this bots folder and
|
|
41
41
|
* spelled as the kit prints them, which is what a bot runs; reading a long
|
|
42
|
-
* message; committing
|
|
43
|
-
* run. `//` is Claude's form for an absolute path.
|
|
42
|
+
* message; and committing. `//` is Claude's form for an absolute path.
|
|
44
43
|
*/
|
|
45
44
|
export function defaultRules(bots, cli = ownCli()) {
|
|
46
45
|
const kit = shellWord(cli);
|
|
@@ -50,19 +49,27 @@ export function defaultRules(bots, cli = ownCli()) {
|
|
|
50
49
|
`Read(/${bots}.messages/**)`,
|
|
51
50
|
'Bash(git add:*)',
|
|
52
51
|
'Bash(git commit:*)',
|
|
53
|
-
'Bash(orca orchestration check --run:*)',
|
|
54
52
|
];
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
|
|
56
|
+
* Orca's check, which the set held until #564: the kit's rules tell a bot to
|
|
57
|
+
* read its mail with `obk message check`, never with the bare check, which
|
|
58
|
+
* leaves it unread (ADR 0042).
|
|
59
|
+
*/
|
|
60
|
+
const ORCA_CHECK = 'Bash(orca orchestration check --run:*)';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The rules that were defaults once and are not now, that the bot's `allow`
|
|
64
|
+
* holds: those of the commands kept back (#548, #615) or gone from the kit (#555),
|
|
65
|
+
* spelled as `defaultRules` spelled them, and Orca's check (#564). The kit
|
|
66
|
+
* cannot tell its own old entry from a user's yes to the same rule, so it
|
|
67
|
+
* names them and leaves them (ADR 0051, ADR 0050).
|
|
62
68
|
*/
|
|
63
69
|
export function heldRules(bots, home, bot, cli = ownCli()) {
|
|
64
70
|
const allowed = allowOf(home, bot);
|
|
65
|
-
return NO_LONGER_DEFAULT.map((command) => `Bash(${shellWord(cli)} ${command} --bots ${shellWord(bots)}:*)`)
|
|
71
|
+
return [...NO_LONGER_DEFAULT, ...REMOVED].map((command) => `Bash(${shellWord(cli)} ${command} --bots ${shellWord(bots)}:*)`)
|
|
72
|
+
.concat(ORCA_CHECK)
|
|
66
73
|
.filter((rule) => allowed.includes(rule));
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -220,9 +227,16 @@ export const runsOnCodex = (bot) => bot.harness === 'codex'
|
|
|
220
227
|
*/
|
|
221
228
|
function missingDefaults(bots, home, bot) {
|
|
222
229
|
const allowed = allowOf(home, bot);
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
return botDefaults(bots, bot).filter((rule) => !allowed.includes(rule));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The default rules a bot on Claude or Codex is given, in the default order:
|
|
235
|
+
* what `bot create` writes into a new bot's bot.yaml (#595).
|
|
236
|
+
*/
|
|
237
|
+
export function botDefaults(bots, bot) {
|
|
238
|
+
if (!runsOnClaude(bot) && !runsOnCodex(bot)) return [];
|
|
239
|
+
return defaultRules(bots).filter((rule) => runsOnClaude(bot) || codexForm(rule).line !== undefined);
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
/**
|
|
@@ -270,7 +284,7 @@ export function refuseNoCodexForm(bot, rules) {
|
|
|
270
284
|
}
|
|
271
285
|
|
|
272
286
|
/**
|
|
273
|
-
* Add the default rules the bot is not allowed yet to its `allow` (ADR
|
|
287
|
+
* Add the default rules the bot is not allowed yet to its `allow` (ADR 0050),
|
|
274
288
|
* then write what it is allowed into the files of the harnesses it runs on:
|
|
275
289
|
* one entry per file, Claude's first, each with `defaults`, the rules this run
|
|
276
290
|
* added, and `held`, the rules of commands no longer in the set that the bot
|
package/src/pi.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Whether Pi is ready to start a session: it runs, it can use a model, and it
|
|
2
|
+
// can use the one the session names (#578).
|
|
3
|
+
//
|
|
4
|
+
// The kit never handles Pi's credentials: the user signs in to Pi on their own,
|
|
5
|
+
// and the kit only asks Pi what it can use. `pi auth check` is not asked, since
|
|
6
|
+
// it does not know a provider an extension of Pi's brings, such as Cursor's;
|
|
7
|
+
// `pi --list-models` lists the models Pi can use, those included (tech notes,
|
|
8
|
+
// section 6).
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
/** How long each question to Pi is given. Each took about 0.3 s on Pi 1.1.0. */
|
|
13
|
+
const ASK_MS = 30_000;
|
|
14
|
+
|
|
15
|
+
/** Pi's thinking levels, which a model name may carry after a colon. */
|
|
16
|
+
const THINKING = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Why Pi cannot start these sessions, or undefined when it can. `models` are
|
|
20
|
+
* the models they name; a session that names none takes Pi's own default.
|
|
21
|
+
*/
|
|
22
|
+
export function piTrouble(models) {
|
|
23
|
+
const version = ask(['--version']);
|
|
24
|
+
if (version.error !== undefined || version.status !== 0) {
|
|
25
|
+
const why = version.error?.code === 'ENOENT' ? 'there is no pi on PATH' : (version.error?.message ?? ((version.stderr ?? '').trim() || `it exited with ${version.status}`));
|
|
26
|
+
return `Pi does not run here (${why}), so no Pi session was started. Install Pi, then run this again.`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const any = ask(['--list-models']);
|
|
30
|
+
if (any.error !== undefined || any.status !== 0 || /^No models available\b/m.test(any.stdout ?? '')) {
|
|
31
|
+
return 'Pi has no model it can use: it is signed in to no provider. Sign in to Pi yourself (start pi and use /login, or set a provider\'s API key), then run this again. The kit never handles Pi\'s credentials.';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const model of new Set(models)) {
|
|
35
|
+
const found = ask(['--list-models', withoutThinking(model)]);
|
|
36
|
+
if (found.error !== undefined || found.status !== 0) {
|
|
37
|
+
const why = found.error?.message ?? ((found.stderr ?? '').trim() || `it exited with ${found.status ?? found.signal}`);
|
|
38
|
+
return `Pi cannot say whether it can use the model ${model}: pi --list-models ${withoutThinking(model)} failed (${why}), so no Pi session was started. Run that command yourself to see why, then run this again.`;
|
|
39
|
+
}
|
|
40
|
+
// Pi says it has no model at all before it searches, so the answer may be
|
|
41
|
+
// that one too, when a provider dropped out since the question above.
|
|
42
|
+
if (/^No models (matching|available)\b/m.test(found.stdout ?? '')) {
|
|
43
|
+
return `Pi cannot use the model ${model}: pi --list-models lists none matching it. Sign in to its provider in Pi yourself, or give the session a model Pi lists.`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The model without a trailing `:<thinking level>`, which `--list-models` does not match. */
|
|
50
|
+
function withoutThinking(model) {
|
|
51
|
+
const at = model.lastIndexOf(':');
|
|
52
|
+
return at > 0 && THINKING.includes(model.slice(at + 1)) ? model.slice(0, at) : model;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ask = (args) => spawnSync('pi', args, { encoding: 'utf8', timeout: ASK_MS });
|