@turri-dev/cli 0.1.28-next.d5f2680 → 0.2.1-next.47b578c
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 +3 -2
- package/dist/chats.d.ts +31 -4
- package/dist/chats.js +146 -52
- package/dist/cli.js +4 -2
- package/dist/config.d.ts +4 -1
- package/dist/config.js +39 -4
- package/dist/environment.d.ts +0 -2
- package/dist/environment.js +0 -6
- package/dist/extension.d.ts +4 -3
- package/dist/extension.js +110 -23
- package/dist/host-session.d.ts +14 -0
- package/dist/host-session.js +2 -0
- package/dist/host.d.ts +6 -1
- package/dist/host.js +5 -1
- package/dist/init-config.d.ts +1 -1
- package/dist/init-config.js +33 -3
- package/dist/installed-providers.d.ts +40 -0
- package/dist/installed-providers.js +98 -0
- package/dist/model-check.d.ts +4 -2
- package/dist/model-check.js +33 -17
- package/dist/notice.d.ts +30 -6
- package/dist/pi.js +8 -24
- package/dist/plain/files.d.ts +16 -0
- package/dist/plain/files.js +68 -5
- package/dist/plain/graph.js +2 -2
- package/dist/plain/module-concepts.js +3 -2
- package/dist/plain/parser.d.ts +8 -3
- package/dist/plain/parser.js +47 -6
- package/dist/plain/stdlib.js +3 -3
- package/dist/regenerate/base-folder.d.ts +2 -0
- package/dist/regenerate/base-folder.js +24 -6
- package/dist/regenerate/command-check.d.ts +12 -1
- package/dist/regenerate/command-check.js +74 -0
- package/dist/regenerate/confine.d.ts +5 -0
- package/dist/regenerate/confine.js +9 -9
- package/dist/regenerate/ledger.d.ts +7 -0
- package/dist/regenerate/lineage.js +43 -2
- package/dist/regenerate/loop.d.ts +8 -0
- package/dist/regenerate/loop.js +11 -4
- package/dist/regenerate/os-sandbox.d.ts +2 -0
- package/dist/regenerate/os-sandbox.js +55 -20
- package/dist/regenerate/release.d.ts +2 -0
- package/dist/regenerate/requires.d.ts +12 -0
- package/dist/regenerate/requires.js +70 -1
- package/dist/regenerate/spec-steps.js +18 -1
- package/dist/regenerate/standing.d.ts +28 -0
- package/dist/regenerate/standing.js +30 -0
- package/dist/regenerate/testing.d.ts +2 -0
- package/dist/regenerate/testing.js +14 -0
- package/dist/release-agent.d.ts +17 -0
- package/dist/release-agent.js +19 -7
- package/dist/release-tools.d.ts +6 -0
- package/dist/release-tools.js +24 -5
- package/dist/sentinel-chat.d.ts +1 -1
- package/dist/sentinel-chat.js +21 -8
- package/dist/sentinel-check.d.ts +36 -0
- package/dist/sentinel-check.js +78 -0
- package/dist/sentinel.d.ts +40 -9
- package/dist/sentinel.js +240 -37
- package/dist/setup.d.ts +24 -3
- package/dist/setup.js +117 -31
- package/dist/spawn-async.d.ts +18 -0
- package/dist/spawn-async.js +35 -0
- package/dist/stack.d.ts +14 -0
- package/dist/stack.js +44 -2
- package/dist/turri-versions.d.ts +71 -0
- package/dist/turri-versions.js +275 -0
- package/docs/README.md +2 -0
- package/docs/claude-subscription.md +131 -0
- package/docs/config.md +5 -0
- package/docs/deploy.md +2 -2
- package/docs/hosting.md +42 -4
- package/docs/installation.md +39 -2
- package/docs/parser.md +1 -1
- package/docs/regenerating.md +1 -1
- package/docs/sandbox.md +2 -1
- package/docs/sentinel.md +11 -3
- package/docs/using.md +4 -2
- package/genome/rules/turri-regeneration.md +1 -1
- package/package.json +1 -1
package/dist/extension.js
CHANGED
|
@@ -17,6 +17,7 @@ import { sandboxAvailable, sandboxSummary } from "./regenerate/os-sandbox.js";
|
|
|
17
17
|
import { findTestScripts, scriptKindOfSkill } from "./regenerate/test-scripts.js";
|
|
18
18
|
import { writingScript } from "./regenerate/scripts-being-written.js";
|
|
19
19
|
import { applyBedrockRegion } from "./bedrock-region.js";
|
|
20
|
+
import { bannerWarnings } from "./turri-versions.js";
|
|
20
21
|
import { conformanceBlock, conformanceTestsOnIn, CONFORMANCE_TOOLS, TURN_ON_TOOL, turnOnConformanceTests, turnOnConformanceTestsTool } from "./conformance-switch.js";
|
|
21
22
|
import { BACKGROUND_NOTE, doneLine, failedTask, ownChanges, runDetails, skillRunDelivery, startedText } from "./background-skills.js";
|
|
22
23
|
import { createAskUserTool } from "./tools.js";
|
|
@@ -26,7 +27,8 @@ import { describeGates, interruptedRun, runSkill } from "./genome.js";
|
|
|
26
27
|
import { unqueue } from "./chat-waiting.js";
|
|
27
28
|
import { skillStatus } from "./skill.js";
|
|
28
29
|
import { runningDeployed, stopDeployed } from "./regenerate/release.js";
|
|
29
|
-
import { createDeployState, createDeployTools } from "./release-tools.js";
|
|
30
|
+
import { createDeployState, createDeployTools, currentRelease } from "./release-tools.js";
|
|
31
|
+
import { checkedSpecs } from "./sentinel-check.js";
|
|
30
32
|
import { runReleaseAgent } from "./release-agent.js";
|
|
31
33
|
import { createSteeringStore, EVERY_MODULE, moduleNameOf, queueSectionOf, steeringFor } from "./regenerate/steer.js";
|
|
32
34
|
import { askedForCommandLine, refusal, selfInvocation, TOOLS_NOT_CLI } from "./self-invocation.js";
|
|
@@ -61,6 +63,8 @@ import { doctorTool } from "./doctor-tool.js";
|
|
|
61
63
|
import { ledgerTool } from "./ledger-tools.js";
|
|
62
64
|
import { describeSpecConflict } from "./regenerate/stop-report.js";
|
|
63
65
|
import { inheritProviders, inheritThinkingLevel } from "./pi.js";
|
|
66
|
+
import { LOADING_PROVIDERS } from "./installed-providers.js";
|
|
67
|
+
import { machineChange, machineChangeReason, writesPlain } from "./regenerate/command-check.js";
|
|
64
68
|
import { beginCapture, captureConsole, describeCaptured, dropNoise } from "./quiet.js";
|
|
65
69
|
import { loadRules, renderRulesBlock } from "./rules.js";
|
|
66
70
|
import { registerTurriConfig, ruleNames } from "./config-command.js";
|
|
@@ -74,7 +78,7 @@ import { supersededByTurri } from "./chat-skills.js";
|
|
|
74
78
|
import { conversationIdentity } from "./identity.js";
|
|
75
79
|
import { setSurface, surfaceOf } from "./surface.js";
|
|
76
80
|
import { createChats, scopedToChat } from "./chats.js";
|
|
77
|
-
import { ignoredGuidance, keepGuidance, landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief, sentinelPrompt, sentinelSetting, sentinelStartNotice, specConflictNotice } from "./sentinel.js";
|
|
81
|
+
import { ignoredGuidance, keepGuidance, landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief, sentinelCheckBrief, sentinelPrompt, sentinelSetting, sentinelStartNotice, specConflictNotice } from "./sentinel.js";
|
|
78
82
|
import { noticeDialogs, registerSentinel } from "./sentinel-chat.js";
|
|
79
83
|
import { currentChat } from "./chat-scope.js";
|
|
80
84
|
import { registerChats } from "./chat-commands.js";
|
|
@@ -404,7 +408,9 @@ export function processSession(make = () => createTurriCore()) {
|
|
|
404
408
|
const global = globalThis;
|
|
405
409
|
return (global[SESSION] ??= make());
|
|
406
410
|
}
|
|
407
|
-
|
|
411
|
+
// Loaded as nothing while the person's pi extensions are loaded only for their providers (installed-providers.ts): an older
|
|
412
|
+
// turri installed into every pi is among them, and would start a session of its own.
|
|
413
|
+
const extension = (pi) => (process.env[LOADING_PROVIDERS] === "1" ? undefined : processSession().extension(pi));
|
|
408
414
|
export default extension;
|
|
409
415
|
/**
|
|
410
416
|
* The session's work loop, for a host to read: made with the first chat's context - the models it plans with - when no
|
|
@@ -431,8 +437,10 @@ function turriExtension(api, session, link) {
|
|
|
431
437
|
const isSentinel = link.role === SENTINEL;
|
|
432
438
|
const hostSentinelTools = isSentinel ? (host.sentinel?.tools ?? []) : [];
|
|
433
439
|
const sentinelAllowed = new Set([...SENTINEL_TOOLS, ...hostSentinelTools.map((tool) => tool.name)]);
|
|
440
|
+
// Its resume: the module brought up again on the session's work loop, as regenerate brings it up.
|
|
441
|
+
const resumeModule = async (plainFile) => describeModules(await sessionLoop(session).bringUp(resolve(cwd, plainFile)));
|
|
434
442
|
if (isSentinel)
|
|
435
|
-
registerSentinel(pi, session, cwd, hostSentinelTools, sentinelAllowed);
|
|
443
|
+
registerSentinel(pi, session, cwd, hostSentinelTools, sentinelAllowed, resumeModule);
|
|
436
444
|
const dialogs = link.dialogs ?? host.dialogs;
|
|
437
445
|
// turri's own skills, and the project's - .turri/skills, .agents/skills (skills.ts).
|
|
438
446
|
const discoveredSkills = discoverSkills(PACKAGE_ROOT, cwd);
|
|
@@ -491,6 +499,39 @@ function turriExtension(api, session, link) {
|
|
|
491
499
|
});
|
|
492
500
|
void pi.sendMessage({ customType: "turri_sentinel_pulse", content, display: false, details: { jobId: job.id, status: job.status } }, { triggerTurn: true });
|
|
493
501
|
};
|
|
502
|
+
// The sentinel's: functional specs a host asked it to check. The module is released first - as release-after-pulse
|
|
503
|
+
// releases it - unless what runs is its own codebase's last commit; then told, in a conversation of its own for the
|
|
504
|
+
// check, its hidden first message `turri_sentinel_check`. Why not, when the release failed: its agent is told nothing.
|
|
505
|
+
const briefCheck = async (check, context) => {
|
|
506
|
+
const state = deployStates.get(jobs) ?? createDeployState();
|
|
507
|
+
let releasedNow = false;
|
|
508
|
+
if (currentRelease(state, check.plainFile, check.module) === undefined) {
|
|
509
|
+
// Behind, never made, or what it started died: released afresh - the release tool would say it shipped that already.
|
|
510
|
+
state.released.delete(check.module);
|
|
511
|
+
let said;
|
|
512
|
+
try {
|
|
513
|
+
const ctx = contextNow();
|
|
514
|
+
const model = ctx !== undefined ? releaseModel(ctx, check.plainFile) : undefined;
|
|
515
|
+
said = await runReleaseAgent({ plainFile: check.plainFile, cwd, state, inModule: check.module, forCheck: { moduleName: check.module }, ...(model !== undefined ? { model } : {}) });
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
return error instanceof Error ? error.message : String(error);
|
|
519
|
+
}
|
|
520
|
+
if (currentRelease(state, check.plainFile, check.module) === undefined)
|
|
521
|
+
return said.trim() || "its last commit was not released";
|
|
522
|
+
releasedNow = true;
|
|
523
|
+
}
|
|
524
|
+
const commit = state.released.get(check.module)?.commit;
|
|
525
|
+
const content = sentinelCheckBrief(check, {
|
|
526
|
+
specs: checkedSpecs(check),
|
|
527
|
+
released: runningDeployed(check.module),
|
|
528
|
+
releasedNow,
|
|
529
|
+
...(commit !== undefined ? { commit } : {}),
|
|
530
|
+
...(context.focused !== undefined ? { focused: context.focused } : {}),
|
|
531
|
+
});
|
|
532
|
+
void pi.sendMessage({ customType: "turri_sentinel_check", content, display: false, details: { checkId: check.id, module: check.module, funcIds: check.funcIds } }, { triggerTurn: true });
|
|
533
|
+
return undefined;
|
|
534
|
+
};
|
|
494
535
|
// A pulse ended, with the sentinel on: said in the chat. One that landed is told to its agent too, to tell the person
|
|
495
536
|
// it landed and what the sentinel does with it - and nothing more: the pulse is the sentinel's. ***plain that
|
|
496
537
|
// contradicts itself is put to the person at once, by turri itself, as the chat would have been started on it.
|
|
@@ -500,7 +541,9 @@ function turriExtension(api, session, link) {
|
|
|
500
541
|
session.notice(specConflictNotice(job, specConflictTask(job.specConflict)));
|
|
501
542
|
if (options.tell !== true)
|
|
502
543
|
return;
|
|
503
|
-
|
|
544
|
+
// A host that shows the person the pulse's end itself: the chat knows of it - in order, with no turn of its own.
|
|
545
|
+
const shown = host.pulseEndsShown === true;
|
|
546
|
+
void pi.sendMessage({ customType: "turri_job_landed", content: landedNote(job, runningDeployed(job.module), { shown }), display: false, details: { jobId: job.id, status: job.status } }, shown ? { triggerTurn: false } : { triggerTurn: true, deliverAs: "followUp" });
|
|
504
547
|
};
|
|
505
548
|
const notices = noticeDialogs(pi, session, link, contextNow);
|
|
506
549
|
// The test advisor's verdicts, once the chat is idle: a yes starts a turn that writes its scenarios, a no is a line.
|
|
@@ -509,6 +552,7 @@ function turriExtension(api, session, link) {
|
|
|
509
552
|
announce,
|
|
510
553
|
landed,
|
|
511
554
|
brief,
|
|
555
|
+
briefCheck,
|
|
512
556
|
notice: notices.put,
|
|
513
557
|
noticeOver: notices.over,
|
|
514
558
|
concluded: notices.concluded,
|
|
@@ -545,10 +589,9 @@ function turriExtension(api, session, link) {
|
|
|
545
589
|
// pi's skills that turri has already - its own and the project's, run with cultivate, and any about ***plain from
|
|
546
590
|
// another tool - are left out of the list the conversation is offered (chat-skills.ts); the person is told once.
|
|
547
591
|
const turriRuns = new Set(discoveredSkills.skills.map((skill) => skill.name));
|
|
548
|
-
// What the modules need of this machine: worked out
|
|
549
|
-
// while a message waits to be sent.
|
|
592
|
+
// What the modules need of this machine: worked out when a turn first asks, and after a turn when a spec changed - never
|
|
593
|
+
// as the session starts, which a host would wait on, and never while a message waits to be sent once it is known.
|
|
550
594
|
const machine = machineNeeds(cwd);
|
|
551
|
-
pi.on("session_start", () => machine.refreshSoon());
|
|
552
595
|
pi.on("agent_settled", () => machine.refreshSoon());
|
|
553
596
|
pi.on("before_agent_start", (event, ctx) => {
|
|
554
597
|
const offered = event.systemPromptOptions.skills;
|
|
@@ -598,9 +641,20 @@ function turriExtension(api, session, link) {
|
|
|
598
641
|
pi.on("tool_call", (event) => {
|
|
599
642
|
// The sentinel reads, and puts what it finds to the person: whatever else it calls is refused.
|
|
600
643
|
if (isSentinel && !sentinelAllowed.has(event.toolName)) {
|
|
601
|
-
return { block: true, reason:
|
|
644
|
+
return { block: true, reason: `${event.toolName} is not one of the sentinel's tools: it fixes the project's testing scripts and their config, and nothing else. Put what needs doing to the person with notify_person.` };
|
|
602
645
|
}
|
|
603
646
|
const command = event.input.command;
|
|
647
|
+
// turri changes nothing of the machine's - no install, no system setting - asked or not, through any tool that runs a
|
|
648
|
+
// command: its own bash, and a host's - a terminal in its window. The person does that.
|
|
649
|
+
if (typeof command === "string") {
|
|
650
|
+
const changes = machineChange(command);
|
|
651
|
+
if (changes !== undefined)
|
|
652
|
+
return { block: true, reason: machineChangeReason(changes) };
|
|
653
|
+
// The sentinel's write and edit reach its tooling alone: a host's command does not write ***plain for it.
|
|
654
|
+
const plain = isSentinel ? writesPlain(command) : undefined;
|
|
655
|
+
if (plain !== undefined)
|
|
656
|
+
return { block: true, reason: `${plain} is ***plain: the sentinel never changes it - put what it needs to the person with notify_person.` };
|
|
657
|
+
}
|
|
604
658
|
if (event.toolName === "bash" && typeof command === "string") {
|
|
605
659
|
const what = selfInvocation(command);
|
|
606
660
|
if (what !== undefined && !askedForCommandLine(lastUserMessage))
|
|
@@ -1183,7 +1237,7 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
1183
1237
|
// Said at once, and kept in the chat: what follows takes a while - the planner is an agent - and the person sees it
|
|
1184
1238
|
// began, not a chat that stays empty until the first pulse starts. The plan is said under it once it is made.
|
|
1185
1239
|
say(scratch
|
|
1186
|
-
? `Deleting ${module}'s code${chain ? " and its whole chain's" : ""}
|
|
1240
|
+
? `Deleting ${module}'s code${chain ? " and its whole chain's" : ""} and then starting regeneration…`
|
|
1187
1241
|
: again.length > 0
|
|
1188
1242
|
? `Planning ${again.join(", ")} again…`
|
|
1189
1243
|
: `Planning ${module}'s pulses…`, "info");
|
|
@@ -1444,8 +1498,9 @@ export function machineBlock(cwd, modules = stackOnMachine(cwd)) {
|
|
|
1444
1498
|
}
|
|
1445
1499
|
/**
|
|
1446
1500
|
* What the project's modules need of this machine, kept between turns - working it out parses every spec, seconds on a
|
|
1447
|
-
* project of long requires chains
|
|
1448
|
-
*
|
|
1501
|
+
* project of long requires chains. Worked out when the first turn asks - not as the session starts: a host's thread
|
|
1502
|
+
* would be held up opening a chat no one may write in - and again, once known, when a turn has settled and a spec
|
|
1503
|
+
* changed since: a turn is told what was known last, at most a turn behind.
|
|
1449
1504
|
*/
|
|
1450
1505
|
export function machineNeeds(cwd, work = stackOnMachine) {
|
|
1451
1506
|
let known;
|
|
@@ -1475,8 +1530,10 @@ export function machineNeeds(cwd, work = stackOnMachine) {
|
|
|
1475
1530
|
refresh();
|
|
1476
1531
|
return known.modules;
|
|
1477
1532
|
},
|
|
1478
|
-
/** Worked out again, if a spec changed - once the chat has drawn what it is drawing. */
|
|
1533
|
+
/** Worked out again, if it is known and a spec changed - once the chat has drawn what it is drawing. */
|
|
1479
1534
|
refreshSoon() {
|
|
1535
|
+
if (known === undefined)
|
|
1536
|
+
return;
|
|
1480
1537
|
setTimeout(() => {
|
|
1481
1538
|
try {
|
|
1482
1539
|
refresh();
|
|
@@ -1598,6 +1655,10 @@ function conversationModel(ctx, override) {
|
|
|
1598
1655
|
* The chat's model, read now - it can change while a skill runs - or, from a context that has
|
|
1599
1656
|
* gone stale since, the model it had when it was last readable.
|
|
1600
1657
|
*/
|
|
1658
|
+
/** The model a release runs on - a pulse's last stage, and a check's: the one that regenerated the module. A release follows steps, and a small model thinking little does that well. */
|
|
1659
|
+
function releaseModel(ctx, plainFile) {
|
|
1660
|
+
return modelFor(chosenModels, "spec_changes", projectModel(ctx, plainFile) ?? chatModelOf(ctx));
|
|
1661
|
+
}
|
|
1601
1662
|
function chatModelOf(ctx) {
|
|
1602
1663
|
try {
|
|
1603
1664
|
const now = conversationModel(ctx);
|
|
@@ -1833,7 +1894,7 @@ function workLoopFor(jobs, cwd, ctx) {
|
|
|
1833
1894
|
// Under release-after-pulse, a pulse releases what it landed itself, before the next starts - on the model that
|
|
1834
1895
|
// regenerated it: a release follows steps, and a small model thinking little does that well.
|
|
1835
1896
|
release: ({ plainFile, moduleName, title, onDelta }) => {
|
|
1836
|
-
const model =
|
|
1897
|
+
const model = releaseModel(from.ctx, plainFile);
|
|
1837
1898
|
return runReleaseAgent({
|
|
1838
1899
|
plainFile,
|
|
1839
1900
|
cwd,
|
|
@@ -2173,15 +2234,15 @@ export const JOB_FINISHED_REACTION = [
|
|
|
2173
2234
|
" `toolchains:` does not - never add it there, and never resume before the fix. Do what the report says if it is the",
|
|
2174
2235
|
" project's to change - a tool told to use `$HOME` in the build's own files - say so, and resume; otherwise tell the",
|
|
2175
2236
|
" person, in a line or two, what the report says and that \"resume\" picks it up once it is done.",
|
|
2176
|
-
"- the machine lacks a toolchain or a tool - a JDK, Maven, Gradle, Go, uv, Node, a system library:
|
|
2177
|
-
"
|
|
2178
|
-
"
|
|
2179
|
-
"
|
|
2180
|
-
"
|
|
2181
|
-
" the
|
|
2182
|
-
"
|
|
2183
|
-
"
|
|
2184
|
-
"
|
|
2237
|
+
"- the machine lacks a toolchain or a tool - a JDK, Maven, Gradle, Go, uv, Node, a system library: you install nothing,",
|
|
2238
|
+
" and change no setting of the machine's or the person's shell - never, even when asked: your shell refuses it. First",
|
|
2239
|
+
" find out, in your shell, whether it is there after all: off the PATH (`command -v`, Homebrew's opt folders,",
|
|
2240
|
+
" /Library/Java/JavaVirtualMachines, a version manager's folder - mise, asdf, sdkman, nvm), or under another name - python",
|
|
2241
|
+
" where the testing script runs python3, of the version the ***plain names. When it is there, the testing script calling",
|
|
2242
|
+
" it the way this machine has it is the fix: change the script, say so in a line, and resume. When it is not, tell the",
|
|
2243
|
+
" person exactly what to run themselves - `brew install openjdk@21`, `mise use -g java@21` - and the line for their",
|
|
2244
|
+
" shell profile when the PATH needs it, and that \"resume\" picks it up once it is in. A tool the person installs where the",
|
|
2245
|
+
" PATH turri was started with does not reach needs turri restarted: say so.",
|
|
2185
2246
|
"- a credential `agent-env:` names that is not there: never make one up - tell the person where it is read from.",
|
|
2186
2247
|
].join("\n");
|
|
2187
2248
|
const sidePanels = new WeakMap();
|
|
@@ -2688,6 +2749,8 @@ function registerDeploy(pi, cwd, jobs, state) {
|
|
|
2688
2749
|
});
|
|
2689
2750
|
}
|
|
2690
2751
|
const MILESTONE_ENTRY = "turri_milestone";
|
|
2752
|
+
/** A newer turri out, found once turri had started (turri-versions.ts). */
|
|
2753
|
+
const VERSION_ENTRY = "turri_version";
|
|
2691
2754
|
const JOB_ENTRY = "turri_job";
|
|
2692
2755
|
const JOBS_LIST_ENTRY = "turri_jobs";
|
|
2693
2756
|
const HISTORY_ENTRY = "turri_history";
|
|
@@ -2901,6 +2964,8 @@ function registerBranding(pi, skills) {
|
|
|
2901
2964
|
const lines = [
|
|
2902
2965
|
`${brandPink(theme)("turri")} ${theme.fg("muted", `v${data.version}`)}` +
|
|
2903
2966
|
(data.commit !== undefined ? theme.fg("dim", ` from a clone, ${data.commit}${data.changed ? " with changes" : ""}`) : ""),
|
|
2967
|
+
// A newer turri out, or one installed and hidden behind this one: said first, under the version it is about.
|
|
2968
|
+
...(data.versions ?? []).map((line) => theme.fg("warning", line)),
|
|
2904
2969
|
`${theme.fg("text", "the first coding agent with the power of ")}${brandPink(theme)("regenerative software")}`,
|
|
2905
2970
|
// Where the settings are changed, without leaving the chat.
|
|
2906
2971
|
theme.fg("muted", "configure with /turri-config"),
|
|
@@ -2912,6 +2977,15 @@ function registerBranding(pi, skills) {
|
|
|
2912
2977
|
// where the terminal shows images, drawn in text where it does not.
|
|
2913
2978
|
return new Banner(lines, (rule) => theme.fg("borderMuted", rule), jellyfishArt(theme.appearance, (theme.getColorMode?.() ?? "truecolor") === "truecolor"));
|
|
2914
2979
|
});
|
|
2980
|
+
// A newer turri, found once turri had started: a line, as the banner would have said it.
|
|
2981
|
+
pi.registerEntryRenderer(VERSION_ENTRY, (entry, _options, theme) => {
|
|
2982
|
+
const data = entry.data;
|
|
2983
|
+
if (data === undefined)
|
|
2984
|
+
return undefined;
|
|
2985
|
+
const box = new Box(1, 0);
|
|
2986
|
+
box.addChild(new Text(theme.fg("warning", data.text), 0, 0));
|
|
2987
|
+
return box;
|
|
2988
|
+
});
|
|
2915
2989
|
let bannered = false;
|
|
2916
2990
|
pi.on("session_start", (_event, ctx) => {
|
|
2917
2991
|
if (ctx.hasUI) {
|
|
@@ -2924,12 +2998,25 @@ function registerBranding(pi, skills) {
|
|
|
2924
2998
|
bannered = true;
|
|
2925
2999
|
try {
|
|
2926
3000
|
const identity = turriIdentity();
|
|
3001
|
+
const versions = bannerWarnings();
|
|
3002
|
+
// A newer turri npm says is out only now - the banner read what it said last time: said under the banner once it does.
|
|
3003
|
+
void versions.later.then((line) => {
|
|
3004
|
+
if (line === undefined)
|
|
3005
|
+
return;
|
|
3006
|
+
try {
|
|
3007
|
+
pi.appendEntry(VERSION_ENTRY, { text: line });
|
|
3008
|
+
}
|
|
3009
|
+
catch {
|
|
3010
|
+
/* the chat let go meanwhile: the next start's banner says it */
|
|
3011
|
+
}
|
|
3012
|
+
});
|
|
2927
3013
|
pi.appendEntry(BANNER_ENTRY, {
|
|
2928
3014
|
version: identity.version ?? "0.0.0",
|
|
2929
3015
|
commit: identity.git_commit?.slice(0, 7),
|
|
2930
3016
|
changed: identity.git_dirty === true,
|
|
2931
3017
|
pending: skills.pending.length,
|
|
2932
3018
|
sandbox: sandboxSummary(),
|
|
3019
|
+
versions: versions.lines,
|
|
2933
3020
|
});
|
|
2934
3021
|
}
|
|
2935
3022
|
catch {
|
package/dist/host-session.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { type EvaluateOutcome } from "./advisor-chat.ts";
|
|
|
5
5
|
import type { JobRegistry } from "./regenerate/jobs.ts";
|
|
6
6
|
import type { WorkLoop } from "./regenerate/loop.ts";
|
|
7
7
|
import type { Notice, NoticeAnswer } from "./notice.ts";
|
|
8
|
+
import { type CheckRequest } from "./sentinel-check.ts";
|
|
8
9
|
/**
|
|
9
10
|
* turri's session for a host that runs its chats itself - a desktop app, an editor: the chats, each a pi session with
|
|
10
11
|
* turri in it, on one job registry and work loop. The host shows each chat in its own way - subscribes to its session,
|
|
@@ -67,6 +68,19 @@ export interface Turri {
|
|
|
67
68
|
* hears each as it is put (`onNotice`) and as it ends (`onNoticeOver`).
|
|
68
69
|
*/
|
|
69
70
|
notices(): Notice[];
|
|
71
|
+
/**
|
|
72
|
+
* The sentinel asked to check functional specs in a module's release - `funcIds` each `module:func-id`, of the module or
|
|
73
|
+
* of any module its requires chain builds on (a bare func-id is the module's own) - in the module's release, as it tests a release after a pulse: the module's own codebase's last
|
|
74
|
+
* commit, which holds every module its chain builds on, released first unless that is what runs. Queued behind the
|
|
75
|
+
* sentinel's work, in a conversation of its own. Returns the check's id - its notices and its conclusion carry it - or
|
|
76
|
+
* why it was refused: the sentinel off or not started this session, a module not there or that does not parse, a
|
|
77
|
+
* module outside its chain, a func-id a module does not have, one not built in its codebase as its ***plain has it now.
|
|
78
|
+
*/
|
|
79
|
+
check(request: CheckRequest): {
|
|
80
|
+
check: string;
|
|
81
|
+
} | {
|
|
82
|
+
refused: string;
|
|
83
|
+
};
|
|
70
84
|
/**
|
|
71
85
|
* The test advisor asked to evaluate functional specs, as the chat's `evaluate_tests` does - for a host's own way in,
|
|
72
86
|
* a button on a functional spec: `module:func-id`s, or func-ids of `plainFile`'s module, even a text judged already and
|
package/dist/host-session.js
CHANGED
|
@@ -6,6 +6,7 @@ import { reachable, ruleNames, settingCandidates } from "./config-command.js";
|
|
|
6
6
|
import { settingOf } from "./config-edit.js";
|
|
7
7
|
import { createTurriCore, sessionAdvisor, sessionLoop, turnOnConformance } from "./extension.js";
|
|
8
8
|
import { stopDeployed } from "./regenerate/release.js";
|
|
9
|
+
import { prepareCheck } from "./sentinel-check.js";
|
|
9
10
|
/** turri's session in a host, in the process's working folder - the project's. */
|
|
10
11
|
export async function createTurri(options = {}) {
|
|
11
12
|
const { dialogs, uiContext, extensions = [], agentDir = getAgentDir(), sessionDir, restore, ...host } = options;
|
|
@@ -71,6 +72,7 @@ export async function createTurri(options = {}) {
|
|
|
71
72
|
},
|
|
72
73
|
decline: async (id) => core.decline(id),
|
|
73
74
|
notices: () => core.notices(),
|
|
75
|
+
check: (request) => core.check(() => prepareCheck(cwd, request)),
|
|
74
76
|
evaluateTests: (funcIds, options) => evaluateTests(cwd, sessionAdvisor(core), funcIds, options),
|
|
75
77
|
turnOnConformanceTests: (plainFile) => turnOnConformance(core, plainFile),
|
|
76
78
|
settingChoices: (key, typed = "") => {
|
package/dist/host.d.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { createTurri, type Turri, type TurriOptions } from "./host-session.ts";
|
|
8
8
|
export { MAX_CHATS, type ChatInfo, type ChatStatus, type TurriHost } from "./chats.ts";
|
|
9
|
-
export { SENTINEL, SENTINEL_START, type Conclusion, type Notice, type NoticeAction, type NoticeAnswer, type NoticeSeverity, type NoticeState } from "./notice.ts";
|
|
9
|
+
export { SENTINEL, SENTINEL_START, type Conclusion, type Notice, type NoticeAction, type NoticeAnswer, type NoticeSeverity, type NoticeState, type SentinelCheck } from "./notice.ts";
|
|
10
|
+
export type { CheckRequest } from "./sentinel-check.ts";
|
|
10
11
|
export type { AskUserIO } from "./ask.ts";
|
|
11
12
|
/** pi itself, as turri is built against it: a host's own extensions and UI are made with this one, not another copy. */
|
|
12
13
|
export * as pi from "@earendil-works/pi-coding-agent";
|
|
@@ -16,6 +17,8 @@ export { configSettings, currentValue, EFFECT_TEXT, overriddenBy, readConfigText
|
|
|
16
17
|
export { bedrockRegionFor, type BedrockRegion, type BedrockRegionSource } from "./bedrock-region.ts";
|
|
17
18
|
export { BEDROCK_DEFAULT, BEDROCK_GEOGRAPHIES, BEDROCK_IN_REGION, BEDROCK_REGIONS, bedrockAwsRegion, bedrockDefaultModels, bedrockMakers, bedrockModels, bedrockModelsIn, bedrockNeedsAwsRegion, bedrockPlaceOf, initConfig, makerLabel, VENDORS, type BedrockChoice, type BedrockMaker, type BedrockOffer, type InitConfigOptions, type ModelChoice, type ModelSet, type Vendor, } from "./init-config.ts";
|
|
18
19
|
export { piModelCatalogue, type CatalogueModel, type ModelCatalogue } from "./model-check.ts";
|
|
20
|
+
export { EXTENSION_PROVIDERS, installedModelRuntime } from "./installed-providers.ts";
|
|
21
|
+
export { machineChange, machineChangeReason } from "./regenerate/command-check.ts";
|
|
19
22
|
export { doctor, type Check, type DoctorOptions, type Level } from "./setup.ts";
|
|
20
23
|
export { checkPlainFile, type CheckResult } from "./plain/check.ts";
|
|
21
24
|
export { plainFileParser } from "./plain/parser.ts";
|
|
@@ -26,6 +29,8 @@ export { conceptBeingTyped, findConcepts } from "./plain/concepts.ts";
|
|
|
26
29
|
export { plainGraph, type PlainConceptNode, type PlainGraph, type PlainItemNode, type PlainLink, type PlainModuleNode, type PlainScenario, type PlainSection, type PlainTestPlainFileNode } from "./plain/graph.ts";
|
|
27
30
|
export { isStack, STDLIB_KINDS, stdlibTemplates, type StdlibKind, type StdlibTemplate } from "./stdlib.ts";
|
|
28
31
|
export { createWorkLoop, describeModules, functionalityCompletion, promptOf, type DriftReport, type ModuleStatus, type WorkLoop } from "./regenerate/loop.ts";
|
|
32
|
+
export { projectStanding, type ModuleStanding, type StandingOptions } from "./regenerate/standing.ts";
|
|
33
|
+
export { requiresOrder } from "./regenerate/requires.ts";
|
|
29
34
|
export { createJobRegistry, defaultParallelJobs, type Job, type JobRegistry, type PulseCases, type PulseSpec } from "./regenerate/jobs.ts";
|
|
30
35
|
export { describeTurnIncident, onModelEvent, type TranscriptEntry } from "./regenerate/activity.ts";
|
|
31
36
|
export { sweepOrphanedGroups } from "./regenerate/shell.ts";
|
package/dist/host.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// The session and its chats.
|
|
8
8
|
export { createTurri } from "./host-session.js";
|
|
9
9
|
export { MAX_CHATS } from "./chats.js";
|
|
10
|
-
// The sentinel's notices: what it puts to the person, and how they answer.
|
|
10
|
+
// The sentinel's notices: what it puts to the person, and how they answer - and the checks a host asks of it.
|
|
11
11
|
export { SENTINEL, SENTINEL_START } from "./notice.js";
|
|
12
12
|
/** pi itself, as turri is built against it: a host's own extensions and UI are made with this one, not another copy. */
|
|
13
13
|
export * as pi from "@earendil-works/pi-coding-agent";
|
|
@@ -24,6 +24,8 @@ export { bedrockRegionFor } from "./bedrock-region.js";
|
|
|
24
24
|
export { BEDROCK_DEFAULT, BEDROCK_GEOGRAPHIES, BEDROCK_IN_REGION, BEDROCK_REGIONS, bedrockAwsRegion, bedrockDefaultModels, bedrockMakers, bedrockModels, bedrockModelsIn, bedrockNeedsAwsRegion, bedrockPlaceOf, initConfig, makerLabel, VENDORS, } from "./init-config.js";
|
|
25
25
|
// pi's model catalogue, read without a request: what initConfig checks the models given against, and what a host offers.
|
|
26
26
|
export { piModelCatalogue } from "./model-check.js";
|
|
27
|
+
export { EXTENSION_PROVIDERS, installedModelRuntime } from "./installed-providers.js";
|
|
28
|
+
export { machineChange, machineChangeReason } from "./regenerate/command-check.js";
|
|
27
29
|
// Whether this machine, and a project on it, is ready for turri - as `turri doctor` checks it, printing nothing.
|
|
28
30
|
export { doctor } from "./setup.js";
|
|
29
31
|
// A spec: parsed, checked, coloured.
|
|
@@ -41,6 +43,8 @@ export { plainGraph } from "./plain/graph.js";
|
|
|
41
43
|
export { isStack, STDLIB_KINDS, stdlibTemplates } from "./stdlib.js";
|
|
42
44
|
// The work: what is built, what is left, and the jobs that build it.
|
|
43
45
|
export { createWorkLoop, describeModules, functionalityCompletion, promptOf } from "./regenerate/loop.js";
|
|
46
|
+
export { projectStanding } from "./regenerate/standing.js";
|
|
47
|
+
export { requiresOrder } from "./regenerate/requires.js";
|
|
44
48
|
export { createJobRegistry, defaultParallelJobs } from "./regenerate/jobs.js";
|
|
45
49
|
export { describeTurnIncident, onModelEvent } from "./regenerate/activity.js";
|
|
46
50
|
export { sweepOrphanedGroups } from "./regenerate/shell.js";
|
package/dist/init-config.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { type CatalogueModel, type ModelCatalogue } from "./model-check.ts";
|
|
|
16
16
|
* new one takes from a config.yaml beside it the keys turri reads too.
|
|
17
17
|
*/
|
|
18
18
|
/** A vendor whose models the quick start config is filled in with. */
|
|
19
|
-
export type Vendor = "anthropic" | "openai" | "google" | "amazon-bedrock" | "other";
|
|
19
|
+
export type Vendor = "anthropic" | "openai" | "google" | "amazon-bedrock" | "claude-code" | "other";
|
|
20
20
|
/** Whose models Amazon Bedrock serves: the maker a model's id names after its geography - anthropic, openai, xai, meta... */
|
|
21
21
|
export type BedrockMaker = string;
|
|
22
22
|
/**
|
package/dist/init-config.js
CHANGED
|
@@ -7,6 +7,7 @@ import { adoptLegacyConfig, CONFIG_FILE, CONFIG_KEYS, LEGACY_CONFIG_FILE, ROLE_T
|
|
|
7
7
|
import { withTopLevelValue } from "./config-edit.js";
|
|
8
8
|
import { brand } from "./brand.js";
|
|
9
9
|
import { closestModels, piModelCatalogue } from "./model-check.js";
|
|
10
|
+
import { EXTENSION_PROVIDERS } from "./installed-providers.js";
|
|
10
11
|
/** The geographies turri's own pair for each maker is served in - the first the default. Every other: bedrockMakers. */
|
|
11
12
|
export const BEDROCK_REGIONS = {
|
|
12
13
|
anthropic: ["global", "us", "eu", "jp", "au"],
|
|
@@ -152,6 +153,14 @@ export const VENDORS = {
|
|
|
152
153
|
...bedrockModels({ maker: "anthropic", region: BEDROCK_DEFAULT.region }),
|
|
153
154
|
keys: [{ vendor: "AWS", variable: "AWS_PROFILE" }],
|
|
154
155
|
},
|
|
156
|
+
// A Claude Pro or Max subscription, through Claude Code: its pi extension's provider (installed-providers.ts), no key.
|
|
157
|
+
"claude-code": {
|
|
158
|
+
label: "Claude subscription",
|
|
159
|
+
harness: "claude-code/claude-opus-5-5",
|
|
160
|
+
coding: "claude-code/claude-sonnet-5",
|
|
161
|
+
strong: "claude-code/claude-opus-5-5",
|
|
162
|
+
keys: [],
|
|
163
|
+
},
|
|
155
164
|
// A mix - the strongest to talk to, a cheap one to code - and the starting point for any other provider pi supports.
|
|
156
165
|
other: {
|
|
157
166
|
label: "Other",
|
|
@@ -370,6 +379,8 @@ export function vendorOf(answer) {
|
|
|
370
379
|
const byNumber = /^\d+$/.test(typed) ? names[Number(typed) - 1] : undefined;
|
|
371
380
|
if (typed === "bedrock" || typed === "aws")
|
|
372
381
|
return "amazon-bedrock";
|
|
382
|
+
if (typed === "claude" || typed === "subscription" || typed === "claude code")
|
|
383
|
+
return "claude-code";
|
|
373
384
|
return byNumber ?? names.find((name) => name === typed || VENDORS[name].label.toLowerCase() === typed);
|
|
374
385
|
}
|
|
375
386
|
/** The makers offered, as numbered: pi's catalogue's, or without it turri's two. */
|
|
@@ -404,6 +415,17 @@ function shellProfile() {
|
|
|
404
415
|
function signInText(vendor, bedrock, models) {
|
|
405
416
|
// Models given on another vendor's terms - other's, a provider of their own: the keys of the providers they name.
|
|
406
417
|
const providers = models !== undefined && vendor === "other" ? [...new Set([models.harness, models.coding].map((model) => model.split("/")[0]))] : [];
|
|
418
|
+
// A provider a pi extension brings - a Claude subscription through Claude Code: no key, its extension and its own sign-in.
|
|
419
|
+
const extensions = (vendor === "other" ? providers : [vendor]).flatMap((provider) => (EXTENSION_PROVIDERS[provider] !== undefined ? [EXTENSION_PROVIDERS[provider]] : []));
|
|
420
|
+
if (extensions.length > 0 && (vendor !== "other" || extensions.length === providers.length)) {
|
|
421
|
+
return extensions.flatMap((extension) => [
|
|
422
|
+
brand.bold(`Set up ${extension.label}`),
|
|
423
|
+
` ${brand.accent(extension.install)} ${brand.muted("its pi extension: turri finds what pi has installed")}`,
|
|
424
|
+
` ${brand.muted(extension.signIn)}`,
|
|
425
|
+
"",
|
|
426
|
+
brand.muted("turri doctor --models checks it answers."),
|
|
427
|
+
]);
|
|
428
|
+
}
|
|
407
429
|
const keys = providers.length > 0
|
|
408
430
|
? providers.flatMap((provider) => (provider in VENDORS && provider !== "other" && provider !== "amazon-bedrock" ? VENDORS[provider].keys : [{ vendor: makerLabel(provider), variable: "" }]))
|
|
409
431
|
: VENDORS[vendor].keys;
|
|
@@ -451,6 +473,8 @@ function whose(vendor, models, given) {
|
|
|
451
473
|
const makers = [...new Set(places.map((place) => `${makerLabel(place.maker)}'s`))];
|
|
452
474
|
return `${and(makers)} models on Amazon Bedrock (${[...new Set(places.map((place) => place.geography))].join(", ")})`;
|
|
453
475
|
}
|
|
476
|
+
if (vendor === "claude-code")
|
|
477
|
+
return "your Claude subscription's models, through Claude Code";
|
|
454
478
|
if (vendor !== "other")
|
|
455
479
|
return `${VENDORS[vendor].label}'s models`;
|
|
456
480
|
if (!given)
|
|
@@ -470,8 +494,12 @@ function notInCatalogue(spec, catalogue) {
|
|
|
470
494
|
if (model === undefined) {
|
|
471
495
|
if (THINKING.test(id) && known.some((entry) => entry.id === id.replace(THINKING, "")))
|
|
472
496
|
return `${spec} names a thinking level: give the model alone, ${spec.replace(THINKING, "")} - turri writes how hard each thinks.`;
|
|
473
|
-
if (known.length === 0)
|
|
474
|
-
|
|
497
|
+
if (known.length === 0) {
|
|
498
|
+
const extension = EXTENSION_PROVIDERS[provider];
|
|
499
|
+
return extension !== undefined
|
|
500
|
+
? `${spec} is not in pi's model catalogue: ${provider} - ${extension.label} - comes with a pi extension that is not installed: ${extension.install}`
|
|
501
|
+
: `${spec} is not in pi's model catalogue: pi knows no provider ${provider}.`;
|
|
502
|
+
}
|
|
475
503
|
const close = closestModels(id, known.filter((entry) => entry.type === "chat").map((entry) => entry.id));
|
|
476
504
|
return `${spec} is not in pi's model catalogue${close.length > 0 ? ` - did you mean ${close.map((other) => `${provider}/${other}`).join(", ")}?` : "."}`;
|
|
477
505
|
}
|
|
@@ -662,7 +690,9 @@ async function askVendor(given, catalogue) {
|
|
|
662
690
|
const { label, harness, coding } = VENDORS[name];
|
|
663
691
|
const what = name === "amazon-bedrock"
|
|
664
692
|
? "Anthropic's, OpenAI's, xAI's, Meta's... models on AWS"
|
|
665
|
-
:
|
|
693
|
+
: name === "claude-code"
|
|
694
|
+
? `your Claude Pro or Max, through Claude Code - ${harness.split("/")[1]} to talk to, ${coding.split("/")[1]} to code`
|
|
695
|
+
: `${name === "other" ? "a mix - " : ""}${harness.split("/")[1]} to talk to, ${coding.split("/")[1]} to code`;
|
|
666
696
|
return [label, what];
|
|
667
697
|
}));
|
|
668
698
|
vendor = await askUntil(rl, choose(names.length), vendorOf, `Type a number from 1 to ${names.length}, or a vendor's name.`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* The model providers the person's pi extensions register - a Claude subscription through Claude Code
|
|
4
|
+
* (pi-claude-code, `claude-code`), a company's own gateway - wherever turri needs a model and no conversation has them.
|
|
5
|
+
*
|
|
6
|
+
* In a chat, pi loads the person's installed pi packages itself, and turri's agents take the providers from the
|
|
7
|
+
* conversation's model registry (pi.ts `inheritProviders`). A command with no conversation - `turri regenerate`,
|
|
8
|
+
* `turri change-plain`, doctor's model check, init-config - has none: here they are loaded the way pi loads them, once a
|
|
9
|
+
* process, and only the providers they register are kept. turri's own extension, when an older turri installed it into
|
|
10
|
+
* every pi, is loaded as nothing meanwhile (`LOADING_PROVIDERS`): it would start a session of its own.
|
|
11
|
+
*/
|
|
12
|
+
/** Set while the person's extensions are loaded for their providers: turri's own extension does nothing then. */
|
|
13
|
+
export declare const LOADING_PROVIDERS = "TURRI_LOADING_PROVIDERS";
|
|
14
|
+
/**
|
|
15
|
+
* pi's own providers that come from an extension rather than with pi: what installs each, and how it is signed in to - for
|
|
16
|
+
* doctor and init-config to say when a config names one that is not there.
|
|
17
|
+
*/
|
|
18
|
+
export declare const EXTENSION_PROVIDERS: Record<string, {
|
|
19
|
+
label: string;
|
|
20
|
+
install: string;
|
|
21
|
+
signIn: string;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* The providers the person's installed pi extensions register, for `cwd` - its project's pi settings with the person's
|
|
25
|
+
* own - loaded once. Undefined when none could be loaded: pi's built-in providers and models.json are what there is.
|
|
26
|
+
*/
|
|
27
|
+
export declare function installedProviders(cwd?: string): Promise<ModelRegistry | undefined>;
|
|
28
|
+
/**
|
|
29
|
+
* pi's model runtime as turri's agents have it with no conversation: pi's providers, models.json and auth.json, and the
|
|
30
|
+
* providers the person's installed pi extensions register - for a host that signs in or lists models before a chat is
|
|
31
|
+
* open, whose own `ModelRuntime.create()` would have none of an extension's (`claude-code`, say).
|
|
32
|
+
*/
|
|
33
|
+
export declare function installedModelRuntime(cwd?: string): Promise<ModelRuntime>;
|
|
34
|
+
/** For tests: forget what was loaded. */
|
|
35
|
+
export declare function forgetInstalledProviders(): void;
|
|
36
|
+
/**
|
|
37
|
+
* pi's model runtime - its catalogue, models.json and its credentials - with `source`'s providers registered in it: a
|
|
38
|
+
* conversation's, or the person's installed ones. Its models are listed as the registrations land.
|
|
39
|
+
*/
|
|
40
|
+
export declare function runtimeWithProviders(source: ModelRegistry | undefined): Promise<ModelRuntime>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createAgentSessionServices, getAgentDir, ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
/**
|
|
4
|
+
* The model providers the person's pi extensions register - a Claude subscription through Claude Code
|
|
5
|
+
* (pi-claude-code, `claude-code`), a company's own gateway - wherever turri needs a model and no conversation has them.
|
|
6
|
+
*
|
|
7
|
+
* In a chat, pi loads the person's installed pi packages itself, and turri's agents take the providers from the
|
|
8
|
+
* conversation's model registry (pi.ts `inheritProviders`). A command with no conversation - `turri regenerate`,
|
|
9
|
+
* `turri change-plain`, doctor's model check, init-config - has none: here they are loaded the way pi loads them, once a
|
|
10
|
+
* process, and only the providers they register are kept. turri's own extension, when an older turri installed it into
|
|
11
|
+
* every pi, is loaded as nothing meanwhile (`LOADING_PROVIDERS`): it would start a session of its own.
|
|
12
|
+
*/
|
|
13
|
+
/** Set while the person's extensions are loaded for their providers: turri's own extension does nothing then. */
|
|
14
|
+
export const LOADING_PROVIDERS = "TURRI_LOADING_PROVIDERS";
|
|
15
|
+
/**
|
|
16
|
+
* pi's own providers that come from an extension rather than with pi: what installs each, and how it is signed in to - for
|
|
17
|
+
* doctor and init-config to say when a config names one that is not there.
|
|
18
|
+
*/
|
|
19
|
+
export const EXTENSION_PROVIDERS = {
|
|
20
|
+
"claude-code": {
|
|
21
|
+
label: "Claude subscription (Claude Code)",
|
|
22
|
+
install: "pi install https://github.com/earendil-works/pi-claude-code",
|
|
23
|
+
signIn: "Claude Code installed (npm install -g @anthropic-ai/claude-code) and signed in to your Claude Pro or Max subscription: claude auth login",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const loaded = new Map();
|
|
27
|
+
/**
|
|
28
|
+
* The providers the person's installed pi extensions register, for `cwd` - its project's pi settings with the person's
|
|
29
|
+
* own - loaded once. Undefined when none could be loaded: pi's built-in providers and models.json are what there is.
|
|
30
|
+
*/
|
|
31
|
+
export function installedProviders(cwd = process.cwd()) {
|
|
32
|
+
let found = loaded.get(cwd);
|
|
33
|
+
if (found === undefined) {
|
|
34
|
+
found = load(cwd);
|
|
35
|
+
loaded.set(cwd, found);
|
|
36
|
+
}
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
async function load(cwd) {
|
|
40
|
+
const before = process.env[LOADING_PROVIDERS];
|
|
41
|
+
process.env[LOADING_PROVIDERS] = "1";
|
|
42
|
+
try {
|
|
43
|
+
// Only the extensions: no skills, prompts, themes or context files are read for this.
|
|
44
|
+
const { modelRuntime } = await createAgentSessionServices({
|
|
45
|
+
cwd,
|
|
46
|
+
resourceLoaderOptions: { noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true },
|
|
47
|
+
});
|
|
48
|
+
const registry = new ModelRegistry(modelRuntime);
|
|
49
|
+
return registry.getRegisteredProviderIds().length > 0 ? registry : undefined;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
if (before === undefined)
|
|
56
|
+
delete process.env[LOADING_PROVIDERS];
|
|
57
|
+
else
|
|
58
|
+
process.env[LOADING_PROVIDERS] = before;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* pi's model runtime as turri's agents have it with no conversation: pi's providers, models.json and auth.json, and the
|
|
63
|
+
* providers the person's installed pi extensions register - for a host that signs in or lists models before a chat is
|
|
64
|
+
* open, whose own `ModelRuntime.create()` would have none of an extension's (`claude-code`, say).
|
|
65
|
+
*/
|
|
66
|
+
export async function installedModelRuntime(cwd = process.cwd()) {
|
|
67
|
+
return runtimeWithProviders(await installedProviders(cwd));
|
|
68
|
+
}
|
|
69
|
+
/** For tests: forget what was loaded. */
|
|
70
|
+
export function forgetInstalledProviders() {
|
|
71
|
+
loaded.clear();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* pi's model runtime - its catalogue, models.json and its credentials - with `source`'s providers registered in it: a
|
|
75
|
+
* conversation's, or the person's installed ones. Its models are listed as the registrations land.
|
|
76
|
+
*/
|
|
77
|
+
export async function runtimeWithProviders(source) {
|
|
78
|
+
const agentDir = getAgentDir();
|
|
79
|
+
const runtime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
|
|
80
|
+
if (source === undefined)
|
|
81
|
+
return runtime;
|
|
82
|
+
const registry = new ModelRegistry(runtime);
|
|
83
|
+
for (const id of source.getRegisteredProviderIds()) {
|
|
84
|
+
const native = source.getRegisteredNativeProvider(id);
|
|
85
|
+
const config = source.getRegisteredProviderConfig(id);
|
|
86
|
+
try {
|
|
87
|
+
if (native !== undefined)
|
|
88
|
+
registry.registerProvider(native);
|
|
89
|
+
else if (config !== undefined)
|
|
90
|
+
registry.registerProvider(id, config);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// One provider that will not register leaves the others, and pi's own, usable.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
await runtime.refresh({ allowNetwork: false });
|
|
97
|
+
return runtime;
|
|
98
|
+
}
|