@howaboua/pi-ask 0.0.17 → 0.0.19
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/CHANGELOG.md +12 -0
- package/README.md +1 -0
- package/ask/contracts.ts +37 -20
- package/ask/normalize.ts +1 -1
- package/ask/tool.ts +23 -6
- package/ask/tui.ts +196 -185
- package/biome.json +1 -1
- package/index.ts +0 -2
- package/package.json +14 -14
- package/prompts/fold.md +1 -0
- package/prompts/grill.md +3 -0
- package/changelog.ts +0 -337
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @howaboua/pi-ask
|
|
2
2
|
|
|
3
|
+
## 0.0.19
|
|
4
|
+
|
|
5
|
+
- Requires Node.js 22.19 or newer.
|
|
6
|
+
- Replaced the TypeBox dependency with Pi's native schema builders and validation.
|
|
7
|
+
|
|
8
|
+
## 0.0.18
|
|
9
|
+
|
|
10
|
+
- Requires Pi 1.5 or newer.
|
|
11
|
+
- Waiting prompts now report Pi's native blocked state. Steering prompts remain non-blocking.
|
|
12
|
+
- Invalid question arguments now explain how to find the supported format.
|
|
13
|
+
- Removed custom changelog suppression. Updates use Pi's native notices and `collapseChangelog`.
|
|
14
|
+
|
|
3
15
|
## 0.0.17
|
|
4
16
|
|
|
5
17
|
- - Removed redundant help-call instructions from Ask's help.
|
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ Set either value to `false`, then run `/reload`, to hide that template. The same
|
|
|
49
49
|
## TUI controls
|
|
50
50
|
|
|
51
51
|
The panel uses Pi's active keybindings. The defaults are:
|
|
52
|
+
|
|
52
53
|
- `Up` / `Down` — move through choices, free text, and comments
|
|
53
54
|
- `Enter` — choose or edit the focused item, or save an edit
|
|
54
55
|
- `Left` / `Right` — move between prompts and the review tab
|
package/ask/contracts.ts
CHANGED
|
@@ -1,26 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
arr,
|
|
3
|
+
bool,
|
|
4
|
+
lit,
|
|
5
|
+
obj,
|
|
6
|
+
oneOf,
|
|
7
|
+
opt,
|
|
8
|
+
type Static,
|
|
9
|
+
str,
|
|
10
|
+
} from "@earendil-works/pi-schema";
|
|
3
11
|
|
|
4
12
|
const ASK_DELIVERIES = ["wait", "steer"] as const;
|
|
5
13
|
|
|
6
|
-
const ChoiceSchema =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
const ChoiceSchema = obj(
|
|
15
|
+
{
|
|
16
|
+
label: str(),
|
|
17
|
+
description: opt(str()),
|
|
18
|
+
},
|
|
19
|
+
{ open: true },
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const PromptSchema = obj(
|
|
23
|
+
{
|
|
24
|
+
title: str(),
|
|
25
|
+
body: opt(str()),
|
|
26
|
+
multiple: opt(bool()),
|
|
27
|
+
choices: opt(arr(ChoiceSchema)),
|
|
28
|
+
},
|
|
29
|
+
{ open: true },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export const AskParameters = obj(
|
|
33
|
+
{
|
|
34
|
+
help: opt({ ...lit(true), type: "boolean" }),
|
|
35
|
+
handoff: opt(bool()),
|
|
36
|
+
prompts: opt(arr(PromptSchema)),
|
|
37
|
+
delivery: opt({ ...oneOf(ASK_DELIVERIES), type: "string" }),
|
|
38
|
+
},
|
|
39
|
+
{ open: true },
|
|
40
|
+
);
|
|
24
41
|
|
|
25
42
|
export type PromptChoice = Static<typeof ChoiceSchema>;
|
|
26
43
|
|
package/ask/normalize.ts
CHANGED
|
@@ -57,7 +57,7 @@ function unknownField(
|
|
|
57
57
|
const field = Object.keys(value).find((key) => !fields.includes(key));
|
|
58
58
|
return field === undefined
|
|
59
59
|
? undefined
|
|
60
|
-
: `
|
|
60
|
+
: `Ask doesn't recognise ${path}${field}. Use ${fields.join(", ")}; call ask({}) for the supported format.`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Why a tool call would lose input to normalization, so a misnamed field fails instead of vanishing. */
|
package/ask/tool.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { compile, type Static } from "@earendil-works/pi-schema";
|
|
2
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import { Check } from "typebox/value";
|
|
4
4
|
import { AskParameters, type AskPrompt } from "./contracts.js";
|
|
5
5
|
import {
|
|
6
6
|
type AskCoordinatorOptions,
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
textContent,
|
|
17
17
|
} from "./normalize.js";
|
|
18
18
|
|
|
19
|
+
const askValidator = compile(AskParameters);
|
|
20
|
+
|
|
19
21
|
interface BlockedState {
|
|
20
22
|
id: string;
|
|
21
23
|
active: boolean;
|
|
@@ -57,11 +59,19 @@ export function createAskRuntime({
|
|
|
57
59
|
const params = input === undefined ? {} : input;
|
|
58
60
|
const problem = askInputProblem(params);
|
|
59
61
|
if (problem) throw new Error(problem);
|
|
60
|
-
if (!
|
|
61
|
-
throw new Error(
|
|
62
|
+
if (!askValidator.check(params))
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Ask couldn't read these arguments. Call ask({}) for the supported format.",
|
|
65
|
+
);
|
|
62
66
|
return params;
|
|
63
67
|
},
|
|
64
|
-
async execute(
|
|
68
|
+
async execute(
|
|
69
|
+
toolCallId,
|
|
70
|
+
params: Static<typeof AskParameters>,
|
|
71
|
+
signal,
|
|
72
|
+
_onUpdate,
|
|
73
|
+
ctx,
|
|
74
|
+
) {
|
|
65
75
|
if (isAskHelpInput(params)) {
|
|
66
76
|
return {
|
|
67
77
|
content: [
|
|
@@ -88,7 +98,7 @@ export function createAskRuntime({
|
|
|
88
98
|
const { delivery, handoff, prompts } = normalizeAskInput(params);
|
|
89
99
|
if (prompts.length === 0) {
|
|
90
100
|
throw new Error(
|
|
91
|
-
"
|
|
101
|
+
"Add at least one prompt with a non-empty title. Call ask({}) for the format.",
|
|
92
102
|
);
|
|
93
103
|
}
|
|
94
104
|
if (!askInComposer && !ctx.hasUI)
|
|
@@ -156,8 +166,15 @@ export function createAskRuntime({
|
|
|
156
166
|
renderCall(args, theme, context) {
|
|
157
167
|
if (isAskHelpInput(args))
|
|
158
168
|
return new Text(theme.fg("toolTitle", theme.bold("ask help")), 0, 0);
|
|
159
|
-
const
|
|
169
|
+
const prompts =
|
|
170
|
+
args && typeof args === "object" && "prompts" in args
|
|
171
|
+
? args.prompts
|
|
172
|
+
: undefined;
|
|
173
|
+
const count = Array.isArray(prompts) ? prompts.length : 0;
|
|
160
174
|
const label =
|
|
175
|
+
args &&
|
|
176
|
+
typeof args === "object" &&
|
|
177
|
+
"handoff" in args &&
|
|
161
178
|
args.handoff === true
|
|
162
179
|
? "ask handoff "
|
|
163
180
|
: isSteeringAskInput(args)
|
package/ask/tui.ts
CHANGED
|
@@ -34,202 +34,213 @@ export async function askInTui(
|
|
|
34
34
|
}: { handoff?: boolean; steering?: boolean; signal?: AbortSignal } = {},
|
|
35
35
|
): Promise<AskResponse[] | null> {
|
|
36
36
|
if (!ctx.hasUI || signal?.aborted) return null;
|
|
37
|
-
return await ctx.ui.custom<AskResponse[] | null>(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
37
|
+
return await ctx.ui.custom<AskResponse[] | null>(
|
|
38
|
+
(tui, theme, kb, done) => {
|
|
39
|
+
let settled = false;
|
|
40
|
+
const finish = (result: AskResponse[] | null) => {
|
|
41
|
+
if (settled) return;
|
|
42
|
+
settled = true;
|
|
43
|
+
signal?.removeEventListener("abort", abort);
|
|
44
|
+
done(result);
|
|
45
|
+
};
|
|
46
|
+
const abort = () => finish(null);
|
|
47
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
48
|
+
if (signal?.aborted) queueMicrotask(abort);
|
|
49
|
+
const state: AskUiState = { tab: 0, focus: 0, editing: null };
|
|
50
|
+
let cached: { width: number; lines: string[] } | undefined;
|
|
51
|
+
// Keep response lifecycle in one object per prompt. Parallel arrays made tab/default
|
|
52
|
+
// transitions easy to desynchronise as this tool grows.
|
|
53
|
+
const promptStates = prompts.map(createPromptState);
|
|
54
|
+
const editor = new Editor(tui, {
|
|
55
|
+
borderColor: (text) => theme.fg("accent", text),
|
|
56
|
+
selectList: {
|
|
57
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
58
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
59
|
+
description: (text) => theme.fg("muted", text),
|
|
60
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
61
|
+
noMatch: (text) => theme.fg("muted", text),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
104
|
-
const selectOther = () => {
|
|
105
|
-
const prompt = current();
|
|
106
|
-
const promptState = currentPromptState();
|
|
107
|
-
if (!prompt || !promptState) return;
|
|
108
|
-
if (prompt.multiple && promptState.customEnabled) {
|
|
109
|
-
clearCustomSelection(promptState);
|
|
65
|
+
const reviewTab = () => prompts.length;
|
|
66
|
+
const current = () => prompts[state.tab];
|
|
67
|
+
const currentPromptState = () => promptStates[state.tab];
|
|
68
|
+
const choices = () => current()?.choices ?? [];
|
|
69
|
+
const isReview = () => state.tab === reviewTab();
|
|
70
|
+
const count = () => choices().length + 2;
|
|
71
|
+
const refresh = () => {
|
|
72
|
+
cached = undefined;
|
|
73
|
+
tui.requestRender();
|
|
74
|
+
};
|
|
75
|
+
const responded = (index: number) =>
|
|
76
|
+
promptStateResponded(promptStates[index]);
|
|
77
|
+
const setTab = (next: number) => {
|
|
78
|
+
state.tab = Math.max(0, Math.min(reviewTab(), next));
|
|
79
|
+
state.focus = 0;
|
|
80
|
+
state.editing = null;
|
|
81
|
+
editor.setText(promptStates[state.tab]?.customText ?? "");
|
|
82
|
+
refresh();
|
|
83
|
+
};
|
|
84
|
+
const saveCustom = (submittedText = editor.getText()) => {
|
|
85
|
+
const prompt = current();
|
|
86
|
+
const promptState = currentPromptState();
|
|
87
|
+
if (!prompt || !promptState) return;
|
|
88
|
+
saveCustomSelection(prompt, promptState, submittedText);
|
|
89
|
+
};
|
|
90
|
+
const saveEditing = (submittedText = editor.getText()) => {
|
|
91
|
+
const promptState = currentPromptState();
|
|
92
|
+
if (!promptState) return;
|
|
93
|
+
if (state.editing === "comment")
|
|
94
|
+
saveComment(promptState, submittedText);
|
|
95
|
+
else saveCustom(submittedText);
|
|
96
|
+
};
|
|
97
|
+
const startEditing = (kind: EditingKind) => {
|
|
98
|
+
const promptState = currentPromptState();
|
|
99
|
+
if (!promptState) return;
|
|
100
|
+
state.editing = kind;
|
|
101
|
+
editor.setText(
|
|
102
|
+
kind === "comment" ? promptState.comment : promptState.customText,
|
|
103
|
+
);
|
|
110
104
|
refresh();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
105
|
+
};
|
|
106
|
+
const selectOther = () => {
|
|
107
|
+
const prompt = current();
|
|
108
|
+
const promptState = currentPromptState();
|
|
109
|
+
if (!prompt || !promptState) return;
|
|
110
|
+
if (prompt.multiple && promptState.customEnabled) {
|
|
111
|
+
clearCustomSelection(promptState);
|
|
112
|
+
refresh();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
startEditing("other");
|
|
116
|
+
};
|
|
117
|
+
const advance = () => setTab(state.tab + 1);
|
|
118
|
+
const advanceWithDefault = () => {
|
|
119
|
+
if (!isReview() && !responded(state.tab)) saveCustom("");
|
|
120
|
+
advance();
|
|
121
|
+
};
|
|
122
|
+
editor.onSubmit = (value) => {
|
|
123
|
+
saveEditing(value);
|
|
124
|
+
state.editing = null;
|
|
125
|
+
refresh();
|
|
126
|
+
};
|
|
127
|
+
const pick = (index: number) => {
|
|
128
|
+
const choice = choices()[index];
|
|
129
|
+
const prompt = current();
|
|
130
|
+
const promptState = currentPromptState();
|
|
131
|
+
if (!choice || !promptState) return;
|
|
132
|
+
pickChoiceSelection(prompt, promptState, choice);
|
|
133
|
+
refresh();
|
|
134
|
+
};
|
|
135
|
+
const handleInput = (data: string) => {
|
|
136
|
+
if (state.editing) {
|
|
137
|
+
handleAskEditingInput(data, {
|
|
138
|
+
advance,
|
|
139
|
+
editor,
|
|
140
|
+
keybindings: kb,
|
|
141
|
+
refresh,
|
|
142
|
+
saveEditing,
|
|
143
|
+
state,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
handleAskNavigationInput(data, {
|
|
149
|
+
done: finish,
|
|
150
|
+
isReview,
|
|
151
|
+
keybindings: kb,
|
|
152
|
+
promptStates,
|
|
153
|
+
prompts,
|
|
154
|
+
setTab,
|
|
155
|
+
state,
|
|
156
|
+
})
|
|
157
|
+
) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
handleAskPromptInput(data, {
|
|
161
|
+
advanceWithDefault,
|
|
162
|
+
count,
|
|
163
|
+
choices,
|
|
138
164
|
keybindings: kb,
|
|
165
|
+
pick,
|
|
139
166
|
refresh,
|
|
140
|
-
|
|
167
|
+
selectOther,
|
|
168
|
+
startEditingComment: () => startEditing("comment"),
|
|
141
169
|
state,
|
|
142
170
|
});
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
pick,
|
|
164
|
-
refresh,
|
|
165
|
-
selectOther,
|
|
166
|
-
startEditingComment: () => startEditing("comment"),
|
|
167
|
-
state,
|
|
168
|
-
});
|
|
169
|
-
};
|
|
170
|
-
const render = (width: number) => {
|
|
171
|
-
if (cached) return cached;
|
|
172
|
-
const lines: string[] = [];
|
|
173
|
-
const add: AddLine = (line = "") =>
|
|
174
|
-
lines.push(truncateToWidth(line, width));
|
|
175
|
-
add(theme.fg("accent", "─".repeat(width)));
|
|
176
|
-
add(
|
|
177
|
-
renderAskTabs({
|
|
178
|
-
handoff,
|
|
179
|
-
prompts,
|
|
180
|
-
responded,
|
|
181
|
-
steering,
|
|
182
|
-
tab: state.tab,
|
|
183
|
-
theme,
|
|
184
|
-
}),
|
|
185
|
-
);
|
|
186
|
-
add();
|
|
187
|
-
if (isReview()) {
|
|
188
|
-
renderAskReview({
|
|
189
|
-
add,
|
|
190
|
-
handoff,
|
|
191
|
-
keybindings: kb,
|
|
192
|
-
promptStates,
|
|
193
|
-
prompts,
|
|
194
|
-
theme,
|
|
195
|
-
width,
|
|
196
|
-
});
|
|
197
|
-
} else {
|
|
198
|
-
const prompt = current();
|
|
199
|
-
if (prompt) {
|
|
200
|
-
renderAskPrompt({
|
|
171
|
+
};
|
|
172
|
+
const render = (width: number) => {
|
|
173
|
+
if (cached?.width === width) return cached.lines;
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
const add: AddLine = (line = "") =>
|
|
176
|
+
lines.push(truncateToWidth(line, width));
|
|
177
|
+
add(theme.fg("accent", "─".repeat(width)));
|
|
178
|
+
add(
|
|
179
|
+
renderAskTabs({
|
|
180
|
+
handoff,
|
|
181
|
+
prompts,
|
|
182
|
+
responded,
|
|
183
|
+
steering,
|
|
184
|
+
tab: state.tab,
|
|
185
|
+
theme,
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
add();
|
|
189
|
+
if (isReview()) {
|
|
190
|
+
renderAskReview({
|
|
201
191
|
add,
|
|
202
|
-
editor,
|
|
203
192
|
handoff,
|
|
204
|
-
isEditing: state.editing,
|
|
205
193
|
keybindings: kb,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
promptState: currentPromptState(),
|
|
209
|
-
state,
|
|
194
|
+
promptStates,
|
|
195
|
+
prompts,
|
|
210
196
|
theme,
|
|
211
197
|
width,
|
|
212
198
|
});
|
|
199
|
+
} else {
|
|
200
|
+
const prompt = current();
|
|
201
|
+
if (prompt) {
|
|
202
|
+
renderAskPrompt({
|
|
203
|
+
add,
|
|
204
|
+
editor,
|
|
205
|
+
handoff,
|
|
206
|
+
isEditing: state.editing,
|
|
207
|
+
keybindings: kb,
|
|
208
|
+
choices: choices(),
|
|
209
|
+
prompt,
|
|
210
|
+
promptState: currentPromptState(),
|
|
211
|
+
state,
|
|
212
|
+
theme,
|
|
213
|
+
width,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
213
216
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
217
|
+
add(theme.fg("accent", "─".repeat(width)));
|
|
218
|
+
cached = { width, lines };
|
|
219
|
+
return lines;
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
get focused() {
|
|
223
|
+
return editor.focused;
|
|
224
|
+
},
|
|
225
|
+
set focused(value: boolean) {
|
|
226
|
+
editor.focused = value;
|
|
227
|
+
cached = undefined;
|
|
228
|
+
},
|
|
229
|
+
render,
|
|
230
|
+
handleInput,
|
|
231
|
+
dispose: () => signal?.removeEventListener("abort", abort),
|
|
232
|
+
invalidate: () => {
|
|
233
|
+
cached = undefined;
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
steering
|
|
238
|
+
? undefined
|
|
239
|
+
: {
|
|
240
|
+
blocked: {
|
|
241
|
+
kind: "question",
|
|
242
|
+
message: handoff ? "Human action needed" : "Waiting for input",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
);
|
|
235
246
|
}
|
package/biome.json
CHANGED
package/index.ts
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
import { isAskHelpInput, isSteeringAskInput } from "./ask/normalize.js";
|
|
17
17
|
import { PENDING_ASK_ENTRY_TYPE, readPendingAsks } from "./ask/pending.js";
|
|
18
18
|
import { createAskRuntime, createAskTool } from "./ask/tool.js";
|
|
19
|
-
import registerPackageChangelog from "./changelog.js";
|
|
20
19
|
|
|
21
20
|
export { createAskTool } from "./ask/tool.js";
|
|
22
21
|
|
|
@@ -28,7 +27,6 @@ const CODE_MODE_MODULE = `${CODE_MODE_PACKAGE}/code-mode`;
|
|
|
28
27
|
const DEVELOPER_MESSAGES_MODULE = `${CODE_MODE_PACKAGE}/developer-messages`;
|
|
29
28
|
|
|
30
29
|
export default async function humanInTheLoop(pi: ExtensionAPI): Promise<void> {
|
|
31
|
-
registerPackageChangelog(pi);
|
|
32
30
|
const trySendCodexDeveloperMessage = await loadCodexDeveloperMessageSender();
|
|
33
31
|
let gpt = false;
|
|
34
32
|
const askRuntime = createAskRuntime({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howaboua/pi-ask",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "Interactive decisions, review triage, and human handoffs for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"handoff"
|
|
24
24
|
],
|
|
25
25
|
"engines": {
|
|
26
|
-
"node": ">=
|
|
26
|
+
"node": ">=22.19.0"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
"LICENSE",
|
|
37
37
|
"tsconfig.json",
|
|
38
38
|
"biome.json",
|
|
39
|
-
"changelog.ts",
|
|
40
39
|
"CHANGELOG.md"
|
|
41
40
|
],
|
|
42
41
|
"pi": {
|
|
@@ -45,11 +44,10 @@
|
|
|
45
44
|
]
|
|
46
45
|
},
|
|
47
46
|
"peerDependencies": {
|
|
48
|
-
"@howaboua/pi-codex-conversion": ">=4.0.
|
|
47
|
+
"@howaboua/pi-codex-conversion": ">=4.0.10",
|
|
49
48
|
"@earendil-works/pi-ai": ">=1.0.0",
|
|
50
|
-
"@earendil-works/pi-coding-agent": ">=1.
|
|
51
|
-
"@earendil-works/pi-tui": ">=1.0.0"
|
|
52
|
-
"typebox": "*"
|
|
49
|
+
"@earendil-works/pi-coding-agent": ">=1.5.0",
|
|
50
|
+
"@earendil-works/pi-tui": ">=1.0.0"
|
|
53
51
|
},
|
|
54
52
|
"peerDependenciesMeta": {
|
|
55
53
|
"@howaboua/pi-codex-conversion": {
|
|
@@ -57,12 +55,11 @@
|
|
|
57
55
|
}
|
|
58
56
|
},
|
|
59
57
|
"devDependencies": {
|
|
60
|
-
"@howaboua/pi-codex-conversion": "4.0.
|
|
61
|
-
"@biomejs/biome": "^2.6.
|
|
62
|
-
"@earendil-works/pi-ai": "1.
|
|
63
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
64
|
-
"@earendil-works/pi-tui": "1.
|
|
65
|
-
"typebox": "1.4.3",
|
|
58
|
+
"@howaboua/pi-codex-conversion": "4.0.10",
|
|
59
|
+
"@biomejs/biome": "^2.6.6",
|
|
60
|
+
"@earendil-works/pi-ai": "1.5.2",
|
|
61
|
+
"@earendil-works/pi-coding-agent": "1.5.2",
|
|
62
|
+
"@earendil-works/pi-tui": "1.5.2",
|
|
66
63
|
"typescript": "^7.0.2"
|
|
67
64
|
},
|
|
68
65
|
"scripts": {
|
|
@@ -74,5 +71,8 @@
|
|
|
74
71
|
"pack:dry": "npm pack --dry-run"
|
|
75
72
|
},
|
|
76
73
|
"main": "index.ts",
|
|
77
|
-
"exports": "./index.ts"
|
|
74
|
+
"exports": "./index.ts",
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"@earendil-works/pi-schema": "1.5.2"
|
|
77
|
+
}
|
|
78
78
|
}
|
package/prompts/fold.md
CHANGED
|
@@ -11,6 +11,7 @@ $ARGUMENTS
|
|
|
11
11
|
If no material follows the command, use the most recent lengthy report or structured list in the conversation.
|
|
12
12
|
|
|
13
13
|
Call `ask` once with one prompt per independently decidable item:
|
|
14
|
+
|
|
14
15
|
- Keep the title short.
|
|
15
16
|
- Preserve concrete evidence, consequences, and recommendations in the body.
|
|
16
17
|
- Use the source's natural choices. For review findings, default to Fix, Defer, and Reject.
|
package/prompts/grill.md
CHANGED
|
@@ -11,6 +11,7 @@ $ARGUMENTS
|
|
|
11
11
|
Use the working directory, web search, available skills, and any relevant tools to understand the thing before asking. Prefer finding answers yourself over asking me.
|
|
12
12
|
|
|
13
13
|
Loop until ask is dismissed. There is no round limit:
|
|
14
|
+
|
|
14
15
|
1. Investigate the relevant context.
|
|
15
16
|
2. Use ask for the next set of decisions.
|
|
16
17
|
3. After the first answer round, create docs/<short-name>.md.
|
|
@@ -18,6 +19,7 @@ Loop until ask is dismissed. There is no round limit:
|
|
|
18
19
|
5. Keep updating the markdown file after each round.
|
|
19
20
|
|
|
20
21
|
The markdown file is the current plan, not a transcript. Keep it ready to use if I stop at any point. Include:
|
|
22
|
+
|
|
21
23
|
- goal
|
|
22
24
|
- agreed decisions
|
|
23
25
|
- important tradeoffs
|
|
@@ -28,6 +30,7 @@ The markdown file is the current plan, not a transcript. Keep it ready to use if
|
|
|
28
30
|
Do not log every question and answer.
|
|
29
31
|
|
|
30
32
|
When ask is dismissed, the grilling session is done. Then:
|
|
33
|
+
|
|
31
34
|
- give a short summary of what was agreed
|
|
32
35
|
- open the markdown file with $EDITOR if available
|
|
33
36
|
- print the file path
|
package/changelog.ts
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
// Generated extension packages copy this file during changelog:extensions.
|
|
2
|
-
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
5
|
-
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import {
|
|
9
|
-
DynamicBorder,
|
|
10
|
-
type ExtensionAPI,
|
|
11
|
-
getAgentDir,
|
|
12
|
-
getMarkdownTheme,
|
|
13
|
-
} from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
15
|
-
|
|
16
|
-
const ENTRY_TYPE = "@howaboua/pi-stuff/changelog";
|
|
17
|
-
const REGISTRATION_CHANNEL = "@howaboua/pi-stuff/changelog/register/v1";
|
|
18
|
-
const STATE_FILENAME = "howaboua-pi-stuff-changelog.json";
|
|
19
|
-
const SUPPRESS_KEY = "suppress";
|
|
20
|
-
const LOCK_STALE_MS = 30_000;
|
|
21
|
-
const LOCK_TIMEOUT_MS = 2_000;
|
|
22
|
-
|
|
23
|
-
interface PackageMetadata {
|
|
24
|
-
name: string;
|
|
25
|
-
version: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface PackageRegistration extends PackageMetadata {
|
|
29
|
-
changelogPath: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface RegistrationEvent {
|
|
33
|
-
accept: () => void;
|
|
34
|
-
registration: PackageRegistration;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ChangelogEntry {
|
|
38
|
-
content: string;
|
|
39
|
-
version: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface ChangelogEntryData {
|
|
43
|
-
markdown: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type ChangelogState = Record<string, boolean | string>;
|
|
47
|
-
|
|
48
|
-
function packageRegistration(): PackageRegistration {
|
|
49
|
-
const packageDirectory = fileURLToPath(new URL(".", import.meta.url));
|
|
50
|
-
const metadata = JSON.parse(
|
|
51
|
-
readFileSync(join(packageDirectory, "package.json"), "utf8"),
|
|
52
|
-
) as Partial<PackageMetadata>;
|
|
53
|
-
if (typeof metadata.name !== "string" || typeof metadata.version !== "string")
|
|
54
|
-
throw new Error(`Invalid package metadata in ${packageDirectory}`);
|
|
55
|
-
return {
|
|
56
|
-
name: metadata.name,
|
|
57
|
-
version: metadata.version,
|
|
58
|
-
changelogPath: join(packageDirectory, "CHANGELOG.md"),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function isRegistrationEvent(value: unknown): value is RegistrationEvent {
|
|
63
|
-
if (!value || typeof value !== "object") return false;
|
|
64
|
-
const event = value as Partial<RegistrationEvent>;
|
|
65
|
-
return (
|
|
66
|
-
typeof event.accept === "function" &&
|
|
67
|
-
typeof event.registration?.name === "string" &&
|
|
68
|
-
typeof event.registration.version === "string" &&
|
|
69
|
-
typeof event.registration.changelogPath === "string"
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function parseChangelog(markdown: string): ChangelogEntry[] {
|
|
74
|
-
const entries: ChangelogEntry[] = [];
|
|
75
|
-
let currentLines: string[] = [];
|
|
76
|
-
let currentVersion: string | undefined;
|
|
77
|
-
|
|
78
|
-
for (const line of markdown.split("\n")) {
|
|
79
|
-
const version = line.match(/^##\s+\[?(\d+\.\d+\.\d+)\]?(?:\s.*)?$/)?.[1];
|
|
80
|
-
if (version) {
|
|
81
|
-
if (currentVersion)
|
|
82
|
-
entries.push({
|
|
83
|
-
content: currentLines.join("\n").trim(),
|
|
84
|
-
version: currentVersion,
|
|
85
|
-
});
|
|
86
|
-
currentVersion = version;
|
|
87
|
-
currentLines = [line];
|
|
88
|
-
} else if (currentVersion) {
|
|
89
|
-
currentLines.push(line);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (currentVersion)
|
|
94
|
-
entries.push({
|
|
95
|
-
content: currentLines.join("\n").trim(),
|
|
96
|
-
version: currentVersion,
|
|
97
|
-
});
|
|
98
|
-
return entries;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function compareVersions(left: string, right: string): number {
|
|
102
|
-
const leftParts = left.split(".").map(Number);
|
|
103
|
-
const rightParts = right.split(".").map(Number);
|
|
104
|
-
for (let index = 0; index < 3; index++) {
|
|
105
|
-
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
106
|
-
if (difference !== 0) return difference;
|
|
107
|
-
}
|
|
108
|
-
return 0;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function isStableVersion(version: string): boolean {
|
|
112
|
-
return /^\d+\.\d+\.\d+$/.test(version);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function statePath(): string {
|
|
116
|
-
return join(getAgentDir(), STATE_FILENAME);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function readState(path: string): Promise<ChangelogState> {
|
|
120
|
-
let text: string;
|
|
121
|
-
try {
|
|
122
|
-
text = await readFile(path, "utf8");
|
|
123
|
-
} catch (error) {
|
|
124
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
const parsed = JSON.parse(text) as unknown;
|
|
128
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
129
|
-
throw new Error(`${path} must contain a JSON object`);
|
|
130
|
-
for (const [name, value] of Object.entries(parsed)) {
|
|
131
|
-
if (name === SUPPRESS_KEY) {
|
|
132
|
-
if (typeof value !== "boolean")
|
|
133
|
-
throw new Error(`${path} ${SUPPRESS_KEY} must be a boolean`);
|
|
134
|
-
} else if (typeof value !== "string") {
|
|
135
|
-
throw new Error(`${path} version for ${name} must be a string`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return parsed as ChangelogState;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function acquireLock(path: string): Promise<() => Promise<void>> {
|
|
142
|
-
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
143
|
-
while (true) {
|
|
144
|
-
try {
|
|
145
|
-
await mkdir(path);
|
|
146
|
-
return () => rm(path, { force: true, recursive: true });
|
|
147
|
-
} catch (error) {
|
|
148
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
149
|
-
const info = await stat(path).catch(() => undefined);
|
|
150
|
-
if (info && Date.now() - info.mtimeMs > LOCK_STALE_MS) {
|
|
151
|
-
await rm(path, { force: true, recursive: true });
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (Date.now() >= deadline)
|
|
155
|
-
throw new Error(`Timed out waiting for ${path}`);
|
|
156
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function writeState(path: string, state: ChangelogState): Promise<void> {
|
|
162
|
-
await mkdir(dirname(path), { mode: 0o700, recursive: true });
|
|
163
|
-
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
164
|
-
try {
|
|
165
|
-
const ordered = Object.fromEntries(
|
|
166
|
-
Object.entries(state).sort(([left], [right]) =>
|
|
167
|
-
left.localeCompare(right),
|
|
168
|
-
),
|
|
169
|
-
);
|
|
170
|
-
await writeFile(temporaryPath, `${JSON.stringify(ordered, null, 2)}\n`, {
|
|
171
|
-
encoding: "utf8",
|
|
172
|
-
mode: 0o600,
|
|
173
|
-
});
|
|
174
|
-
await rename(temporaryPath, path);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
await rm(temporaryPath, { force: true });
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function packageMarkdown(
|
|
182
|
-
registration: PackageRegistration,
|
|
183
|
-
entries: ChangelogEntry[],
|
|
184
|
-
): string {
|
|
185
|
-
const body = (entry: ChangelogEntry) =>
|
|
186
|
-
entry.content
|
|
187
|
-
.replace(/^##[^\n]*\n*/, "")
|
|
188
|
-
.replace(/^###\s+(?:Changes|Minor Changes|Patch Changes)\s*\n*/gim, "")
|
|
189
|
-
.replace(/^###\s+(.+)$/gm, "**$1**");
|
|
190
|
-
if (entries.length === 1) {
|
|
191
|
-
const [entry] = entries;
|
|
192
|
-
if (!entry) return "";
|
|
193
|
-
return `## ${registration.name} ${entry.version}\n\n${body(entry)}`;
|
|
194
|
-
}
|
|
195
|
-
return [
|
|
196
|
-
`## ${registration.name}`,
|
|
197
|
-
...entries.map((entry) => `**${entry.version}**\n\n${body(entry)}`),
|
|
198
|
-
].join("\n\n");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function claimUpdates(
|
|
202
|
-
registrations: Iterable<PackageRegistration>,
|
|
203
|
-
): Promise<{ errors: string[]; markdown?: string }> {
|
|
204
|
-
const stableRegistrations = [...registrations]
|
|
205
|
-
.filter((registration) => isStableVersion(registration.version))
|
|
206
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
207
|
-
if (stableRegistrations.length === 0) return { errors: [] };
|
|
208
|
-
const path = statePath();
|
|
209
|
-
await mkdir(dirname(path), { mode: 0o700, recursive: true });
|
|
210
|
-
const release = await acquireLock(`${path}.lock`);
|
|
211
|
-
try {
|
|
212
|
-
const state = await readState(path);
|
|
213
|
-
const errors: string[] = [];
|
|
214
|
-
const sections: string[] = [];
|
|
215
|
-
let changed = false;
|
|
216
|
-
if (state[SUPPRESS_KEY] === true) {
|
|
217
|
-
for (const registration of stableRegistrations) {
|
|
218
|
-
const seenVersion = state[registration.name];
|
|
219
|
-
if (
|
|
220
|
-
typeof seenVersion !== "string" ||
|
|
221
|
-
compareVersions(registration.version, seenVersion) > 0
|
|
222
|
-
) {
|
|
223
|
-
state[registration.name] = registration.version;
|
|
224
|
-
changed = true;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (changed) await writeState(path, state);
|
|
228
|
-
return { errors };
|
|
229
|
-
}
|
|
230
|
-
for (const registration of stableRegistrations) {
|
|
231
|
-
const seenVersion = state[registration.name];
|
|
232
|
-
if (
|
|
233
|
-
typeof seenVersion === "string" &&
|
|
234
|
-
compareVersions(registration.version, seenVersion) <= 0
|
|
235
|
-
)
|
|
236
|
-
continue;
|
|
237
|
-
try {
|
|
238
|
-
const entries = parseChangelog(
|
|
239
|
-
await readFile(registration.changelogPath, "utf8"),
|
|
240
|
-
).filter(
|
|
241
|
-
(entry) =>
|
|
242
|
-
compareVersions(entry.version, registration.version) <= 0 &&
|
|
243
|
-
(typeof seenVersion === "string"
|
|
244
|
-
? compareVersions(entry.version, seenVersion) > 0
|
|
245
|
-
: entry.version === registration.version),
|
|
246
|
-
);
|
|
247
|
-
if (entries.length === 0) {
|
|
248
|
-
errors.push(
|
|
249
|
-
`No ${registration.version} changelog entry found for ${registration.name}`,
|
|
250
|
-
);
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
sections.push(packageMarkdown(registration, entries));
|
|
254
|
-
state[registration.name] = registration.version;
|
|
255
|
-
changed = true;
|
|
256
|
-
} catch (error) {
|
|
257
|
-
errors.push(
|
|
258
|
-
`Could not read the ${registration.name} changelog: ${error instanceof Error ? error.message : String(error)}`,
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (changed) await writeState(path, state);
|
|
263
|
-
return {
|
|
264
|
-
errors,
|
|
265
|
-
...(sections.length > 0 ? { markdown: sections.join("\n\n") } : {}),
|
|
266
|
-
};
|
|
267
|
-
} finally {
|
|
268
|
-
await release();
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function registerCoordinator(
|
|
273
|
-
pi: ExtensionAPI,
|
|
274
|
-
registration: PackageRegistration,
|
|
275
|
-
): void {
|
|
276
|
-
let accepted = false;
|
|
277
|
-
pi.events.emit(REGISTRATION_CHANNEL, {
|
|
278
|
-
registration,
|
|
279
|
-
accept: () => {
|
|
280
|
-
accepted = true;
|
|
281
|
-
},
|
|
282
|
-
} satisfies RegistrationEvent);
|
|
283
|
-
if (accepted) return;
|
|
284
|
-
|
|
285
|
-
const registrations = new Map([[registration.name, registration]]);
|
|
286
|
-
pi.events.on(REGISTRATION_CHANNEL, (value) => {
|
|
287
|
-
if (!isRegistrationEvent(value)) return;
|
|
288
|
-
registrations.set(value.registration.name, value.registration);
|
|
289
|
-
value.accept();
|
|
290
|
-
});
|
|
291
|
-
pi.registerEntryRenderer<ChangelogEntryData>(
|
|
292
|
-
ENTRY_TYPE,
|
|
293
|
-
(entry, _options, theme) => {
|
|
294
|
-
if (!entry.data) return undefined;
|
|
295
|
-
const container = new Container();
|
|
296
|
-
container.addChild(new DynamicBorder());
|
|
297
|
-
container.addChild(
|
|
298
|
-
new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0),
|
|
299
|
-
);
|
|
300
|
-
container.addChild(new Spacer(1));
|
|
301
|
-
container.addChild(
|
|
302
|
-
new Markdown(entry.data.markdown.trim(), 1, 0, getMarkdownTheme()),
|
|
303
|
-
);
|
|
304
|
-
container.addChild(new Spacer(1));
|
|
305
|
-
container.addChild(new DynamicBorder());
|
|
306
|
-
return container;
|
|
307
|
-
},
|
|
308
|
-
);
|
|
309
|
-
pi.on("session_start", async (event, ctx) => {
|
|
310
|
-
if (event.reason === "reload" || ctx.mode !== "tui") return;
|
|
311
|
-
if (
|
|
312
|
-
ctx.sessionManager
|
|
313
|
-
.getEntries()
|
|
314
|
-
.some((entry) =>
|
|
315
|
-
["branch_summary", "compaction", "message"].includes(entry.type),
|
|
316
|
-
)
|
|
317
|
-
)
|
|
318
|
-
return;
|
|
319
|
-
try {
|
|
320
|
-
const update = await claimUpdates(registrations.values());
|
|
321
|
-
if (update.markdown)
|
|
322
|
-
pi.appendEntry<ChangelogEntryData>(ENTRY_TYPE, {
|
|
323
|
-
markdown: update.markdown,
|
|
324
|
-
});
|
|
325
|
-
for (const error of update.errors) ctx.ui.notify(error, "warning");
|
|
326
|
-
} catch (error) {
|
|
327
|
-
ctx.ui.notify(
|
|
328
|
-
`Could not update Howaboua changelog state: ${error instanceof Error ? error.message : String(error)}`,
|
|
329
|
-
"warning",
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export default function howabouaPackageChangelog(pi: ExtensionAPI): void {
|
|
336
|
-
registerCoordinator(pi, packageRegistration());
|
|
337
|
-
}
|