pi-robot-hand 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/check.ts +16 -0
- package/src/hand-select.ts +6 -3
- package/src/index.ts +15 -3
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ Not sure what the command does? Press `e` in the selector and another model read
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
The first `e` asks which signed-in model to use and remembers it in `~/.pi/agent/robot-hand.json`; after a failed check, `e` asks again, and `E` always does.
|
|
47
|
+
The list shows every model of each provider pi holds a credential for, even an expired one; the provider you are using comes first, and a provider whose check failed is marked and moved to the end.
|
|
47
48
|
Known `secret_drop` values are replaced with `[REDACTED]` before anything is sent; when its registry cannot be read, the check fails and sends nothing. The check informs your choice and never blocks one. When you checked, the agent's result says which model checked and its verdict.
|
|
48
49
|
It needs the terminal UI.
|
|
49
50
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-robot-hand",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "The lazy kit for pi: no IDE, no copy-paste, safety net still on. The agent hands you commands (prompt, clipboard, or a Herdr pane that reports back), answers your ! commands, and takes secrets through a masked dialog it never sees",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/check.ts
CHANGED
|
@@ -63,6 +63,22 @@ export function modelRef(model: Pick<Model<Api>, "provider" | "id">): string {
|
|
|
63
63
|
return `${model.provider}/${model.id}`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The model list for the picker. pi counts a provider as available when it has a stored credential,
|
|
68
|
+
* even an expired one, so the provider pi is using now goes first and a provider whose check failed
|
|
69
|
+
* this session goes last; the rest keep pi's order.
|
|
70
|
+
*/
|
|
71
|
+
export function orderModels(refs: readonly string[], current: string | undefined, failed: ReadonlyMap<string, string>): string[] {
|
|
72
|
+
const provider = (ref: string) => ref.slice(0, ref.indexOf("/"));
|
|
73
|
+
const rank = (ref: string) => (failed.has(provider(ref)) ? 2 : provider(ref) === current ? 0 : 1);
|
|
74
|
+
return refs.map((ref, i) => ({ ref, i })).sort((a, b) => rank(a.ref) - rank(b.ref) || a.i - b.i).map(({ ref }) => ref);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** How the picker marks a provider whose check failed: sign-in trouble, or any other failure. */
|
|
78
|
+
export function failureNote(error: string): string {
|
|
79
|
+
return /oauth|auth|unauthori[sz]ed|forbidden|\b40[13]\b|api key|token|credential|login/i.test(error) ? "sign-in failed" : "check failed";
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
/**
|
|
67
83
|
* The saved check model, or undefined when none was picked yet or the file is unreadable. Read
|
|
68
84
|
* synchronously so the selector opens in the same tick as before the check existed.
|
package/src/hand-select.ts
CHANGED
|
@@ -12,8 +12,10 @@ export interface HandSelectProps {
|
|
|
12
12
|
/** The same title the plain selector shows: the reason, a blank line, then the command. */
|
|
13
13
|
title: string;
|
|
14
14
|
labels: readonly string[];
|
|
15
|
-
/** `provider/id` of every signed-in model. */
|
|
15
|
+
/** `provider/id` of every signed-in model, in the order to list them. */
|
|
16
16
|
models: () => string[];
|
|
17
|
+
/** A dim note after a model in the list, such as a failed sign-in. */
|
|
18
|
+
note?: (model: string) => string | undefined;
|
|
17
19
|
/** The saved check model. */
|
|
18
20
|
saved: () => string | undefined;
|
|
19
21
|
/** Remembers the model the user picked. */
|
|
@@ -198,8 +200,9 @@ export class HandSelect implements Component, Focusable {
|
|
|
198
200
|
const first = Math.max(0, Math.min(picker.cursor - Math.floor(MAX_LISTED / 2), list.length - MAX_LISTED));
|
|
199
201
|
list.slice(first, first + MAX_LISTED).forEach((model, i) => {
|
|
200
202
|
const at = first + i === picker.cursor;
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
+
const notes = [model === this.props.saved() ? "saved" : undefined, this.props.note?.(model)].filter(Boolean);
|
|
204
|
+
const suffix = notes.length > 0 ? t.fg("dim", ` (${notes.join(", ")})`) : "";
|
|
205
|
+
lines.push(`${at ? t.fg("accent", "› ") : " "}${t.fg(at ? "accent" : "text", model)}${suffix}`);
|
|
203
206
|
});
|
|
204
207
|
if (list.length > MAX_LISTED) lines.push(t.fg("dim", ` ${list.length} models; type to narrow`));
|
|
205
208
|
return lines;
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
11
11
|
import { copyToClipboard, type ExtensionAPI, type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Text } from "@earendil-works/pi-tui";
|
|
13
13
|
import { Type } from "typebox";
|
|
14
|
-
import { type CheckInput, type CheckResult, afterUserMessage, checkSummary, findScripts, lastUserText, loadCheckModel, loadSecrets, modelRef, redactInput, runCheck, saveCheckModel, settingsPath, type Verdict } from "./check.ts";
|
|
14
|
+
import { type CheckInput, type CheckResult, afterUserMessage, checkSummary, failureNote, findScripts, lastUserText, loadCheckModel, loadSecrets, modelRef, orderModels, redactInput, runCheck, saveCheckModel, settingsPath, type Verdict } from "./check.ts";
|
|
15
15
|
import { openDialog } from "./dialog.ts";
|
|
16
16
|
import { busChoices, type Choice, dialogTitle, type Offer, options, promptText, resultText, toOffer } from "./hand.ts";
|
|
17
17
|
import { HandSelect } from "./hand-select.ts";
|
|
@@ -108,7 +108,11 @@ export function createRobotHand(
|
|
|
108
108
|
}
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
+
// Providers whose check failed in this session, with the note the picker shows.
|
|
112
|
+
const failedProviders = new Map<string, string>();
|
|
113
|
+
|
|
111
114
|
pi.on("session_shutdown", () => {
|
|
115
|
+
failedProviders.clear();
|
|
112
116
|
if (timer) clearInterval(timer);
|
|
113
117
|
timer = undefined;
|
|
114
118
|
runs.clear();
|
|
@@ -134,7 +138,8 @@ export function createRobotHand(
|
|
|
134
138
|
{
|
|
135
139
|
title,
|
|
136
140
|
labels,
|
|
137
|
-
models: () => ctx.modelRegistry.getAvailable().map(modelRef),
|
|
141
|
+
models: () => orderModels(ctx.modelRegistry.getAvailable().map(modelRef), ctx.model?.provider, failedProviders),
|
|
142
|
+
note: (ref) => failedProviders.get(ref.slice(0, ref.indexOf("/"))),
|
|
138
143
|
saved: () => saved,
|
|
139
144
|
pick: (model) => {
|
|
140
145
|
saved = model;
|
|
@@ -148,7 +153,14 @@ export function createRobotHand(
|
|
|
148
153
|
input = undefined;
|
|
149
154
|
throw error;
|
|
150
155
|
});
|
|
151
|
-
|
|
156
|
+
try {
|
|
157
|
+
const result = await runCheck((m, context, options) => ctx.modelRegistry.complete(m, context, options), model, gathered, signal);
|
|
158
|
+
failedProviders.delete(model.provider);
|
|
159
|
+
return result;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!signal.aborted) failedProviders.set(model.provider, failureNote((error as Error).message));
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
152
164
|
},
|
|
153
165
|
},
|
|
154
166
|
theme,
|