@dlacaille/opencode-copilot-instructions 0.1.0 → 0.3.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/README.md +2 -2
- package/index.ts +1 -1
- package/package.json +5 -2
- package/src/frontmatter.ts +15 -15
- package/src/index.ts +109 -63
- package/src/loader.ts +37 -26
- package/src/matcher.ts +11 -9
- package/src/session-state.ts +12 -12
package/README.md
CHANGED
|
@@ -35,9 +35,9 @@ Always use explicit return types.
|
|
|
35
35
|
|
|
36
36
|
When the `read`, `edit`, or `write` tools touch a matching file, the instructions are appended to the tool result. Each file is added once per session, and again after the session is compacted.
|
|
37
37
|
|
|
38
|
-
`applyTo` accepts a comma-separated string or a YAML list of [picomatch](https://github.com/micromatch/picomatch) globs, relative to the
|
|
38
|
+
`applyTo` accepts a comma-separated string or a YAML list of [picomatch](https://github.com/micromatch/picomatch) globs, relative to the git root.
|
|
39
39
|
|
|
40
|
-
Instructions are
|
|
40
|
+
Instructions are read from the `.github` folder at the git root of each session's directory (or the directory itself outside a git repository). They are reloaded on every use, so edits apply without restarting OpenCode.
|
|
41
41
|
|
|
42
42
|
## Development
|
|
43
43
|
|
package/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from "./src/index"
|
|
1
|
+
export { default } from "./src/index";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dlacaille/opencode-copilot-instructions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "OpenCode plugin that loads GitHub Copilot custom instruction files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "vitest run",
|
|
25
|
-
"typecheck": "tsc --noEmit"
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"lint": "biome check",
|
|
27
|
+
"format": "biome check --write"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
30
|
"front-matter": "^4.0.2",
|
|
@@ -32,6 +34,7 @@
|
|
|
32
34
|
"@opencode/plugin": ">=2.0.0"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
37
|
+
"@biomejs/biome": "2.5.14",
|
|
35
38
|
"@opencode/plugin": "^2.0.15",
|
|
36
39
|
"@types/node": "^22.0.0",
|
|
37
40
|
"@types/picomatch": "^4.0.0",
|
package/src/frontmatter.ts
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const fm = (frontMatterModule as any).default ?? frontMatterModule
|
|
1
|
+
import fm from "front-matter";
|
|
4
2
|
|
|
5
3
|
export interface Frontmatter {
|
|
6
|
-
applyTo?: string | string[]
|
|
4
|
+
applyTo?: string | string[];
|
|
7
5
|
}
|
|
8
6
|
|
|
9
7
|
export interface ParsedFrontmatter {
|
|
10
|
-
frontmatter: Frontmatter
|
|
11
|
-
body: string
|
|
8
|
+
frontmatter: Frontmatter;
|
|
9
|
+
body: string;
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
export function parseFrontmatter(content: string): ParsedFrontmatter {
|
|
15
|
-
const normalized = content.replace(/\r\n/g, "\n")
|
|
13
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
16
14
|
|
|
17
15
|
if (!fm.test(normalized)) {
|
|
18
|
-
return { frontmatter: {}, body: content }
|
|
16
|
+
return { frontmatter: {}, body: content };
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
try {
|
|
22
|
-
const parsed = fm(normalized)
|
|
23
|
-
const attrs = parsed.attributes as Record<string, unknown
|
|
24
|
-
const result: Frontmatter = {}
|
|
20
|
+
const parsed = fm(normalized);
|
|
21
|
+
const attrs = parsed.attributes as Record<string, unknown>;
|
|
22
|
+
const result: Frontmatter = {};
|
|
25
23
|
|
|
26
24
|
if (attrs.applyTo !== undefined) {
|
|
27
25
|
if (typeof attrs.applyTo === "string") {
|
|
28
|
-
result.applyTo = attrs.applyTo
|
|
26
|
+
result.applyTo = attrs.applyTo;
|
|
29
27
|
} else if (Array.isArray(attrs.applyTo)) {
|
|
30
|
-
result.applyTo = attrs.applyTo.filter(
|
|
28
|
+
result.applyTo = attrs.applyTo.filter(
|
|
29
|
+
(item): item is string => typeof item === "string",
|
|
30
|
+
);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
return { frontmatter: result, body: parsed.body }
|
|
34
|
+
return { frontmatter: result, body: parsed.body };
|
|
35
35
|
} catch {
|
|
36
|
-
return { frontmatter: {}, body: content }
|
|
36
|
+
return { frontmatter: {}, body: content };
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,95 +1,141 @@
|
|
|
1
|
-
import * as path from "node:path"
|
|
2
|
-
import { Plugin } from "@opencode/plugin"
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { Plugin } from "@opencode/plugin";
|
|
3
|
+
import {
|
|
4
|
+
findRepoRoot,
|
|
5
|
+
loadPathInstructions,
|
|
6
|
+
loadRepoInstructions,
|
|
7
|
+
} from "./loader";
|
|
8
|
+
import { SessionState } from "./session-state";
|
|
9
|
+
|
|
10
|
+
const FILE_TOOLS = new Set(["read", "edit", "write"]);
|
|
11
|
+
|
|
12
|
+
/** Path relative to the repo root, or null when the file is outside it. */
|
|
13
|
+
function getRepoRelativePath(
|
|
14
|
+
repoRoot: string,
|
|
15
|
+
sessionDirectory: string,
|
|
16
|
+
filePath: string,
|
|
17
|
+
): string | null {
|
|
18
|
+
const relative = path.relative(
|
|
19
|
+
repoRoot,
|
|
20
|
+
path.resolve(sessionDirectory, filePath),
|
|
21
|
+
);
|
|
22
|
+
if (
|
|
23
|
+
relative === ".." ||
|
|
24
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
25
|
+
path.isAbsolute(relative)
|
|
26
|
+
)
|
|
27
|
+
return null;
|
|
28
|
+
return relative;
|
|
29
|
+
}
|
|
7
30
|
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
31
|
+
function appendInstructions<Content>(
|
|
32
|
+
result: { content?: string | ReadonlyArray<Content>; output?: unknown },
|
|
33
|
+
text: string,
|
|
34
|
+
): string | ReadonlyArray<Content | { type: "text"; text: string }> {
|
|
35
|
+
const { content, output } = result;
|
|
36
|
+
if (Array.isArray(content)) return [...content, { type: "text", text }];
|
|
37
|
+
// Without content, the model would otherwise see only the output, so keep it in the text.
|
|
38
|
+
const existing =
|
|
39
|
+
typeof content === "string"
|
|
40
|
+
? content
|
|
41
|
+
: output === undefined
|
|
42
|
+
? ""
|
|
43
|
+
: typeof output === "string"
|
|
44
|
+
? output
|
|
45
|
+
: JSON.stringify(output, null, 2);
|
|
46
|
+
return existing ? `${existing}\n\n${text}` : text;
|
|
12
47
|
}
|
|
13
48
|
|
|
14
49
|
export default Plugin.define({
|
|
15
50
|
id: "copilot-instructions",
|
|
16
51
|
async setup(ctx) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
const state = new SessionState();
|
|
53
|
+
|
|
54
|
+
const getSessionDirectory = async (
|
|
55
|
+
sessionID: string,
|
|
56
|
+
): Promise<string | null> => {
|
|
57
|
+
try {
|
|
58
|
+
const session = await ctx.session.get({ sessionID });
|
|
59
|
+
return session.location.directory;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const injectRepoInstructions = async (event: {
|
|
66
|
+
sessionID: string;
|
|
67
|
+
system: Array<{ type: string; text: string }>;
|
|
68
|
+
}) => {
|
|
69
|
+
const directory = await getSessionDirectory(event.sessionID);
|
|
70
|
+
if (!directory) return;
|
|
71
|
+
const repoInstructions = loadRepoInstructions(findRepoRoot(directory));
|
|
72
|
+
if (!repoInstructions) return;
|
|
35
73
|
event.system.push({
|
|
36
74
|
type: "text",
|
|
37
75
|
text: `<copilot-instruction:copilot-instructions.md>\n${repoInstructions.trimEnd()}\n</copilot-instruction:copilot-instructions.md>`,
|
|
38
|
-
})
|
|
39
|
-
}
|
|
76
|
+
});
|
|
77
|
+
};
|
|
40
78
|
|
|
41
79
|
// Inject repo-wide instructions into every model call so they survive compaction.
|
|
42
|
-
await ctx.session.hook("context", injectRepoInstructions)
|
|
43
|
-
await ctx.session.hook("compaction", injectRepoInstructions)
|
|
80
|
+
await ctx.session.hook("context", injectRepoInstructions);
|
|
81
|
+
await ctx.session.hook("compaction", injectRepoInstructions);
|
|
44
82
|
|
|
45
|
-
await ctx.tool.hook("execute.before", (event) => {
|
|
46
|
-
if (!FILE_TOOLS.has(event.tool)) return
|
|
83
|
+
await ctx.tool.hook("execute.before", async (event) => {
|
|
84
|
+
if (!FILE_TOOLS.has(event.tool)) return;
|
|
47
85
|
|
|
48
|
-
const input = event.input as { path?: unknown }
|
|
49
|
-
const filePath = input?.path
|
|
50
|
-
if (!filePath || typeof filePath !== "string") return
|
|
86
|
+
const input = event.input as { path?: unknown };
|
|
87
|
+
const filePath = input?.path;
|
|
88
|
+
if (!filePath || typeof filePath !== "string") return;
|
|
51
89
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (matching.length === 0) return
|
|
90
|
+
const directory = await getSessionDirectory(event.sessionID);
|
|
91
|
+
if (!directory) return;
|
|
92
|
+
const repoRoot = findRepoRoot(directory);
|
|
93
|
+
const relativePath = getRepoRelativePath(repoRoot, directory, filePath);
|
|
94
|
+
if (relativePath === null) return;
|
|
58
95
|
|
|
59
|
-
|
|
96
|
+
const matching = loadPathInstructions(repoRoot).filter((instruction) => {
|
|
97
|
+
if (state.isFileInjected(event.sessionID, instruction.file))
|
|
98
|
+
return false;
|
|
99
|
+
return instruction.matcher(relativePath);
|
|
100
|
+
});
|
|
101
|
+
if (matching.length === 0) return;
|
|
102
|
+
|
|
103
|
+
for (const instruction of matching)
|
|
104
|
+
state.markFileInjected(event.sessionID, instruction.file);
|
|
60
105
|
|
|
61
106
|
const text = matching
|
|
62
107
|
.map((instruction) => {
|
|
63
|
-
const filename = path.basename(instruction.file)
|
|
64
|
-
const patterns = instruction.applyTo.join(", ")
|
|
65
|
-
return `<copilot-instruction:${filename}>\n## Path-Specific Instructions (applies to: ${patterns})\n\n${instruction.content.trimEnd()}\n</copilot-instruction:${filename}
|
|
108
|
+
const filename = path.basename(instruction.file);
|
|
109
|
+
const patterns = instruction.applyTo.join(", ");
|
|
110
|
+
return `<copilot-instruction:${filename}>\n## Path-Specific Instructions (applies to: ${patterns})\n\n${instruction.content.trimEnd()}\n</copilot-instruction:${filename}>`;
|
|
66
111
|
})
|
|
67
|
-
.join("\n\n")
|
|
112
|
+
.join("\n\n");
|
|
68
113
|
|
|
69
|
-
state.setPending(event.id, text)
|
|
70
|
-
})
|
|
114
|
+
state.setPending(event.id, text);
|
|
115
|
+
});
|
|
71
116
|
|
|
72
117
|
await ctx.tool.hook("execute.after", (event) => {
|
|
73
|
-
const text = state.consumePending(event.id)
|
|
74
|
-
if (!text) return
|
|
75
|
-
if (event.status !== "completed") return
|
|
118
|
+
const text = state.consumePending(event.id);
|
|
119
|
+
if (!text) return;
|
|
120
|
+
if (event.status !== "completed") return;
|
|
76
121
|
|
|
77
|
-
const existing = typeof event.result.content === "string" ? event.result.content : ""
|
|
78
122
|
event.result = {
|
|
79
123
|
...event.result,
|
|
80
|
-
content:
|
|
81
|
-
}
|
|
82
|
-
})
|
|
124
|
+
content: appendInstructions(event.result, text),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
83
127
|
|
|
84
|
-
const controller = new AbortController()
|
|
128
|
+
const controller = new AbortController();
|
|
85
129
|
void (async () => {
|
|
86
|
-
for await (const event of ctx.event.subscribe({
|
|
130
|
+
for await (const event of ctx.event.subscribe({
|
|
131
|
+
signal: controller.signal,
|
|
132
|
+
})) {
|
|
87
133
|
if (event.type === "session.compaction.ended") {
|
|
88
|
-
state.clearSession(event.data.sessionID)
|
|
134
|
+
state.clearSession(event.data.sessionID);
|
|
89
135
|
}
|
|
90
136
|
}
|
|
91
|
-
})()
|
|
137
|
+
})();
|
|
92
138
|
|
|
93
|
-
return () => controller.abort()
|
|
139
|
+
return () => controller.abort();
|
|
94
140
|
},
|
|
95
|
-
})
|
|
141
|
+
});
|
package/src/loader.ts
CHANGED
|
@@ -1,57 +1,68 @@
|
|
|
1
|
-
import * as fs from "node:fs"
|
|
2
|
-
import * as path from "node:path"
|
|
3
|
-
import { parseFrontmatter } from "./frontmatter"
|
|
4
|
-
import { createMatcher,
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
4
|
+
import { createMatcher, type Matcher, normalizePatterns } from "./matcher";
|
|
5
5
|
|
|
6
6
|
export interface PathInstruction {
|
|
7
|
-
file: string
|
|
8
|
-
applyTo: string[]
|
|
9
|
-
content: string
|
|
10
|
-
matcher: Matcher
|
|
7
|
+
file: string;
|
|
8
|
+
applyTo: string[];
|
|
9
|
+
content: string;
|
|
10
|
+
matcher: Matcher;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Find the nearest ancestor containing `.git`, or return the directory itself. */
|
|
14
|
+
export function findRepoRoot(directory: string): string {
|
|
15
|
+
let current = path.resolve(directory);
|
|
16
|
+
while (true) {
|
|
17
|
+
if (fs.existsSync(path.join(current, ".git"))) return current;
|
|
18
|
+
const parent = path.dirname(current);
|
|
19
|
+
if (parent === current) return path.resolve(directory);
|
|
20
|
+
current = parent;
|
|
21
|
+
}
|
|
11
22
|
}
|
|
12
23
|
|
|
13
24
|
/** Load repo-wide instructions from .github/copilot-instructions.md */
|
|
14
25
|
export function loadRepoInstructions(directory: string): string | null {
|
|
15
|
-
const filePath = path.join(directory, ".github", "copilot-instructions.md")
|
|
26
|
+
const filePath = path.join(directory, ".github", "copilot-instructions.md");
|
|
16
27
|
try {
|
|
17
|
-
return fs.readFileSync(filePath, "utf-8")
|
|
28
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
18
29
|
} catch {
|
|
19
|
-
return null
|
|
30
|
+
return null;
|
|
20
31
|
}
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
/** Load path-specific instructions from .github/instructions/*.instructions.md */
|
|
24
35
|
export function loadPathInstructions(directory: string): PathInstruction[] {
|
|
25
|
-
const instructionsDir = path.join(directory, ".github", "instructions")
|
|
26
|
-
let files: string[]
|
|
36
|
+
const instructionsDir = path.join(directory, ".github", "instructions");
|
|
37
|
+
let files: string[];
|
|
27
38
|
try {
|
|
28
|
-
files = fs.readdirSync(instructionsDir)
|
|
39
|
+
files = fs.readdirSync(instructionsDir);
|
|
29
40
|
} catch {
|
|
30
|
-
return []
|
|
41
|
+
return [];
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
const result: PathInstruction[] = []
|
|
44
|
+
const result: PathInstruction[] = [];
|
|
34
45
|
for (const filename of files) {
|
|
35
|
-
if (!filename.endsWith(".instructions.md")) continue
|
|
46
|
+
if (!filename.endsWith(".instructions.md")) continue;
|
|
36
47
|
|
|
37
|
-
const filePath = path.join(instructionsDir, filename)
|
|
38
|
-
let content: string
|
|
48
|
+
const filePath = path.join(instructionsDir, filename);
|
|
49
|
+
let content: string;
|
|
39
50
|
try {
|
|
40
|
-
content = fs.readFileSync(filePath, "utf-8")
|
|
51
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
41
52
|
} catch {
|
|
42
|
-
continue
|
|
53
|
+
continue;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
|
-
const parsed = parseFrontmatter(content)
|
|
46
|
-
const patterns = normalizePatterns(parsed.frontmatter.applyTo)
|
|
47
|
-
if (patterns.length === 0) continue
|
|
56
|
+
const parsed = parseFrontmatter(content);
|
|
57
|
+
const patterns = normalizePatterns(parsed.frontmatter.applyTo);
|
|
58
|
+
if (patterns.length === 0) continue;
|
|
48
59
|
|
|
49
60
|
result.push({
|
|
50
61
|
file: filePath,
|
|
51
62
|
applyTo: patterns,
|
|
52
63
|
content: parsed.body,
|
|
53
64
|
matcher: createMatcher(patterns),
|
|
54
|
-
})
|
|
65
|
+
});
|
|
55
66
|
}
|
|
56
|
-
return result
|
|
67
|
+
return result;
|
|
57
68
|
}
|
package/src/matcher.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import picomatch from "picomatch"
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
2
|
|
|
3
|
-
export type Matcher = (path: string) => boolean
|
|
3
|
+
export type Matcher = (path: string) => boolean;
|
|
4
4
|
|
|
5
5
|
export function createMatcher(patterns: string[]): Matcher {
|
|
6
|
-
if (patterns.length === 0) return () => false
|
|
7
|
-
const isMatch = picomatch(patterns)
|
|
8
|
-
return (path) => isMatch(path)
|
|
6
|
+
if (patterns.length === 0) return () => false;
|
|
7
|
+
const isMatch = picomatch(patterns);
|
|
8
|
+
return (path) => isMatch(path);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function normalizePatterns(
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
export function normalizePatterns(
|
|
12
|
+
applyTo: string | string[] | undefined,
|
|
13
|
+
): string[] {
|
|
14
|
+
if (applyTo === undefined) return [];
|
|
15
|
+
if (Array.isArray(applyTo)) return applyTo;
|
|
14
16
|
return applyTo
|
|
15
17
|
.split(",")
|
|
16
18
|
.map((pattern) => pattern.trim())
|
|
17
|
-
.filter((pattern) => pattern.length > 0)
|
|
19
|
+
.filter((pattern) => pattern.length > 0);
|
|
18
20
|
}
|
package/src/session-state.ts
CHANGED
|
@@ -5,33 +5,33 @@
|
|
|
5
5
|
* pending instructions keyed by tool call ID.
|
|
6
6
|
*/
|
|
7
7
|
export class SessionState {
|
|
8
|
-
private injectedPerSession = new Map<string, Set<string>>()
|
|
9
|
-
private pendingInstructions = new Map<string, string>()
|
|
8
|
+
private injectedPerSession = new Map<string, Set<string>>();
|
|
9
|
+
private pendingInstructions = new Map<string, string>();
|
|
10
10
|
|
|
11
11
|
isFileInjected(sessionId: string, file: string): boolean {
|
|
12
|
-
return this.injectedPerSession.get(sessionId)?.has(file) ?? false
|
|
12
|
+
return this.injectedPerSession.get(sessionId)?.has(file) ?? false;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
markFileInjected(sessionId: string, file: string): void {
|
|
16
|
-
let files = this.injectedPerSession.get(sessionId)
|
|
16
|
+
let files = this.injectedPerSession.get(sessionId);
|
|
17
17
|
if (!files) {
|
|
18
|
-
files = new Set()
|
|
19
|
-
this.injectedPerSession.set(sessionId, files)
|
|
18
|
+
files = new Set();
|
|
19
|
+
this.injectedPerSession.set(sessionId, files);
|
|
20
20
|
}
|
|
21
|
-
files.add(file)
|
|
21
|
+
files.add(file);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
clearSession(sessionId: string): void {
|
|
25
|
-
this.injectedPerSession.delete(sessionId)
|
|
25
|
+
this.injectedPerSession.delete(sessionId);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
setPending(callId: string, text: string): void {
|
|
29
|
-
this.pendingInstructions.set(callId, text)
|
|
29
|
+
this.pendingInstructions.set(callId, text);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
consumePending(callId: string): string | undefined {
|
|
33
|
-
const text = this.pendingInstructions.get(callId)
|
|
34
|
-
this.pendingInstructions.delete(callId)
|
|
35
|
-
return text
|
|
33
|
+
const text = this.pendingInstructions.get(callId);
|
|
34
|
+
this.pendingInstructions.delete(callId);
|
|
35
|
+
return text;
|
|
36
36
|
}
|
|
37
37
|
}
|