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 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.1",
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.
@@ -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 saved = model === this.props.saved() ? t.fg("dim", " (saved)") : "";
202
- lines.push(`${at ? t.fg("accent", "› ") : " "}${t.fg(at ? "accent" : "text", model)}${saved}`);
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
- return runCheck((m, context, options) => ctx.modelRegistry.complete(m, context, options), model, gathered, signal);
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,