portable-agent-layer 0.89.1 → 0.91.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/package.json +3 -2
- package/src/cli/doctor/agents.ts +41 -7
- package/src/cli/doctor/environment.ts +16 -8
- package/src/cli/doctor/finding.ts +7 -2
- package/src/cli/doctor/inference.ts +5 -1
- package/src/cli/doctor/render.ts +231 -50
- package/src/cli/doctor/run.ts +33 -15
- package/src/cli/index.ts +46 -165
- package/src/cli/install-flow.ts +331 -0
- package/src/cli/reinstall.ts +5 -1
- package/src/cli/setup-attribution.ts +9 -9
- package/src/cli/setup-auto-update.ts +8 -7
- package/src/cli/setup-identity.ts +10 -15
- package/src/cli/ui/banner.ts +50 -0
- package/src/cli/ui/live.ts +112 -0
- package/src/cli/ui/rail.ts +119 -0
- package/src/cli/ui/style.ts +132 -0
- package/src/cli/ui/terminal.ts +81 -0
- package/src/cli/ui/wrap.ts +19 -0
- package/src/cli/update-flow.ts +98 -0
- package/src/cli/whats-new.ts +108 -0
- package/src/hooks/handlers/rating.ts +21 -10
- package/src/hooks/lib/inference.ts +1 -1
- package/src/hooks/lib/models.ts +23 -2
- package/src/hooks/lib/reaction-rating.ts +81 -1
- package/src/targets/lib.ts +49 -6
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { palPkg } from "../hooks/lib/paths";
|
|
4
|
+
|
|
5
|
+
export interface Release {
|
|
6
|
+
tag_name: string;
|
|
7
|
+
body?: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WhatsNew {
|
|
11
|
+
items: string[];
|
|
12
|
+
more: number;
|
|
13
|
+
url: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SHOWN = 3;
|
|
17
|
+
|
|
18
|
+
function parts(version: string): number[] {
|
|
19
|
+
return version.replace(/^v/, "").split(".").map(Number);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function newer(a: string, b: string): boolean {
|
|
23
|
+
const [x, y] = [parts(a), parts(b)];
|
|
24
|
+
for (let i = 0; i < 3; i++) {
|
|
25
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** "* **rating:** confirm corrections ([abc](url))" reads "Confirm corrections". */
|
|
31
|
+
export function noteText(bullet: string): string {
|
|
32
|
+
const text = bullet
|
|
33
|
+
.replace(/^\s*[*-]\s*/, "")
|
|
34
|
+
.replace(/^\*\*[^*]+:\*\*\s*/, "")
|
|
35
|
+
.replace(/\s*\(\[[^\]]*\]\([^)]*\)\)/g, "")
|
|
36
|
+
.trim();
|
|
37
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function section(body: string, heading: string): string[] {
|
|
41
|
+
const start = body.indexOf(`### ${heading}`);
|
|
42
|
+
if (start < 0) return [];
|
|
43
|
+
const rest = body.slice(start).split("\n").slice(1);
|
|
44
|
+
const end = rest.findIndex((line) => line.startsWith("#"));
|
|
45
|
+
return (end < 0 ? rest : rest.slice(0, end))
|
|
46
|
+
.filter((line) => /^\s*[*-]\s/.test(line))
|
|
47
|
+
.map(noteText);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function releaseNotes(body: string): string[] {
|
|
51
|
+
return [...section(body, "Features"), ...section(body, "Bug Fixes")];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function repoSlug(pkg = palPkg()): string | null {
|
|
55
|
+
try {
|
|
56
|
+
const manifest = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8")) as {
|
|
57
|
+
repository?: { url?: string };
|
|
58
|
+
};
|
|
59
|
+
return (
|
|
60
|
+
/github\.com\/([^/]+\/[^/.]+)/.exec(manifest.repository?.url ?? "")?.[1] ?? null
|
|
61
|
+
);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function summarizeReleases(
|
|
68
|
+
releases: Release[],
|
|
69
|
+
from: string,
|
|
70
|
+
to: string,
|
|
71
|
+
slug: string
|
|
72
|
+
): WhatsNew | null {
|
|
73
|
+
const items = releases
|
|
74
|
+
.filter((r) => newer(r.tag_name, from) && !newer(r.tag_name, to))
|
|
75
|
+
.flatMap((r) => releaseNotes(r.body ?? ""));
|
|
76
|
+
if (items.length === 0) return null;
|
|
77
|
+
return {
|
|
78
|
+
items: items.slice(0, SHOWN),
|
|
79
|
+
more: Math.max(0, items.length - SHOWN),
|
|
80
|
+
url: `github.com/${slug}/releases/tag/v${to}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function fetchReleases(slug: string): Promise<Release[]> {
|
|
85
|
+
const response = await fetch(
|
|
86
|
+
`https://api.github.com/repos/${slug}/releases?per_page=30`,
|
|
87
|
+
{
|
|
88
|
+
headers: { accept: "application/vnd.github+json" },
|
|
89
|
+
signal: AbortSignal.timeout(3000),
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
return response.ok ? ((await response.json()) as Release[]) : [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Null when the notes cannot be fetched in time: an update never waits on GitHub. */
|
|
96
|
+
export async function whatsNew(
|
|
97
|
+
from: string,
|
|
98
|
+
to: string,
|
|
99
|
+
fetchAll: (slug: string) => Promise<Release[]> = fetchReleases
|
|
100
|
+
): Promise<WhatsNew | null> {
|
|
101
|
+
const slug = repoSlug();
|
|
102
|
+
if (!slug) return null;
|
|
103
|
+
try {
|
|
104
|
+
return summarizeReleases(await fetchAll(slug), from, to, slug);
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -21,14 +21,18 @@ import { spawnDetachedInference } from "../lib/detached-inference";
|
|
|
21
21
|
import { canInfer, inference } from "../lib/inference";
|
|
22
22
|
import { replyEnd } from "../lib/interaction-samples";
|
|
23
23
|
import { logDebug } from "../lib/log";
|
|
24
|
+
import { HAIKU_5_5_MODEL } from "../lib/models";
|
|
24
25
|
import { paths } from "../lib/paths";
|
|
25
26
|
import { isSystemText, stripInjectedTags } from "../lib/prompt-text";
|
|
26
27
|
import {
|
|
27
|
-
|
|
28
|
+
correctionCheckRequest,
|
|
29
|
+
needsCorrectionCheck,
|
|
30
|
+
parseCorrectionCheck,
|
|
28
31
|
parseReactionLabel,
|
|
29
32
|
ratingContext,
|
|
30
33
|
ratingFromLabels,
|
|
31
34
|
reactionRequest,
|
|
35
|
+
settledLabel,
|
|
32
36
|
turnFromLabels,
|
|
33
37
|
} from "../lib/reaction-rating";
|
|
34
38
|
import {
|
|
@@ -225,6 +229,12 @@ async function labelReaction(reply: string, message: string, sessionId?: string)
|
|
|
225
229
|
return result.success ? parseReactionLabel(result.output) : null;
|
|
226
230
|
}
|
|
227
231
|
|
|
232
|
+
async function checkCorrection(reply: string, message: string, sessionId?: string) {
|
|
233
|
+
const result = await inference(correctionCheckRequest(reply, message, sessionId));
|
|
234
|
+
if (result.usage) logTokenUsage("rating", result.usage);
|
|
235
|
+
return result.success ? parseCorrectionCheck(result.output) : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
228
238
|
function knownRules() {
|
|
229
239
|
const waiting = readCandidates().filter((candidate) => candidate.verdict === "waiting");
|
|
230
240
|
return [
|
|
@@ -240,13 +250,13 @@ async function draftRuleCandidate(sessionId?: string): Promise<void> {
|
|
|
240
250
|
const known = knownRules();
|
|
241
251
|
if (!canRepeat(corrections) && !canWiden(corrections, known)) return;
|
|
242
252
|
const result = await inference(drafterRequest(corrections, known, sessionId));
|
|
243
|
-
if (result.usage) logTokenUsage("rule-drafter", result.usage);
|
|
253
|
+
if (result.usage) logTokenUsage("rule-drafter", result.usage, HAIKU_5_5_MODEL);
|
|
244
254
|
const candidate = result.success ? parseDraft(result.output, corrections, known) : null;
|
|
245
255
|
if (candidate) recordCandidate(candidate, turns);
|
|
246
256
|
else logDebug("rule-drafter", `no candidate: ${result.output ?? result.error ?? ""}`);
|
|
247
257
|
}
|
|
248
258
|
|
|
249
|
-
/** Background mode: label the reaction,
|
|
259
|
+
/** Background mode: label the reaction, check a possible correction, store the rating. */
|
|
250
260
|
async function runReactionRatingAndStore(
|
|
251
261
|
message: string,
|
|
252
262
|
reply: string,
|
|
@@ -255,17 +265,18 @@ async function runReactionRatingAndStore(
|
|
|
255
265
|
): Promise<void> {
|
|
256
266
|
try {
|
|
257
267
|
const first = await labelReaction(reply, message, sessionId);
|
|
258
|
-
const
|
|
259
|
-
? await
|
|
268
|
+
const check = needsCorrectionCheck(first)
|
|
269
|
+
? await checkCorrection(reply, message, sessionId)
|
|
260
270
|
: null;
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
271
|
+
const label = settledLabel(first, check);
|
|
272
|
+
const rating = ratingFromLabels(label, check);
|
|
273
|
+
if (label && rating !== null) {
|
|
274
|
+
handleRating(rating, ratingContext(label, message), "implicit", reply, message);
|
|
264
275
|
}
|
|
265
276
|
const turn = turnFromLabels(
|
|
266
277
|
{ session: sessionId ?? "", message, replyEnd: reply },
|
|
267
|
-
|
|
268
|
-
|
|
278
|
+
label,
|
|
279
|
+
check
|
|
269
280
|
);
|
|
270
281
|
if (turn) appendTurn(turn, sentAt);
|
|
271
282
|
if (turn?.confirmed) await draftRuleCandidate(sessionId);
|
|
@@ -74,7 +74,7 @@ interface RoutePreview {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function modelFor(route: FixedModelRoute, opts: InferenceOptions): string {
|
|
77
|
-
return inferenceModel(route, opts.tier);
|
|
77
|
+
return inferenceModel(route, opts.tier, opts.caller);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function smallModelOf(route: InferenceRoute): string | undefined {
|
package/src/hooks/lib/models.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { AgentType } from "./agent";
|
|
6
6
|
|
|
7
7
|
export const HAIKU_MODEL = "claude-haiku-4-5-20251001";
|
|
8
|
+
export const HAIKU_5_5_MODEL = "claude-haiku-5-5";
|
|
8
9
|
export const SONNET_MODEL = "claude-sonnet-5";
|
|
9
10
|
export const FABLE_MODEL = "claude-fable-5";
|
|
10
11
|
|
|
@@ -54,11 +55,23 @@ export function isFixedModelRoute(route: string): route is FixedModelRoute {
|
|
|
54
55
|
return Object.hasOwn(INFERENCE_MODELS, route);
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/** Callers whose prompt has been evaluated on Haiku 5.5; the rest stay on HAIKU_MODEL until theirs is. */
|
|
59
|
+
const ANTHROPIC_EVALUATED_ON_HAIKU_5_5 = new Set(["rule-drafter"]);
|
|
60
|
+
|
|
61
|
+
function evaluatedModel(models: RouteModels, caller?: string): string | undefined {
|
|
62
|
+
const onAnthropic = models === ANTHROPIC_MODELS;
|
|
63
|
+
return onAnthropic && caller && ANTHROPIC_EVALUATED_ON_HAIKU_5_5.has(caller)
|
|
64
|
+
? HAIKU_5_5_MODEL
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
export function inferenceModel(
|
|
58
69
|
route: FixedModelRoute,
|
|
59
|
-
tier: InferenceTier = "small"
|
|
70
|
+
tier: InferenceTier = "small",
|
|
71
|
+
caller?: string
|
|
60
72
|
): string {
|
|
61
|
-
|
|
73
|
+
const models = INFERENCE_MODELS[route];
|
|
74
|
+
return evaluatedModel(models, caller) ?? models[tier];
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
const AGENT_SPAWN_ROUTE: Partial<Record<AgentType, FixedModelRoute>> = {
|
|
@@ -102,6 +115,14 @@ export const MODEL_PRICING: Record<string, ModelPricing> = {
|
|
|
102
115
|
cacheWrite1h: 2,
|
|
103
116
|
cacheRead: 0.1,
|
|
104
117
|
},
|
|
118
|
+
// Rates for prompts up to 100k tokens; longer prompts cost 5x, and PAL's never get there.
|
|
119
|
+
[HAIKU_5_5_MODEL]: {
|
|
120
|
+
input: 0.1,
|
|
121
|
+
output: 0.5,
|
|
122
|
+
cacheWrite5m: 0.125,
|
|
123
|
+
cacheWrite1h: 0.2,
|
|
124
|
+
cacheRead: 0.01,
|
|
125
|
+
},
|
|
105
126
|
[FABLE_MODEL]: {
|
|
106
127
|
input: 10,
|
|
107
128
|
output: 50,
|
|
@@ -57,12 +57,47 @@ const REACTION_SCHEMA = {
|
|
|
57
57
|
additionalProperties: false,
|
|
58
58
|
} as const;
|
|
59
59
|
|
|
60
|
+
const CORRECTION_CHECK_PROMPT = `An AI assistant sent a reply and the user answered. A first reader labelled the answer a correction of the reply. Check whether that is right.
|
|
61
|
+
|
|
62
|
+
It is a correction only when the user says or shows that the reply itself was wrong:
|
|
63
|
+
- something the reply claimed is false;
|
|
64
|
+
- something it said was done is not done;
|
|
65
|
+
- a command or step it gave does not work;
|
|
66
|
+
- a promise it made was not kept;
|
|
67
|
+
- following its advice or steps caused a problem;
|
|
68
|
+
- it missed what the user asked, or is waiting on or asking for something the user had already done;
|
|
69
|
+
- the user still does not understand after its explanation, even when they phrase it as a question.
|
|
70
|
+
|
|
71
|
+
It is not a correction when the user:
|
|
72
|
+
- wants more, or adds a requirement the reply was not asked to meet. "This needs more" or "it should also do X" is a new requirement unless the reply claimed X was already done;
|
|
73
|
+
- cannot find where or how to do a step the reply described;
|
|
74
|
+
- doubts the reply or asks for a check, without saying it is wrong;
|
|
75
|
+
- reports something that failed later, when the reply never claimed it would work;
|
|
76
|
+
- complains about something other than what the reply claimed;
|
|
77
|
+
- decides to change or drop a feature.`;
|
|
78
|
+
|
|
79
|
+
const CORRECTION_CHECK_ANSWER = `Answer corrected if it is a correction, otherwise not-corrected. For corrected, set issue to what the reply got wrong, in at most twelve words; otherwise set issue to an empty string.`;
|
|
80
|
+
|
|
81
|
+
const CORRECTION_CHECK_SCHEMA = {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
reaction: { enum: ["corrected", "not-corrected"] },
|
|
85
|
+
issue: { type: "string" },
|
|
86
|
+
},
|
|
87
|
+
required: ["reaction", "issue"],
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
} as const;
|
|
90
|
+
|
|
60
91
|
const MESSAGE_MAX = 800;
|
|
61
92
|
|
|
93
|
+
function reactionPrompt(replyEnd: string, message: string): string {
|
|
94
|
+
return `The end of the assistant's reply:\n<reply_end>\n${replyEnd}\n</reply_end>\n\nThe user's next message:\n<message>\n${message.slice(0, MESSAGE_MAX)}\n</message>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
62
97
|
export function reactionRequest(replyEnd: string, message: string, sessionId?: string) {
|
|
63
98
|
return {
|
|
64
99
|
system: REACTION_SYSTEM_PROMPT,
|
|
65
|
-
user:
|
|
100
|
+
user: reactionPrompt(replyEnd, message),
|
|
66
101
|
maxTokens: 200,
|
|
67
102
|
timeout: 90_000,
|
|
68
103
|
jsonSchema: REACTION_SCHEMA,
|
|
@@ -71,6 +106,51 @@ export function reactionRequest(replyEnd: string, message: string, sessionId?: s
|
|
|
71
106
|
};
|
|
72
107
|
}
|
|
73
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Asked again with the labelling prompt, a model repeats its own mistake; a
|
|
111
|
+
* narrower question catches the follow-ups it misreads as corrections, and the
|
|
112
|
+
* corrections it misreads as follow-ups. The answer instruction follows the
|
|
113
|
+
* message, the order eval/correction-check measures.
|
|
114
|
+
*/
|
|
115
|
+
export function correctionCheckRequest(
|
|
116
|
+
replyEnd: string,
|
|
117
|
+
message: string,
|
|
118
|
+
sessionId?: string
|
|
119
|
+
) {
|
|
120
|
+
return {
|
|
121
|
+
...reactionRequest(replyEnd, message, sessionId),
|
|
122
|
+
system: CORRECTION_CHECK_PROMPT,
|
|
123
|
+
user: `${reactionPrompt(replyEnd, message)}\n\n${CORRECTION_CHECK_ANSWER}`,
|
|
124
|
+
jsonSchema: CORRECTION_CHECK_SCHEMA,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function parseCorrectionCheck(
|
|
129
|
+
output: string | null | undefined
|
|
130
|
+
): ReactionLabel | null {
|
|
131
|
+
if (!output) return null;
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(output) as { reaction?: unknown; issue?: unknown };
|
|
134
|
+
if (parsed.reaction !== "corrected") return { reaction: "follow-up", issue: "" };
|
|
135
|
+
const issue = typeof parsed.issue === "string" ? parsed.issue.trim() : "";
|
|
136
|
+
return { reaction: "corrected", issue };
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function needsCorrectionCheck(first: ReactionLabel | null): boolean {
|
|
143
|
+
return isCorrectionLabel(first) || first?.reaction === "follow-up";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function settledLabel(
|
|
147
|
+
first: ReactionLabel | null,
|
|
148
|
+
check: ReactionLabel | null
|
|
149
|
+
): ReactionLabel | null {
|
|
150
|
+
const caughtFollowUp = first?.reaction === "follow-up" && isCorrectionLabel(check);
|
|
151
|
+
return caughtFollowUp ? check : first;
|
|
152
|
+
}
|
|
153
|
+
|
|
74
154
|
function isModelReaction(value: unknown): value is ModelReaction {
|
|
75
155
|
return MODEL_REACTIONS.includes(value as ModelReaction);
|
|
76
156
|
}
|
package/src/targets/lib.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
21
|
+
import { createStyle, PALETTE, type Rgb } from "../cli/ui/style";
|
|
21
22
|
import {
|
|
22
23
|
AGENT_NAMES,
|
|
23
24
|
AGENT_REGISTRY,
|
|
@@ -45,12 +46,47 @@ function narrate(line: string): void {
|
|
|
45
46
|
if (narrating) console.log(line);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
export type LogLevel = "info" | "success" | "warn" | "error" | "detail";
|
|
50
|
+
type LogSink = (level: LogLevel, message: string) => void;
|
|
51
|
+
|
|
52
|
+
let sink: LogSink | null = null;
|
|
53
|
+
|
|
54
|
+
/** Hands every log line to `collect` while `work` runs, so a caller can place it under its own step. */
|
|
55
|
+
export async function capturingLog<T>(
|
|
56
|
+
collect: LogSink,
|
|
57
|
+
work: () => Promise<T>
|
|
58
|
+
): Promise<T> {
|
|
59
|
+
const previous = sink;
|
|
60
|
+
sink = collect;
|
|
61
|
+
try {
|
|
62
|
+
return await work();
|
|
63
|
+
} finally {
|
|
64
|
+
sink = previous;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const LEVEL_COLOR: Record<LogLevel, Rgb> = {
|
|
69
|
+
info: PALETTE.indigo,
|
|
70
|
+
success: PALETTE.green,
|
|
71
|
+
warn: PALETTE.amber,
|
|
72
|
+
error: PALETTE.rose,
|
|
73
|
+
detail: PALETTE.indigo,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function tagged(level: LogLevel, message: string): string {
|
|
77
|
+
return `${createStyle().paint(LEVEL_COLOR[level], "[pal]")} ${message}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function emit(level: LogLevel, message: string, write: (line: string) => void): void {
|
|
81
|
+
if (sink) sink(level, message);
|
|
82
|
+
else write(tagged(level, message));
|
|
83
|
+
}
|
|
84
|
+
|
|
48
85
|
export const log = {
|
|
49
|
-
info: (msg: string) =>
|
|
50
|
-
success: (msg: string) =>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
error: (msg: string) => console.error(`\x1b[31m[pal]\x1b[0m ${msg}`),
|
|
86
|
+
info: (msg: string) => emit("info", msg, narrate),
|
|
87
|
+
success: (msg: string) => emit("success", msg, narrate),
|
|
88
|
+
warn: (msg: string) => emit("warn", msg, (line) => console.log(line)),
|
|
89
|
+
error: (msg: string) => emit("error", msg, (line) => console.error(line)),
|
|
54
90
|
|
|
55
91
|
/**
|
|
56
92
|
* Per-item narration from inside a loop, where the caller already reports the
|
|
@@ -61,7 +97,7 @@ export const log = {
|
|
|
61
97
|
*/
|
|
62
98
|
detail: (msg: string) => {
|
|
63
99
|
if (runningUnderTest()) return;
|
|
64
|
-
|
|
100
|
+
emit("detail", msg, narrate);
|
|
65
101
|
},
|
|
66
102
|
};
|
|
67
103
|
|
|
@@ -1152,6 +1188,13 @@ function shippedAgentNames(): Set<string> {
|
|
|
1152
1188
|
);
|
|
1153
1189
|
}
|
|
1154
1190
|
|
|
1191
|
+
export function installedShippedAgentCount(agent: AgentName): number {
|
|
1192
|
+
const dir = nativeAgentsDir(agent);
|
|
1193
|
+
return [...shippedAgentNames()].filter((name) =>
|
|
1194
|
+
existsSync(resolve(dir, agentFileName(name, agent)))
|
|
1195
|
+
).length;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1155
1198
|
/** List the user-authored subagents in ~/.pal/agents/. */
|
|
1156
1199
|
export function listPersonalSubagents(): string[] {
|
|
1157
1200
|
if (!existsSync(palAgentsStore())) return [];
|