@hobin/developer 0.1.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/LICENSE +21 -0
- package/README.md +231 -0
- package/extensions/developer.ts +700 -0
- package/extensions/skills.ts +89 -0
- package/extensions/state.ts +283 -0
- package/extensions/tool-policy.ts +65 -0
- package/extensions/tui.ts +391 -0
- package/package.json +43 -0
- package/skills/abstraction-review/SKILL.md +87 -0
- package/skills/abstraction-review/references/field-card.md +209 -0
- package/skills/abstraction-review/references/recipe-cards.md +255 -0
- package/skills/abstraction-review/references/repair-table.md +98 -0
- package/skills/abstraction-review/references/worked-examples.md +306 -0
- package/skills/adversarial-eval/SKILL.md +87 -0
- package/skills/model/SKILL.md +76 -0
- package/skills/model/references/problem-modeling.md +262 -0
- package/skills/naming-judgment/SKILL.md +77 -0
- package/skills/naming-judgment/references/elements-of-clojure-naming.md +74 -0
- package/skills/schedule/SKILL.md +71 -0
- package/skills/signal/SKILL.md +75 -0
- package/skills/sketch/SKILL.md +70 -0
- package/skills/specify/SKILL.md +67 -0
- package/skills/verify/SKILL.md +71 -0
- package/skills/visualize/SKILL.md +65 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_MAX_BYTES,
|
|
7
|
+
DEFAULT_MAX_LINES,
|
|
8
|
+
loadSkillsFromDir,
|
|
9
|
+
stripFrontmatter,
|
|
10
|
+
type Skill,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
const METHOD_OUTPUT_OVERHEAD_BYTES = 4096;
|
|
14
|
+
const METHOD_OUTPUT_OVERHEAD_LINES = 20;
|
|
15
|
+
|
|
16
|
+
function escapeAttribute(value: string): string {
|
|
17
|
+
return value
|
|
18
|
+
.replaceAll("&", "&")
|
|
19
|
+
.replaceAll('"', """)
|
|
20
|
+
.replaceAll("<", "<")
|
|
21
|
+
.replaceAll(">", ">");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isWithinRoot(root: string, path: string): boolean {
|
|
25
|
+
const canonical = (value: string) => {
|
|
26
|
+
try {
|
|
27
|
+
return realpathSync.native(value);
|
|
28
|
+
} catch {
|
|
29
|
+
return resolve(value);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const relation = relative(canonical(root), canonical(path));
|
|
33
|
+
return relation === "" || (!relation.startsWith("..") && !isAbsolute(relation));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadCandidateSkills(skillsRoot: string): Map<string, Skill> {
|
|
37
|
+
const loaded = loadSkillsFromDir({ dir: skillsRoot, source: "@hobin/developer" });
|
|
38
|
+
const candidates = new Map<string, Skill>();
|
|
39
|
+
|
|
40
|
+
for (const skill of loaded.skills) {
|
|
41
|
+
if (candidates.has(skill.name)) throw new Error(`Duplicate Developer skill name: ${skill.name}`);
|
|
42
|
+
candidates.set(skill.name, skill);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (candidates.size === 0) throw new Error(`No Developer skills found in ${skillsRoot}`);
|
|
46
|
+
return candidates;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function availablePackageSkills(
|
|
50
|
+
loadedSkills: Skill[],
|
|
51
|
+
candidates: ReadonlyMap<string, Skill>,
|
|
52
|
+
skillsRoot: string,
|
|
53
|
+
): Map<string, Skill> {
|
|
54
|
+
const available = new Map<string, Skill>();
|
|
55
|
+
|
|
56
|
+
for (const skill of loadedSkills) {
|
|
57
|
+
if (skill.disableModelInvocation) continue;
|
|
58
|
+
if (!candidates.has(skill.name)) continue;
|
|
59
|
+
if (!isWithinRoot(skillsRoot, skill.filePath)) continue;
|
|
60
|
+
available.set(skill.name, skill);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return available;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function renderSkillMethod(skill: Skill): Promise<string> {
|
|
67
|
+
const source = await readFile(skill.filePath, "utf8");
|
|
68
|
+
const body = stripFrontmatter(source).trim();
|
|
69
|
+
const bodyBytes = Buffer.byteLength(body, "utf8");
|
|
70
|
+
const bodyLines = body.split(/\r?\n/).length;
|
|
71
|
+
if (
|
|
72
|
+
bodyBytes > DEFAULT_MAX_BYTES - METHOD_OUTPUT_OVERHEAD_BYTES ||
|
|
73
|
+
bodyLines > DEFAULT_MAX_LINES - METHOD_OUTPUT_OVERHEAD_LINES
|
|
74
|
+
) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Developer skill ${skill.name} is too large for safe forced loading. Move detail into relative references before routing it.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const name = escapeAttribute(skill.name);
|
|
80
|
+
const location = escapeAttribute(skill.filePath);
|
|
81
|
+
const baseDir = escapeAttribute(skill.baseDir);
|
|
82
|
+
return [
|
|
83
|
+
`<developer-method name="${name}" location="${location}" base-dir="${baseDir}">`,
|
|
84
|
+
body,
|
|
85
|
+
"</developer-method>",
|
|
86
|
+
"",
|
|
87
|
+
`Resolve relative references from ${skill.baseDir}.`,
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
export const PROTOCOL = "developer/v2" as const;
|
|
2
|
+
export const LEGACY_PROTOCOL = "developer/v1" as const;
|
|
3
|
+
export const MODE_ENTRY = "developer.mode" as const;
|
|
4
|
+
export const ROUTE_TOOL = "developer_route_question" as const;
|
|
5
|
+
export const JUDGMENT_TOOL = "developer_record_judgment" as const;
|
|
6
|
+
export const LEGACY_ROUTE_TOOL = "route_question" as const;
|
|
7
|
+
export const LEGACY_JUDGMENT_TOOL = "record_judgment" as const;
|
|
8
|
+
|
|
9
|
+
export type DeveloperMode = "off" | "on" | "strict";
|
|
10
|
+
export type JudgmentStatus = "resolved" | "needs-evidence" | "not-applicable" | "blocked";
|
|
11
|
+
export type PendingQuestionStatus = "needs-evidence" | "blocked";
|
|
12
|
+
|
|
13
|
+
export interface ModeEvent {
|
|
14
|
+
protocol: typeof PROTOCOL;
|
|
15
|
+
kind: "mode";
|
|
16
|
+
mode: DeveloperMode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RouteEvent {
|
|
20
|
+
protocol: typeof PROTOCOL;
|
|
21
|
+
kind: "route";
|
|
22
|
+
routeId: string;
|
|
23
|
+
question: string;
|
|
24
|
+
owner: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
knownEvidence: string[];
|
|
27
|
+
targetQuestionId?: string;
|
|
28
|
+
methodLocation?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PendingQuestion {
|
|
32
|
+
id: string;
|
|
33
|
+
question: string;
|
|
34
|
+
status: PendingQuestionStatus;
|
|
35
|
+
sourceRouteId: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface JudgmentEvent {
|
|
39
|
+
protocol: typeof PROTOCOL;
|
|
40
|
+
kind: "judgment";
|
|
41
|
+
routeId: string;
|
|
42
|
+
question: string;
|
|
43
|
+
owner: string;
|
|
44
|
+
status: JudgmentStatus;
|
|
45
|
+
result: string;
|
|
46
|
+
basis: string[];
|
|
47
|
+
openedQuestions: PendingQuestion[];
|
|
48
|
+
artifacts: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type DeveloperEvent = ModeEvent | RouteEvent | JudgmentEvent;
|
|
52
|
+
|
|
53
|
+
export interface DeveloperState {
|
|
54
|
+
mode: DeveloperMode;
|
|
55
|
+
activeRoute?: RouteEvent;
|
|
56
|
+
lastRoute?: RouteEvent;
|
|
57
|
+
lastJudgment?: JudgmentEvent;
|
|
58
|
+
pendingQuestions: PendingQuestion[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const initialState = (): DeveloperState => ({
|
|
62
|
+
mode: "off",
|
|
63
|
+
pendingQuestions: [],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type ProtocolState = "idle" | "needs-judgment" | "needs-evidence" | "blocked";
|
|
67
|
+
|
|
68
|
+
export function protocolState(state: DeveloperState): ProtocolState {
|
|
69
|
+
if (state.activeRoute) return "needs-judgment";
|
|
70
|
+
if (state.pendingQuestions.some((question) => question.status === "blocked")) return "blocked";
|
|
71
|
+
if (state.pendingQuestions.length > 0) return "needs-evidence";
|
|
72
|
+
return "idle";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function upsertQuestion(questions: PendingQuestion[], next: PendingQuestion): PendingQuestion[] {
|
|
76
|
+
return [...questions.filter((question) => question.id !== next.id), next];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function applyDeveloperEvent(state: DeveloperState, event: DeveloperEvent): DeveloperState {
|
|
80
|
+
if (event.kind === "mode") {
|
|
81
|
+
if (event.mode === "off") return initialState();
|
|
82
|
+
return { ...state, mode: event.mode };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (event.kind === "route") {
|
|
86
|
+
if (state.activeRoute) return state;
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
activeRoute: event,
|
|
90
|
+
lastRoute: event,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!state.activeRoute || state.activeRoute.routeId !== event.routeId) return state;
|
|
95
|
+
|
|
96
|
+
const route = state.activeRoute;
|
|
97
|
+
const judgment = { ...event, question: route.question, owner: route.owner };
|
|
98
|
+
let pending = [...state.pendingQuestions];
|
|
99
|
+
const remainsOpen = event.status === "needs-evidence" || event.status === "blocked";
|
|
100
|
+
|
|
101
|
+
if (route.targetQuestionId) {
|
|
102
|
+
pending = pending.filter((question) => question.id !== route.targetQuestionId);
|
|
103
|
+
if (remainsOpen) {
|
|
104
|
+
pending = upsertQuestion(pending, {
|
|
105
|
+
id: route.targetQuestionId,
|
|
106
|
+
question: route.question,
|
|
107
|
+
status: event.status,
|
|
108
|
+
sourceRouteId: route.routeId,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} else if (remainsOpen) {
|
|
112
|
+
pending = upsertQuestion(pending, {
|
|
113
|
+
id: `question:${route.routeId}`,
|
|
114
|
+
question: route.question,
|
|
115
|
+
status: event.status,
|
|
116
|
+
sourceRouteId: route.routeId,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const question of event.openedQuestions) pending = upsertQuestion(pending, question);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
...state,
|
|
124
|
+
activeRoute: undefined,
|
|
125
|
+
lastJudgment: judgment,
|
|
126
|
+
pendingQuestions: pending,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
131
|
+
return Boolean(value) && typeof value === "object";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isStringArray(value: unknown): value is string[] {
|
|
135
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isMode(value: unknown): value is DeveloperMode {
|
|
139
|
+
return value === "off" || value === "on" || value === "strict";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isJudgmentStatus(value: unknown): value is JudgmentStatus {
|
|
143
|
+
return value === "resolved" || value === "needs-evidence" || value === "not-applicable" || value === "blocked";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parsePendingQuestion(value: unknown): PendingQuestion | undefined {
|
|
147
|
+
if (!isObject(value)) return undefined;
|
|
148
|
+
if (
|
|
149
|
+
typeof value.id !== "string" ||
|
|
150
|
+
typeof value.question !== "string" ||
|
|
151
|
+
(value.status !== "needs-evidence" && value.status !== "blocked") ||
|
|
152
|
+
typeof value.sourceRouteId !== "string"
|
|
153
|
+
) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
id: value.id,
|
|
158
|
+
question: value.question,
|
|
159
|
+
status: value.status,
|
|
160
|
+
sourceRouteId: value.sourceRouteId,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefined {
|
|
165
|
+
if (!isObject(value)) return undefined;
|
|
166
|
+
if (value.protocol !== PROTOCOL && value.protocol !== LEGACY_PROTOCOL) return undefined;
|
|
167
|
+
|
|
168
|
+
if (value.kind === "mode") {
|
|
169
|
+
if (!isMode(value.mode)) return undefined;
|
|
170
|
+
return { protocol: PROTOCOL, kind: "mode", mode: value.mode };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (value.kind === "route") {
|
|
174
|
+
if (
|
|
175
|
+
typeof value.routeId !== "string" ||
|
|
176
|
+
typeof value.question !== "string" ||
|
|
177
|
+
typeof value.owner !== "string" ||
|
|
178
|
+
typeof value.reason !== "string" ||
|
|
179
|
+
!isStringArray(value.knownEvidence) ||
|
|
180
|
+
(value.targetQuestionId !== undefined && typeof value.targetQuestionId !== "string") ||
|
|
181
|
+
(value.methodLocation !== undefined && typeof value.methodLocation !== "string")
|
|
182
|
+
) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
protocol: PROTOCOL,
|
|
187
|
+
kind: "route",
|
|
188
|
+
routeId: value.routeId,
|
|
189
|
+
question: value.question,
|
|
190
|
+
owner: value.owner,
|
|
191
|
+
reason: value.reason,
|
|
192
|
+
knownEvidence: value.knownEvidence,
|
|
193
|
+
targetQuestionId: value.targetQuestionId,
|
|
194
|
+
methodLocation: value.methodLocation,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (value.kind !== "judgment") return undefined;
|
|
199
|
+
if (
|
|
200
|
+
typeof value.routeId !== "string" ||
|
|
201
|
+
typeof value.question !== "string" ||
|
|
202
|
+
typeof value.owner !== "string" ||
|
|
203
|
+
!isJudgmentStatus(value.status) ||
|
|
204
|
+
typeof value.result !== "string" ||
|
|
205
|
+
!isStringArray(value.basis) ||
|
|
206
|
+
!isStringArray(value.artifacts)
|
|
207
|
+
) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (value.protocol === LEGACY_PROTOCOL) {
|
|
212
|
+
if (!isStringArray(value.openQuestions)) return undefined;
|
|
213
|
+
return {
|
|
214
|
+
protocol: PROTOCOL,
|
|
215
|
+
kind: "judgment",
|
|
216
|
+
routeId: value.routeId,
|
|
217
|
+
question: value.question,
|
|
218
|
+
owner: value.owner,
|
|
219
|
+
status: value.status,
|
|
220
|
+
result: value.result,
|
|
221
|
+
basis: value.basis,
|
|
222
|
+
openedQuestions: value.openQuestions.map((question, index) => ({
|
|
223
|
+
id: `question:${value.routeId}:legacy:${index + 1}`,
|
|
224
|
+
question,
|
|
225
|
+
status: "needs-evidence",
|
|
226
|
+
sourceRouteId: value.routeId,
|
|
227
|
+
})),
|
|
228
|
+
artifacts: value.artifacts,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!Array.isArray(value.openedQuestions)) return undefined;
|
|
233
|
+
const openedQuestions = value.openedQuestions.map(parsePendingQuestion);
|
|
234
|
+
if (openedQuestions.some((question) => !question)) return undefined;
|
|
235
|
+
return {
|
|
236
|
+
protocol: PROTOCOL,
|
|
237
|
+
kind: "judgment",
|
|
238
|
+
routeId: value.routeId,
|
|
239
|
+
question: value.question,
|
|
240
|
+
owner: value.owner,
|
|
241
|
+
status: value.status,
|
|
242
|
+
result: value.result,
|
|
243
|
+
basis: value.basis,
|
|
244
|
+
openedQuestions: openedQuestions as PendingQuestion[],
|
|
245
|
+
artifacts: value.artifacts,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
interface BranchEntryLike {
|
|
250
|
+
type: string;
|
|
251
|
+
customType?: string;
|
|
252
|
+
data?: unknown;
|
|
253
|
+
message?: { role?: string; toolName?: string; details?: unknown };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const DEVELOPER_TOOL_NAMES = new Set([
|
|
257
|
+
ROUTE_TOOL,
|
|
258
|
+
JUDGMENT_TOOL,
|
|
259
|
+
LEGACY_ROUTE_TOOL,
|
|
260
|
+
LEGACY_JUDGMENT_TOOL,
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
export function eventFromBranchEntry(entry: BranchEntryLike): DeveloperEvent | undefined {
|
|
264
|
+
if (entry.type === "custom" && entry.customType === MODE_ENTRY) {
|
|
265
|
+
return normalizeDeveloperEvent(entry.data);
|
|
266
|
+
}
|
|
267
|
+
if (
|
|
268
|
+
entry.type === "message" &&
|
|
269
|
+
entry.message?.role === "toolResult" &&
|
|
270
|
+
entry.message.toolName &&
|
|
271
|
+
DEVELOPER_TOOL_NAMES.has(entry.message.toolName)
|
|
272
|
+
) {
|
|
273
|
+
return normalizeDeveloperEvent(entry.message.details);
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function reconstructState(entries: ReadonlyArray<BranchEntryLike>): DeveloperState {
|
|
279
|
+
return entries.reduce((state, entry) => {
|
|
280
|
+
const event = eventFromBranchEntry(entry);
|
|
281
|
+
return event ? applyDeveloperEvent(state, event) : state;
|
|
282
|
+
}, initialState());
|
|
283
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { DeveloperMode } from "./state.ts";
|
|
2
|
+
|
|
3
|
+
const MUTATION_TOOL_NAMES = new Set(["edit", "write", "bash"]);
|
|
4
|
+
|
|
5
|
+
export interface ToolMetadataLike {
|
|
6
|
+
name: string;
|
|
7
|
+
sourceInfo: { source: string };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ToolPolicyMemory {
|
|
11
|
+
withheldBuiltins: Set<string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ToolPolicyResult {
|
|
15
|
+
activeTools: string[];
|
|
16
|
+
memory: ToolPolicyMemory;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function builtinMutationToolNames(tools: ToolMetadataLike[]): Set<string> {
|
|
20
|
+
return new Set(
|
|
21
|
+
tools
|
|
22
|
+
.filter((tool) => tool.sourceInfo.source === "builtin" && MUTATION_TOOL_NAMES.has(tool.name))
|
|
23
|
+
.map((tool) => tool.name),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function reconcileProtocolTools(input: {
|
|
28
|
+
activeTools: string[];
|
|
29
|
+
allTools: ToolMetadataLike[];
|
|
30
|
+
mode: DeveloperMode;
|
|
31
|
+
directRouteOpen: boolean;
|
|
32
|
+
protocolTools: readonly string[];
|
|
33
|
+
memory: ToolPolicyMemory;
|
|
34
|
+
}): ToolPolicyResult {
|
|
35
|
+
const active = new Set(input.activeTools);
|
|
36
|
+
const mutationBuiltins = builtinMutationToolNames(input.allTools);
|
|
37
|
+
const withheld = new Set(
|
|
38
|
+
[...input.memory.withheldBuiltins].filter((name) => mutationBuiltins.has(name)),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (input.mode === "strict") {
|
|
42
|
+
for (const tool of input.protocolTools) active.add(tool);
|
|
43
|
+
|
|
44
|
+
if (input.directRouteOpen) {
|
|
45
|
+
for (const tool of withheld) active.add(tool);
|
|
46
|
+
} else {
|
|
47
|
+
for (const tool of mutationBuiltins) {
|
|
48
|
+
if (active.delete(tool)) withheld.add(tool);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
for (const tool of withheld) active.add(tool);
|
|
53
|
+
withheld.clear();
|
|
54
|
+
|
|
55
|
+
for (const tool of input.protocolTools) {
|
|
56
|
+
if (input.mode === "on") active.add(tool);
|
|
57
|
+
else active.delete(tool);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
activeTools: [...active],
|
|
63
|
+
memory: { withheldBuiltins: withheld },
|
|
64
|
+
};
|
|
65
|
+
}
|