ai-foreman 1.7.2 → 1.7.4
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/dist/adapters/claude.d.ts +3 -1
- package/dist/adapters/claude.js +29 -5
- package/dist/adapters/types.d.ts +17 -0
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +34 -6
- package/dist/foreman.d.ts +3 -1
- package/dist/foreman.js +14 -1
- package/dist/providerQuestions.d.ts +14 -0
- package/dist/providerQuestions.js +192 -0
- package/package.json +1 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import type { PermissionResult } from "@anthropic-ai/claude-agent-sdk";
|
|
1
2
|
/** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
|
|
2
3
|
export declare function requireClaudeSDK(): Promise<typeof import("@anthropic-ai/claude-agent-sdk")>;
|
|
3
|
-
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
|
|
4
|
+
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, PermissionDecision, ProviderSettingSwitch, TurnResult } from "./types.js";
|
|
4
5
|
/**
|
|
5
6
|
* Pure function: build the `options` object passed to `query()`.
|
|
6
7
|
* Extracted so tests can assert on the shape without making a live SDK call.
|
|
7
8
|
* `canUseTool` is omitted here — it's a closure that the constructor adds.
|
|
8
9
|
*/
|
|
9
10
|
export declare function buildClaudeQueryOptions(opts: Omit<BuilderAdapterOptions, "permission">): Record<string, unknown>;
|
|
11
|
+
export declare function permissionDecisionToClaudeResult(decision: PermissionDecision, toolUseID?: string): PermissionResult;
|
|
10
12
|
/**
|
|
11
13
|
* Drives Claude Code through the Claude Agent SDK in streaming-input mode:
|
|
12
14
|
* one persistent session, follow-up turns pushed as user messages, permission
|
package/dist/adapters/claude.js
CHANGED
|
@@ -37,6 +37,22 @@ export function buildClaudeQueryOptions(opts) {
|
|
|
37
37
|
}
|
|
38
38
|
return base;
|
|
39
39
|
}
|
|
40
|
+
export function permissionDecisionToClaudeResult(decision, toolUseID) {
|
|
41
|
+
if (decision.behavior === "allow") {
|
|
42
|
+
return {
|
|
43
|
+
behavior: "allow",
|
|
44
|
+
updatedInput: decision.updatedInput,
|
|
45
|
+
updatedPermissions: decision.updatedPermissions,
|
|
46
|
+
toolUseID,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
behavior: "deny",
|
|
51
|
+
message: decision.message,
|
|
52
|
+
interrupt: decision.interrupt,
|
|
53
|
+
toolUseID,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
40
56
|
/**
|
|
41
57
|
* Drives Claude Code through the Claude Agent SDK in streaming-input mode:
|
|
42
58
|
* one persistent session, follow-up turns pushed as user messages, permission
|
|
@@ -81,11 +97,19 @@ export class ClaudeAdapter {
|
|
|
81
97
|
...buildClaudeQueryOptions(opts),
|
|
82
98
|
abortController: this.abort,
|
|
83
99
|
stderr: (data) => this.captureStderr(data),
|
|
84
|
-
canUseTool: async (toolName, input) => {
|
|
85
|
-
const decision = await opts.permission({
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
:
|
|
100
|
+
canUseTool: async (toolName, input, requestOptions = {}) => {
|
|
101
|
+
const decision = await opts.permission({
|
|
102
|
+
toolName,
|
|
103
|
+
input,
|
|
104
|
+
signal: requestOptions.signal,
|
|
105
|
+
title: requestOptions.title,
|
|
106
|
+
displayName: requestOptions.displayName,
|
|
107
|
+
description: requestOptions.description,
|
|
108
|
+
decisionReason: requestOptions.decisionReason,
|
|
109
|
+
blockedPath: requestOptions.blockedPath,
|
|
110
|
+
toolUseID: requestOptions.toolUseID,
|
|
111
|
+
});
|
|
112
|
+
return permissionDecisionToClaudeResult(decision, requestOptions.toolUseID);
|
|
89
113
|
},
|
|
90
114
|
},
|
|
91
115
|
});
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -8,13 +8,30 @@ export interface PermissionRequest {
|
|
|
8
8
|
input: Record<string, unknown>;
|
|
9
9
|
/** Human-readable prompt from the agent, when available. */
|
|
10
10
|
title?: string;
|
|
11
|
+
/** Signaled if the provider-side operation should be aborted. */
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
/** Provider-native identifier for this specific tool call, when available. */
|
|
14
|
+
toolUseID?: string;
|
|
15
|
+
/** Short provider-rendered name for the requested action, when available. */
|
|
16
|
+
displayName?: string;
|
|
17
|
+
/** Provider-rendered detail for the requested action, when available. */
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Provider explanation for why permission was requested, when available. */
|
|
20
|
+
decisionReason?: string;
|
|
21
|
+
/** Provider path that triggered the permission request, when available. */
|
|
22
|
+
blockedPath?: string;
|
|
11
23
|
}
|
|
12
24
|
/** The foreman's verdict on a permission request. */
|
|
13
25
|
export type PermissionDecision = {
|
|
14
26
|
behavior: "allow";
|
|
27
|
+
/** Provider-native tool input to continue with after host interaction. */
|
|
28
|
+
updatedInput?: Record<string, unknown>;
|
|
29
|
+
/** Provider-native permission updates to apply after approval. */
|
|
30
|
+
updatedPermissions?: Array<Record<string, unknown>>;
|
|
15
31
|
} | {
|
|
16
32
|
behavior: "deny";
|
|
17
33
|
message: string;
|
|
34
|
+
interrupt?: boolean;
|
|
18
35
|
};
|
|
19
36
|
/** Decides each permission request. Supplied by the foreman, called by the adapter. */
|
|
20
37
|
export type PermissionHandler = (req: PermissionRequest) => Promise<PermissionDecision>;
|
package/dist/agentRun.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type StepStatus } from "./foreman.js";
|
|
|
4
4
|
import type { BuilderAdapter, BuilderAdapterOptions, EffortLevel, TurnResult } from "./adapters/types.js";
|
|
5
5
|
import { type RoleBundle } from "./roles.js";
|
|
6
6
|
import type { AgentRuntime } from "./runtimeAuth.js";
|
|
7
|
+
import type { AgentRoleDefaultsV1 } from "rafi-spec";
|
|
7
8
|
export type { EffortLevel } from "./adapters/types.js";
|
|
8
9
|
export declare const VALID_EFFORT: readonly EffortLevel[];
|
|
9
10
|
export interface RoleBuilderOptions {
|
|
@@ -54,4 +55,5 @@ export declare function makeLogPath(projectDir: string, label: string): string;
|
|
|
54
55
|
export declare function mergeSkills(roleSkills: readonly string[], extraSkills?: readonly string[]): string[];
|
|
55
56
|
export declare function readOnlyPermissionConfig(): PermissionConfig;
|
|
56
57
|
export declare function createRoleBuilder(opts: RoleBuilderOptions): Promise<RoleBuilder>;
|
|
58
|
+
export declare function readRoleDefaultsForExecution(projectDir: string, role: string): AgentRoleDefaultsV1 | undefined;
|
|
57
59
|
export declare function runRoleInstruction(opts: RoleInstructionRunOptions): Promise<RoleInstructionRunResult>;
|
package/dist/agentRun.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadConfig, DEFAULT_CONFIG } from "./config.js";
|
|
4
4
|
import { Log } from "./log.js";
|
|
@@ -86,7 +86,7 @@ export async function createRoleBuilder(opts) {
|
|
|
86
86
|
if (!existsSync(opts.projectDir)) {
|
|
87
87
|
throw new Error(`project directory not found: ${opts.projectDir}`);
|
|
88
88
|
}
|
|
89
|
-
const saved =
|
|
89
|
+
const saved = readRoleDefaultsForExecution(opts.projectDir, opts.role);
|
|
90
90
|
const configuredEffort = saved?.reasoning && saved.reasoning !== "default" ? saved.reasoning : undefined;
|
|
91
91
|
const effectiveEffort = opts.effort ?? (VALID_EFFORT.includes(configuredEffort) ? configuredEffort : undefined);
|
|
92
92
|
assertEffortLevel(effectiveEffort);
|
|
@@ -107,6 +107,7 @@ export async function createRoleBuilder(opts) {
|
|
|
107
107
|
let runtimeExecutable = ready.executable;
|
|
108
108
|
const policy = new PermissionPolicy(opts.permissionConfig ?? config.permissions, opts.projectDir);
|
|
109
109
|
const skills = mergeSkills(roleBundle.skills, opts.extraSkills);
|
|
110
|
+
const interactive = !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
110
111
|
const makeAdapter = async (nextRuntime, resumeSessionId) => {
|
|
111
112
|
const adapterOpts = {
|
|
112
113
|
cwd: opts.projectDir,
|
|
@@ -114,7 +115,7 @@ export async function createRoleBuilder(opts) {
|
|
|
114
115
|
runtimePhase: phaseForRole(opts.role),
|
|
115
116
|
model,
|
|
116
117
|
resumeSessionId,
|
|
117
|
-
permission: createPermissionHandler(policy, log),
|
|
118
|
+
permission: createPermissionHandler(policy, log, { interactive }),
|
|
118
119
|
effort,
|
|
119
120
|
fast: opts.fast ?? saved?.fast,
|
|
120
121
|
sandboxMode: opts.sandboxMode,
|
|
@@ -130,7 +131,7 @@ export async function createRoleBuilder(opts) {
|
|
|
130
131
|
initial,
|
|
131
132
|
runtime,
|
|
132
133
|
label: opts.label,
|
|
133
|
-
enabled:
|
|
134
|
+
enabled: interactive,
|
|
134
135
|
allowSwitch: opts.allowSwitch !== false && !opts.resumeSessionId,
|
|
135
136
|
recreate: async (nextRuntime, resumeSessionId) => {
|
|
136
137
|
const nextReady = await ensureRuntimeReadyForCommand(opts.projectDir, nextRuntime, {
|
|
@@ -157,20 +158,47 @@ function phaseForRole(role) {
|
|
|
157
158
|
return "uninstaller";
|
|
158
159
|
return "builder";
|
|
159
160
|
}
|
|
160
|
-
function
|
|
161
|
+
export function readRoleDefaultsForExecution(projectDir, role) {
|
|
161
162
|
const path = join(projectDir, "rafi-config.yaml");
|
|
162
163
|
if (!existsSync(path))
|
|
163
164
|
return undefined;
|
|
164
165
|
try {
|
|
165
166
|
const config = parseYaml(readFileSync(path, "utf8"));
|
|
166
|
-
|
|
167
|
+
const saved = config?.agent_defaults?.version === 1
|
|
167
168
|
? config.agent_defaults.roles[role]
|
|
168
169
|
: undefined;
|
|
170
|
+
if (!saved)
|
|
171
|
+
return undefined;
|
|
172
|
+
if (isFullyInitializedTracker(projectDir))
|
|
173
|
+
return saved;
|
|
174
|
+
const { make: _pendingMake, ...active } = saved;
|
|
175
|
+
return active;
|
|
169
176
|
}
|
|
170
177
|
catch {
|
|
171
178
|
return undefined;
|
|
172
179
|
}
|
|
173
180
|
}
|
|
181
|
+
function isFullyInitializedTracker(projectDir) {
|
|
182
|
+
try {
|
|
183
|
+
const trackerPath = join(projectDir, ".tickets", "config.yaml");
|
|
184
|
+
const ticketsPath = join(projectDir, ".tickets", "tickets.yaml");
|
|
185
|
+
const statePath = join(projectDir, ".tickets", "ticket-state.sqlite");
|
|
186
|
+
if (!existsSync(trackerPath) || !existsSync(ticketsPath) || !existsSync(statePath))
|
|
187
|
+
return false;
|
|
188
|
+
const tracker = parseYaml(readFileSync(trackerPath, "utf8"));
|
|
189
|
+
if (!tracker || typeof tracker !== "object" || Array.isArray(tracker))
|
|
190
|
+
return false;
|
|
191
|
+
const tickets = parseYaml(readFileSync(ticketsPath, "utf8"));
|
|
192
|
+
if (!tickets || !Array.isArray(tickets.tickets))
|
|
193
|
+
return false;
|
|
194
|
+
if (!statSync(statePath).isFile())
|
|
195
|
+
return false;
|
|
196
|
+
return readFileSync(statePath).subarray(0, 16).toString("utf8") === "SQLite format 3\u0000";
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
174
202
|
export async function runRoleInstruction(opts) {
|
|
175
203
|
const logPath = opts.logPath ?? makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-"));
|
|
176
204
|
const log = new Log(logPath);
|
package/dist/foreman.d.ts
CHANGED
|
@@ -36,7 +36,9 @@ export declare function buildQaFixInstruction(issues: string): string;
|
|
|
36
36
|
/** Pre-flight planning turn: ask the builder to list its next N steps without implementing anything. */
|
|
37
37
|
export declare function buildPlanningTurn(n: number, ticketsContent?: string): string;
|
|
38
38
|
/** Builds the permission callback: classify, log, allow or escalate. */
|
|
39
|
-
export declare function createPermissionHandler(policy: PermissionPolicy, log: Log
|
|
39
|
+
export declare function createPermissionHandler(policy: PermissionPolicy, log: Log, opts?: {
|
|
40
|
+
interactive?: boolean;
|
|
41
|
+
}): PermissionHandler;
|
|
40
42
|
export interface BatchResult {
|
|
41
43
|
completed: number;
|
|
42
44
|
requested: number;
|
package/dist/foreman.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { select, isCancel } from "@clack/prompts";
|
|
2
2
|
import { MARKER_SPEC, QA_MARKER_SPEC } from "./markers.js";
|
|
3
|
+
import { countProviderQuestions, handleProviderQuestionTool } from "./providerQuestions.js";
|
|
3
4
|
import { fireNotification } from "./notify.js";
|
|
4
5
|
import { isTicketsInitialized, loadTicketsConfig } from "./tickets/config.js";
|
|
5
6
|
import { cmdUpdate, cmdComplete, cmdBlock, cmdImplementationQueue } from "./tickets/commands.js";
|
|
@@ -166,8 +167,20 @@ export function buildPlanningTurn(n, ticketsContent) {
|
|
|
166
167
|
return `${header}Before we begin, list the next ${n} ticket(s) or step(s) you plan to implement, in order. Be specific — reference ticket IDs or titles where applicable. Do not implement anything yet; output the numbered list only. Do not emit a STEP_STATUS marker on this turn.`;
|
|
167
168
|
}
|
|
168
169
|
/** Builds the permission callback: classify, log, allow or escalate. */
|
|
169
|
-
export function createPermissionHandler(policy, log) {
|
|
170
|
+
export function createPermissionHandler(policy, log, opts = {}) {
|
|
170
171
|
return async (req) => {
|
|
172
|
+
const providerQuestion = await handleProviderQuestionTool(req, {
|
|
173
|
+
interactive: opts.interactive ?? true,
|
|
174
|
+
});
|
|
175
|
+
if (providerQuestion) {
|
|
176
|
+
log.write("permission", {
|
|
177
|
+
tool: req.toolName,
|
|
178
|
+
decision: providerQuestion.behavior,
|
|
179
|
+
reason: "provider question prompt",
|
|
180
|
+
questionCount: countProviderQuestions(req.input),
|
|
181
|
+
});
|
|
182
|
+
return providerQuestion;
|
|
183
|
+
}
|
|
171
184
|
const verdict = policy.classify(req);
|
|
172
185
|
log.write("permission", {
|
|
173
186
|
tool: req.toolName,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
2
|
+
import type { PermissionDecision, PermissionRequest } from "./adapters/types.js";
|
|
3
|
+
export interface ProviderQuestionPromptDeps {
|
|
4
|
+
select: typeof select;
|
|
5
|
+
multiselect: typeof multiselect;
|
|
6
|
+
text: typeof text;
|
|
7
|
+
isCancel: typeof isCancel;
|
|
8
|
+
}
|
|
9
|
+
export interface ProviderQuestionOptions {
|
|
10
|
+
interactive: boolean;
|
|
11
|
+
prompts?: ProviderQuestionPromptDeps;
|
|
12
|
+
}
|
|
13
|
+
export declare function handleProviderQuestionTool(req: PermissionRequest, opts: ProviderQuestionOptions): Promise<PermissionDecision | undefined>;
|
|
14
|
+
export declare function countProviderQuestions(input: Record<string, unknown>): number;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
2
|
+
const ASK_USER_QUESTION_TOOL = "AskUserQuestion";
|
|
3
|
+
const CUSTOM_VALUE = "__rafi_custom_response__";
|
|
4
|
+
export async function handleProviderQuestionTool(req, opts) {
|
|
5
|
+
if (req.toolName !== ASK_USER_QUESTION_TOOL)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (!opts.interactive) {
|
|
8
|
+
return {
|
|
9
|
+
behavior: "deny",
|
|
10
|
+
interrupt: true,
|
|
11
|
+
message: "AskUserQuestion requires interactive input, but this Rafi run is non-interactive.",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const questions = parseQuestions(req.input);
|
|
15
|
+
if (questions.length === 0) {
|
|
16
|
+
return {
|
|
17
|
+
behavior: "deny",
|
|
18
|
+
interrupt: true,
|
|
19
|
+
message: "AskUserQuestion did not include any readable questions.",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const prompts = opts.prompts ?? { select, multiselect, text, isCancel };
|
|
23
|
+
const answers = recordFromUnknown(req.input.answers);
|
|
24
|
+
const annotations = annotationRecordFromUnknown(req.input.annotations);
|
|
25
|
+
for (const question of questions) {
|
|
26
|
+
const answered = await askOneQuestion(question, prompts, req.signal);
|
|
27
|
+
if (answered.cancelled) {
|
|
28
|
+
return {
|
|
29
|
+
behavior: "deny",
|
|
30
|
+
interrupt: true,
|
|
31
|
+
message: "User cancelled the provider question prompt.",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
answers[question.question] = answered.answer;
|
|
35
|
+
if (answered.annotation)
|
|
36
|
+
annotations[question.question] = answered.annotation;
|
|
37
|
+
}
|
|
38
|
+
const updatedInput = {
|
|
39
|
+
...req.input,
|
|
40
|
+
answers,
|
|
41
|
+
};
|
|
42
|
+
if (Object.keys(annotations).length > 0)
|
|
43
|
+
updatedInput.annotations = annotations;
|
|
44
|
+
return { behavior: "allow", updatedInput };
|
|
45
|
+
}
|
|
46
|
+
export function countProviderQuestions(input) {
|
|
47
|
+
const questions = input.questions;
|
|
48
|
+
return Array.isArray(questions) ? questions.length : 0;
|
|
49
|
+
}
|
|
50
|
+
function parseQuestions(input) {
|
|
51
|
+
const rawQuestions = input.questions;
|
|
52
|
+
if (!Array.isArray(rawQuestions))
|
|
53
|
+
return [];
|
|
54
|
+
return rawQuestions.flatMap((raw) => {
|
|
55
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
56
|
+
return [];
|
|
57
|
+
const record = raw;
|
|
58
|
+
const question = typeof record.question === "string" ? record.question.trim() : "";
|
|
59
|
+
if (!question)
|
|
60
|
+
return [];
|
|
61
|
+
const header = typeof record.header === "string" && record.header.trim()
|
|
62
|
+
? record.header.trim()
|
|
63
|
+
: undefined;
|
|
64
|
+
const options = Array.isArray(record.options)
|
|
65
|
+
? record.options.flatMap((option) => {
|
|
66
|
+
if (!option || typeof option !== "object" || Array.isArray(option))
|
|
67
|
+
return [];
|
|
68
|
+
const optionRecord = option;
|
|
69
|
+
const label = typeof optionRecord.label === "string" ? optionRecord.label.trim() : "";
|
|
70
|
+
if (!label)
|
|
71
|
+
return [];
|
|
72
|
+
return [{
|
|
73
|
+
label,
|
|
74
|
+
description: typeof optionRecord.description === "string" ? optionRecord.description : undefined,
|
|
75
|
+
preview: typeof optionRecord.preview === "string" ? optionRecord.preview : undefined,
|
|
76
|
+
}];
|
|
77
|
+
})
|
|
78
|
+
: [];
|
|
79
|
+
return [{ question, header, options, multiSelect: record.multiSelect === true }];
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function askOneQuestion(question, prompts, signal) {
|
|
83
|
+
const message = question.header ? `${question.header}: ${question.question}` : question.question;
|
|
84
|
+
if (question.options.length === 0) {
|
|
85
|
+
const answer = await prompts.text({ message, signal });
|
|
86
|
+
if (prompts.isCancel(answer))
|
|
87
|
+
return { cancelled: true, answer: "" };
|
|
88
|
+
return { cancelled: false, answer: String(answer) };
|
|
89
|
+
}
|
|
90
|
+
if (question.multiSelect) {
|
|
91
|
+
const selected = await prompts.multiselect({
|
|
92
|
+
message,
|
|
93
|
+
options: [
|
|
94
|
+
...question.options.map((option, index) => ({
|
|
95
|
+
value: String(index),
|
|
96
|
+
label: option.label,
|
|
97
|
+
hint: option.description,
|
|
98
|
+
})),
|
|
99
|
+
{ value: CUSTOM_VALUE, label: "Custom response", hint: "Type a different answer" },
|
|
100
|
+
],
|
|
101
|
+
required: true,
|
|
102
|
+
signal,
|
|
103
|
+
});
|
|
104
|
+
if (prompts.isCancel(selected))
|
|
105
|
+
return { cancelled: true, answer: "" };
|
|
106
|
+
const selectedValues = selected;
|
|
107
|
+
const labels = selectedValues
|
|
108
|
+
.filter((value) => value !== CUSTOM_VALUE)
|
|
109
|
+
.map((value) => question.options[Number(value)]?.label)
|
|
110
|
+
.filter((value) => Boolean(value));
|
|
111
|
+
const previews = selectedValues
|
|
112
|
+
.filter((value) => value !== CUSTOM_VALUE)
|
|
113
|
+
.map((value) => question.options[Number(value)]?.preview)
|
|
114
|
+
.filter((value) => Boolean(value));
|
|
115
|
+
let notes;
|
|
116
|
+
if (selectedValues.includes(CUSTOM_VALUE)) {
|
|
117
|
+
const custom = await prompts.text({ message: "Custom response:", signal });
|
|
118
|
+
if (prompts.isCancel(custom))
|
|
119
|
+
return { cancelled: true, answer: "" };
|
|
120
|
+
notes = String(custom);
|
|
121
|
+
labels.push(notes);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
cancelled: false,
|
|
125
|
+
answer: labels.join(", "),
|
|
126
|
+
annotation: annotationFor(previews, notes),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const selected = await prompts.select({
|
|
130
|
+
message,
|
|
131
|
+
options: [
|
|
132
|
+
...question.options.map((option, index) => ({
|
|
133
|
+
value: String(index),
|
|
134
|
+
label: option.label,
|
|
135
|
+
hint: option.description,
|
|
136
|
+
})),
|
|
137
|
+
{ value: CUSTOM_VALUE, label: "Custom response", hint: "Type a different answer" },
|
|
138
|
+
],
|
|
139
|
+
signal,
|
|
140
|
+
});
|
|
141
|
+
if (prompts.isCancel(selected))
|
|
142
|
+
return { cancelled: true, answer: "" };
|
|
143
|
+
if (selected === CUSTOM_VALUE) {
|
|
144
|
+
const custom = await prompts.text({ message: "Custom response:", signal });
|
|
145
|
+
if (prompts.isCancel(custom))
|
|
146
|
+
return { cancelled: true, answer: "" };
|
|
147
|
+
const answer = String(custom);
|
|
148
|
+
return { cancelled: false, answer, annotation: { notes: answer } };
|
|
149
|
+
}
|
|
150
|
+
const option = question.options[Number(selected)];
|
|
151
|
+
return {
|
|
152
|
+
cancelled: false,
|
|
153
|
+
answer: option?.label ?? String(selected),
|
|
154
|
+
annotation: annotationFor(option?.preview ? [option.preview] : [], undefined),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function annotationFor(previews, notes) {
|
|
158
|
+
const annotation = {};
|
|
159
|
+
if (previews.length > 0)
|
|
160
|
+
annotation.preview = previews.join("\n\n");
|
|
161
|
+
if (notes)
|
|
162
|
+
annotation.notes = notes;
|
|
163
|
+
return Object.keys(annotation).length > 0 ? annotation : undefined;
|
|
164
|
+
}
|
|
165
|
+
function recordFromUnknown(value) {
|
|
166
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
167
|
+
return {};
|
|
168
|
+
const result = {};
|
|
169
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
170
|
+
if (typeof entry === "string")
|
|
171
|
+
result[key] = entry;
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
function annotationRecordFromUnknown(value) {
|
|
176
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
177
|
+
return {};
|
|
178
|
+
const result = {};
|
|
179
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
180
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
181
|
+
continue;
|
|
182
|
+
const record = entry;
|
|
183
|
+
const annotation = {};
|
|
184
|
+
if (typeof record.preview === "string")
|
|
185
|
+
annotation.preview = record.preview;
|
|
186
|
+
if (typeof record.notes === "string")
|
|
187
|
+
annotation.notes = record.notes;
|
|
188
|
+
if (Object.keys(annotation).length > 0)
|
|
189
|
+
result[key] = annotation;
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
package/package.json
CHANGED