@ksm0709/context 0.0.18 → 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/README.md +4 -0
- package/dist/cli/index.js +169 -37
- package/dist/index.js +98 -62
- package/dist/omx/index.mjs +780 -0
- package/package.json +8 -4
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
// src/omx/index.ts
|
|
2
|
+
import { join as join5 } from "node:path";
|
|
3
|
+
|
|
4
|
+
// src/constants.ts
|
|
5
|
+
var DEFAULTS = {
|
|
6
|
+
configPath: ".context/config.jsonc",
|
|
7
|
+
promptDir: ".context/prompts",
|
|
8
|
+
turnStartFile: "turn-start.md",
|
|
9
|
+
turnEndFile: "turn-end.md",
|
|
10
|
+
knowledgeSources: ["AGENTS.md"],
|
|
11
|
+
templateDir: ".context/templates",
|
|
12
|
+
indexFilename: "INDEX.md",
|
|
13
|
+
maxDomainDepth: 2,
|
|
14
|
+
knowledgeDir: "docs"
|
|
15
|
+
};
|
|
16
|
+
var LIMITS = {
|
|
17
|
+
maxPromptFileSize: 64 * 1024,
|
|
18
|
+
maxIndexEntries: 100,
|
|
19
|
+
maxTotalInjectionSize: 128 * 1024,
|
|
20
|
+
maxScanDepth: 3,
|
|
21
|
+
maxSummaryLength: 100,
|
|
22
|
+
maxIndexFileSize: 32 * 1024
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/lib/config.ts
|
|
26
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
|
+
import { join as join2 } from "node:path";
|
|
29
|
+
|
|
30
|
+
// src/lib/context-dir.ts
|
|
31
|
+
import { existsSync } from "node:fs";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
function resolveContextDir(projectDir) {
|
|
34
|
+
const nextContextDir = ".context";
|
|
35
|
+
if (existsSync(join(projectDir, nextContextDir))) {
|
|
36
|
+
return nextContextDir;
|
|
37
|
+
}
|
|
38
|
+
const legacyContextDir = ".opencode/context";
|
|
39
|
+
if (existsSync(join(projectDir, legacyContextDir))) {
|
|
40
|
+
return legacyContextDir;
|
|
41
|
+
}
|
|
42
|
+
return nextContextDir;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/lib/config.ts
|
|
46
|
+
function getDefaultConfig() {
|
|
47
|
+
return {
|
|
48
|
+
prompts: {
|
|
49
|
+
turnStart: join2(DEFAULTS.promptDir, DEFAULTS.turnStartFile),
|
|
50
|
+
turnEnd: join2(DEFAULTS.promptDir, DEFAULTS.turnEndFile)
|
|
51
|
+
},
|
|
52
|
+
knowledge: {
|
|
53
|
+
dir: "docs",
|
|
54
|
+
sources: [...DEFAULTS.knowledgeSources],
|
|
55
|
+
mode: "auto",
|
|
56
|
+
indexFilename: DEFAULTS.indexFilename,
|
|
57
|
+
maxDomainDepth: DEFAULTS.maxDomainDepth
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function mergeWithDefaults(partial) {
|
|
62
|
+
const defaults = getDefaultConfig();
|
|
63
|
+
return {
|
|
64
|
+
prompts: {
|
|
65
|
+
turnStart: partial.prompts?.turnStart ?? defaults.prompts.turnStart,
|
|
66
|
+
turnEnd: partial.prompts?.turnEnd ?? defaults.prompts.turnEnd
|
|
67
|
+
},
|
|
68
|
+
knowledge: {
|
|
69
|
+
dir: partial.knowledge?.dir ?? defaults.knowledge.dir,
|
|
70
|
+
sources: partial.knowledge?.sources ?? defaults.knowledge.sources,
|
|
71
|
+
mode: partial.knowledge?.mode ?? defaults.knowledge.mode,
|
|
72
|
+
indexFilename: partial.knowledge?.indexFilename ?? defaults.knowledge.indexFilename,
|
|
73
|
+
maxDomainDepth: partial.knowledge?.maxDomainDepth ?? defaults.knowledge.maxDomainDepth
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function loadConfig(projectDir) {
|
|
78
|
+
const configPath = join2(projectDir, resolveContextDir(projectDir), "config.jsonc");
|
|
79
|
+
try {
|
|
80
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
81
|
+
const parsed = parseJsonc(raw);
|
|
82
|
+
if (!parsed || typeof parsed !== "object")
|
|
83
|
+
return getDefaultConfig();
|
|
84
|
+
return mergeWithDefaults(parsed);
|
|
85
|
+
} catch {
|
|
86
|
+
return getDefaultConfig();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/lib/knowledge-index.ts
|
|
91
|
+
import { readdirSync, readFileSync as readFileSync2, statSync, existsSync as existsSync2 } from "node:fs";
|
|
92
|
+
import { join as join3, relative, extname } from "node:path";
|
|
93
|
+
function extractSummary(filePath) {
|
|
94
|
+
try {
|
|
95
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
96
|
+
const firstNonEmpty = content.split(`
|
|
97
|
+
`).find((line) => line.trim().length > 0);
|
|
98
|
+
if (!firstNonEmpty)
|
|
99
|
+
return "";
|
|
100
|
+
return firstNonEmpty.trim().slice(0, LIMITS.maxSummaryLength);
|
|
101
|
+
} catch {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function scanDir(dir, projectDir, depth, entries) {
|
|
106
|
+
if (depth > LIMITS.maxScanDepth)
|
|
107
|
+
return;
|
|
108
|
+
if (entries.length >= LIMITS.maxIndexEntries)
|
|
109
|
+
return;
|
|
110
|
+
try {
|
|
111
|
+
const items = readdirSync(dir);
|
|
112
|
+
for (const item of items) {
|
|
113
|
+
if (entries.length >= LIMITS.maxIndexEntries)
|
|
114
|
+
break;
|
|
115
|
+
const fullPath = join3(dir, item);
|
|
116
|
+
try {
|
|
117
|
+
const stat = statSync(fullPath);
|
|
118
|
+
if (stat.isDirectory()) {
|
|
119
|
+
scanDir(fullPath, projectDir, depth + 1, entries);
|
|
120
|
+
} else if (stat.isFile() && extname(item) === ".md") {
|
|
121
|
+
entries.push({
|
|
122
|
+
filename: relative(projectDir, fullPath),
|
|
123
|
+
summary: extractSummary(fullPath)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
function buildKnowledgeIndex(projectDir, sources) {
|
|
131
|
+
const entries = [];
|
|
132
|
+
for (const source of sources) {
|
|
133
|
+
if (entries.length >= LIMITS.maxIndexEntries)
|
|
134
|
+
break;
|
|
135
|
+
const fullPath = join3(projectDir, source);
|
|
136
|
+
if (!existsSync2(fullPath))
|
|
137
|
+
continue;
|
|
138
|
+
try {
|
|
139
|
+
const stat = statSync(fullPath);
|
|
140
|
+
if (stat.isFile() && extname(source) === ".md") {
|
|
141
|
+
entries.push({
|
|
142
|
+
filename: source,
|
|
143
|
+
summary: extractSummary(fullPath)
|
|
144
|
+
});
|
|
145
|
+
} else if (stat.isDirectory()) {
|
|
146
|
+
scanDir(fullPath, projectDir, 1, entries);
|
|
147
|
+
}
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
150
|
+
return entries;
|
|
151
|
+
}
|
|
152
|
+
function formatKnowledgeIndex(entries) {
|
|
153
|
+
if (entries.length === 0)
|
|
154
|
+
return "";
|
|
155
|
+
const lines = ["## Available Knowledge", ""];
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
lines.push(`- ${entry.filename}${entry.summary ? ` — ${entry.summary}` : ""}`);
|
|
158
|
+
}
|
|
159
|
+
return lines.join(`
|
|
160
|
+
`);
|
|
161
|
+
}
|
|
162
|
+
function countMdFiles(dir, indexFilename) {
|
|
163
|
+
try {
|
|
164
|
+
const items = readdirSync(dir);
|
|
165
|
+
return items.filter((item) => extname(item) === ".md" && item !== indexFilename && statSync(join3(dir, item)).isFile()).length;
|
|
166
|
+
} catch {
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function scanDomainsRecursive(baseDir, projectDir, indexFilename, currentDepth, maxDepth, results) {
|
|
171
|
+
if (currentDepth > maxDepth)
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
const items = readdirSync(baseDir);
|
|
175
|
+
for (const item of items) {
|
|
176
|
+
const fullPath = join3(baseDir, item);
|
|
177
|
+
try {
|
|
178
|
+
if (!statSync(fullPath).isDirectory())
|
|
179
|
+
continue;
|
|
180
|
+
const indexPath = join3(fullPath, indexFilename);
|
|
181
|
+
if (existsSync2(indexPath) && statSync(indexPath).isFile()) {
|
|
182
|
+
const rawContent = readFileSync2(indexPath, "utf-8");
|
|
183
|
+
const indexContent = rawContent.slice(0, LIMITS.maxIndexFileSize);
|
|
184
|
+
results.push({
|
|
185
|
+
domain: item,
|
|
186
|
+
path: relative(projectDir, fullPath),
|
|
187
|
+
indexContent,
|
|
188
|
+
noteCount: countMdFiles(fullPath, indexFilename)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
scanDomainsRecursive(fullPath, projectDir, indexFilename, currentDepth + 1, maxDepth, results);
|
|
192
|
+
} catch {}
|
|
193
|
+
}
|
|
194
|
+
} catch {}
|
|
195
|
+
}
|
|
196
|
+
function scanDomains(projectDir, knowledgeDir, indexFilename, maxDepth) {
|
|
197
|
+
const baseDir = join3(projectDir, knowledgeDir);
|
|
198
|
+
if (!existsSync2(baseDir))
|
|
199
|
+
return [];
|
|
200
|
+
const results = [];
|
|
201
|
+
scanDomainsRecursive(baseDir, projectDir, indexFilename, 1, maxDepth, results);
|
|
202
|
+
return results;
|
|
203
|
+
}
|
|
204
|
+
function detectKnowledgeMode(projectDir, knowledgeDir, indexFilename, configMode) {
|
|
205
|
+
if (configMode !== "auto")
|
|
206
|
+
return configMode;
|
|
207
|
+
const domains = scanDomains(projectDir, knowledgeDir, indexFilename, 1);
|
|
208
|
+
return domains.length > 0 ? "domain" : "flat";
|
|
209
|
+
}
|
|
210
|
+
function formatDomainIndex(index) {
|
|
211
|
+
const hasDomains = index.domains.length > 0;
|
|
212
|
+
const hasFiles = index.individualFiles.length > 0;
|
|
213
|
+
if (!hasDomains && !hasFiles)
|
|
214
|
+
return "";
|
|
215
|
+
const lines = ["## Available Knowledge", ""];
|
|
216
|
+
if (hasDomains) {
|
|
217
|
+
lines.push("### Domains", "");
|
|
218
|
+
for (const domain of index.domains) {
|
|
219
|
+
lines.push(`#### ${domain.path}/ (${domain.noteCount} notes)`, "");
|
|
220
|
+
lines.push(domain.indexContent, "");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (hasFiles) {
|
|
224
|
+
if (hasDomains) {
|
|
225
|
+
lines.push("### Individual Files", "");
|
|
226
|
+
}
|
|
227
|
+
for (const file of index.individualFiles) {
|
|
228
|
+
lines.push(`- ${file.filename}${file.summary ? ` — ${file.summary}` : ""}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return lines.join(`
|
|
232
|
+
`);
|
|
233
|
+
}
|
|
234
|
+
function collectRootFiles(projectDir, knowledgeDir, indexFilename) {
|
|
235
|
+
const baseDir = join3(projectDir, knowledgeDir);
|
|
236
|
+
if (!existsSync2(baseDir))
|
|
237
|
+
return [];
|
|
238
|
+
const entries = [];
|
|
239
|
+
try {
|
|
240
|
+
const items = readdirSync(baseDir);
|
|
241
|
+
for (const item of items) {
|
|
242
|
+
const fullPath = join3(baseDir, item);
|
|
243
|
+
try {
|
|
244
|
+
const stat = statSync(fullPath);
|
|
245
|
+
if (stat.isFile() && extname(item) === ".md" && item !== indexFilename) {
|
|
246
|
+
entries.push({
|
|
247
|
+
filename: relative(projectDir, fullPath),
|
|
248
|
+
summary: extractSummary(fullPath)
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
return entries;
|
|
255
|
+
}
|
|
256
|
+
function buildKnowledgeIndexV2(projectDir, knowledgeConfig) {
|
|
257
|
+
const dir = knowledgeConfig.dir ?? "docs";
|
|
258
|
+
const indexFilename = knowledgeConfig.indexFilename ?? "INDEX.md";
|
|
259
|
+
const maxDepth = knowledgeConfig.maxDomainDepth ?? 2;
|
|
260
|
+
const configMode = knowledgeConfig.mode ?? "auto";
|
|
261
|
+
const mode = detectKnowledgeMode(projectDir, dir, indexFilename, configMode);
|
|
262
|
+
if (mode === "flat") {
|
|
263
|
+
const allSources = [dir, ...knowledgeConfig.sources].filter(Boolean);
|
|
264
|
+
const entries = buildKnowledgeIndex(projectDir, allSources);
|
|
265
|
+
return { mode: "flat", domains: [], individualFiles: entries };
|
|
266
|
+
}
|
|
267
|
+
const domains = scanDomains(projectDir, dir, indexFilename, maxDepth);
|
|
268
|
+
const rootFiles = collectRootFiles(projectDir, dir, indexFilename);
|
|
269
|
+
const sourcesEntries = [];
|
|
270
|
+
for (const source of knowledgeConfig.sources) {
|
|
271
|
+
const fullPath = join3(projectDir, source);
|
|
272
|
+
if (!existsSync2(fullPath))
|
|
273
|
+
continue;
|
|
274
|
+
try {
|
|
275
|
+
const stat = statSync(fullPath);
|
|
276
|
+
if (stat.isFile() && extname(source) === ".md") {
|
|
277
|
+
sourcesEntries.push({
|
|
278
|
+
filename: source,
|
|
279
|
+
summary: extractSummary(fullPath)
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
284
|
+
const individualFiles = [...rootFiles, ...sourcesEntries];
|
|
285
|
+
return { mode: "domain", domains, individualFiles };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/lib/prompt-reader.ts
|
|
289
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
290
|
+
function readPromptFile(filePath) {
|
|
291
|
+
try {
|
|
292
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
293
|
+
if (content.length > LIMITS.maxPromptFileSize) {
|
|
294
|
+
return content.slice(0, LIMITS.maxPromptFileSize);
|
|
295
|
+
}
|
|
296
|
+
return content;
|
|
297
|
+
} catch {
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function resolvePromptVariables(content, vars) {
|
|
302
|
+
const normalized = (vars.knowledgeDir || "docs").replace(/\\/g, "/").replace(/\/+$/, "");
|
|
303
|
+
return content.replaceAll("{{knowledgeDir}}", normalized);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/lib/scaffold.ts
|
|
307
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
308
|
+
import { join as join4 } from "node:path";
|
|
309
|
+
// package.json
|
|
310
|
+
var package_default = {
|
|
311
|
+
name: "@ksm0709/context",
|
|
312
|
+
version: "0.0.19",
|
|
313
|
+
author: {
|
|
314
|
+
name: "TaehoKang",
|
|
315
|
+
email: "ksm07091@gmail.com"
|
|
316
|
+
},
|
|
317
|
+
type: "module",
|
|
318
|
+
main: "./dist/index.js",
|
|
319
|
+
bin: {
|
|
320
|
+
context: "./dist/cli/index.js"
|
|
321
|
+
},
|
|
322
|
+
exports: {
|
|
323
|
+
".": {
|
|
324
|
+
import: "./dist/index.js",
|
|
325
|
+
types: "./dist/index.d.ts",
|
|
326
|
+
default: "./dist/index.js"
|
|
327
|
+
},
|
|
328
|
+
"./omx": {
|
|
329
|
+
import: "./dist/omx/index.mjs",
|
|
330
|
+
types: "./dist/omx/index.d.ts",
|
|
331
|
+
default: "./dist/omx/index.mjs"
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
repository: {
|
|
335
|
+
type: "git",
|
|
336
|
+
url: "git@github.com:ksm0709/context.git"
|
|
337
|
+
},
|
|
338
|
+
publishConfig: {
|
|
339
|
+
access: "public"
|
|
340
|
+
},
|
|
341
|
+
scripts: {
|
|
342
|
+
build: "bun build ./src/index.ts --outdir dist --target bun && bun build ./src/cli/index.ts --outdir dist/cli --target bun && bun build ./src/omx/index.ts --outdir dist/omx --target node --format esm --external jsonc-parser && mv dist/omx/index.js dist/omx/index.mjs",
|
|
343
|
+
test: "vitest run",
|
|
344
|
+
lint: "eslint src --ext .ts",
|
|
345
|
+
prepublishOnly: "bun build ./src/index.ts --outdir dist --target bun && bun build ./src/cli/index.ts --outdir dist/cli --target bun && bun build ./src/omx/index.ts --outdir dist/omx --target node --format esm --external jsonc-parser && mv dist/omx/index.js dist/omx/index.mjs"
|
|
346
|
+
},
|
|
347
|
+
files: [
|
|
348
|
+
"dist"
|
|
349
|
+
],
|
|
350
|
+
peerDependencies: {
|
|
351
|
+
"@opencode-ai/plugin": ">=1.0.0"
|
|
352
|
+
},
|
|
353
|
+
dependencies: {
|
|
354
|
+
"jsonc-parser": "^3.0.0"
|
|
355
|
+
},
|
|
356
|
+
devDependencies: {
|
|
357
|
+
"@opencode-ai/plugin": "^1.2.10",
|
|
358
|
+
"@eslint/js": "^9.39.1",
|
|
359
|
+
"@types/node": "^20.11.5",
|
|
360
|
+
"@typescript-eslint/eslint-plugin": "8.47.0",
|
|
361
|
+
"@typescript-eslint/parser": "8.47.0",
|
|
362
|
+
"bun-types": "latest",
|
|
363
|
+
eslint: "^9.39.1",
|
|
364
|
+
"eslint-config-prettier": "10.1.8",
|
|
365
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
366
|
+
prettier: "^3.2.4",
|
|
367
|
+
"typescript-eslint": "^8.47.0",
|
|
368
|
+
vitest: "^3.2.4",
|
|
369
|
+
"@vitest/coverage-v8": "^3.2.4"
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// src/lib/scaffold.ts
|
|
374
|
+
var PLUGIN_VERSION = package_default.version;
|
|
375
|
+
var DEFAULT_CONFIG = `{
|
|
376
|
+
// Context Plugin Configuration
|
|
377
|
+
// See: https://github.com/ksm0709/context
|
|
378
|
+
"prompts": {
|
|
379
|
+
"turnStart": "prompts/turn-start.md",
|
|
380
|
+
"turnEnd": "prompts/turn-end.md"
|
|
381
|
+
},
|
|
382
|
+
"knowledge": {
|
|
383
|
+
"dir": "docs",
|
|
384
|
+
"sources": ["AGENTS.md"]
|
|
385
|
+
}
|
|
386
|
+
}`;
|
|
387
|
+
var DEFAULT_TURN_START = `## Knowledge Context
|
|
388
|
+
|
|
389
|
+
이 프로젝트는 **제텔카스텐(Zettelkasten)** 방식으로 지식을 관리합니다.
|
|
390
|
+
세션 간 컨텍스트를 보존하여, 이전 세션의 결정/패턴/실수가 다음 세션에서 재활용됩니다.
|
|
391
|
+
|
|
392
|
+
### 제텔카스텐 핵심 원칙
|
|
393
|
+
|
|
394
|
+
1. **원자성** -- 하나의 노트 = 하나의 주제. 여러 주제를 섞지 마세요.
|
|
395
|
+
2. **연결** -- 모든 노트는 [[wikilink]]로 관련 노트에 연결. 고립된 노트는 발견되지 않습니다.
|
|
396
|
+
3. **자기 언어** -- 복사-붙여넣기가 아닌, 핵심을 이해하고 간결하게 서술하세요.
|
|
397
|
+
|
|
398
|
+
### 작업 전 필수
|
|
399
|
+
|
|
400
|
+
- 메인 에이전트가 아래 **Available Knowledge** 목록에서 현재 작업과 관련된 문서를 **직접 먼저** 읽으세요
|
|
401
|
+
- 도메인 폴더 구조가 있다면 INDEX.md의 요약을 참고하여 필요한 노트만 선택적으로 읽으세요
|
|
402
|
+
- 문서 내 [[링크]]를 따라가며 관련 노트를 탐색하세요 -- 링크를 놓치면 중요한 맥락을 잃습니다
|
|
403
|
+
- 지식 파일에 기록된 아키텍처 결정, 패턴, 제약사항을 반드시 따르세요
|
|
404
|
+
- 읽은 지식을 현재 작업의 설계, 구현, 검증에 직접 반영하세요
|
|
405
|
+
|
|
406
|
+
### 개발 원칙
|
|
407
|
+
|
|
408
|
+
- **TDD** (Test-Driven Development): 테스트를 먼저 작성하고(RED), 구현하여 통과시킨 뒤(GREEN), 리팩토링하세요
|
|
409
|
+
- **DDD** (Domain-Driven Design): 도메인 개념을 코드 구조에 반영하세요. 타입과 모듈은 비즈니스 도메인을 기준으로 분리하세요
|
|
410
|
+
- **테스트 커버리지**: 새로 작성하거나 변경한 코드는 테스트 커버리지 80% 이상을 목표로 하세요. 구현 전에 테스트부터 작성하면 자연스럽게 달성됩니다
|
|
411
|
+
|
|
412
|
+
### 우선순위
|
|
413
|
+
|
|
414
|
+
- AGENTS.md의 지시사항이 항상 최우선
|
|
415
|
+
- 지식 노트의 결정사항 > 일반적 관행
|
|
416
|
+
- 지식 노트에 없는 새로운 결정이나 반복 가치가 있는 발견은 작업 메모나 지식 노트 후보로 기록하세요
|
|
417
|
+
`;
|
|
418
|
+
var DEFAULT_TURN_END = `## 작업 마무리
|
|
419
|
+
|
|
420
|
+
작업이 완료되면 아래 항목을 메인 에이전트가 직접 확인하세요.
|
|
421
|
+
|
|
422
|
+
### 1. 퀄리티 체크
|
|
423
|
+
|
|
424
|
+
- 변경한 코드에 대해 필요한 lint, format, test, build 검증을 직접 실행하세요
|
|
425
|
+
- 새로 작성하거나 변경한 코드의 커버리지 기대치를 확인하세요
|
|
426
|
+
- 변경 범위를 검토하여 요청과 무관한 파일을 건드리지 않았는지 확인하세요
|
|
427
|
+
- 실패 항목이 있으면 원인, 에러 메시지, 관련 파일 위치를 정리한 뒤 직접 수정하세요
|
|
428
|
+
- 작업이 끝났다고 판단하기 전에 위 검증 결과를 직접 다시 확인하세요
|
|
429
|
+
|
|
430
|
+
### 2. 지식 정리
|
|
431
|
+
|
|
432
|
+
작업 중 기록할 만한 발견이 있었다면 직접 정리하세요.
|
|
433
|
+
|
|
434
|
+
**기록 대상 판단 기준:**
|
|
435
|
+
|
|
436
|
+
| 상황 | 템플릿 | 파일명 패턴 |
|
|
437
|
+
| ------------------------------- | --------------------------------------------------- | --------------------------- |
|
|
438
|
+
| 아키텍처/기술 스택 중대 결정 | [ADR](.context/templates/adr.md) | \`adr-NNN-제목.md\` |
|
|
439
|
+
| 반복 사용할 코드 패턴 발견 | [Pattern](.context/templates/pattern.md) | \`pattern-제목.md\` |
|
|
440
|
+
| 비자명한 버그 해결 | [Bug](.context/templates/bug.md) | \`bug-제목.md\` |
|
|
441
|
+
| 외부 API/라이브러리 예상외 동작 | [Gotcha](.context/templates/gotcha.md) | \`gotcha-라이브러리-제목.md\` |
|
|
442
|
+
| 작은 기술적 선택 | [Decision](.context/templates/decision.md) | \`decision-제목.md\` |
|
|
443
|
+
| 모듈/프로젝트 개요 필요 | [Context](.context/templates/context.md) | \`context-제목.md\` |
|
|
444
|
+
| 반복 가능한 프로세스 정립 | [Runbook](.context/templates/runbook.md) | \`runbook-제목.md\` |
|
|
445
|
+
| 실험/디버깅 중 학습 | [Insight](.context/templates/insight.md) | \`insight-제목.md\` |
|
|
446
|
+
|
|
447
|
+
해당 사항이 없으면 이 단계는 건너뛰세요.
|
|
448
|
+
|
|
449
|
+
- 관련 템플릿 파일을 읽고 그 구조에 맞춰 내용을 정리하세요
|
|
450
|
+
- 노트 첫 줄은 명확한 제목(\`# Title\`)으로 시작하세요
|
|
451
|
+
- 핵심 내용을 자기 언어로 간결하게 서술하고, 관련 노트는 \`[[relative/path/file.md]]\` 형태로 연결하세요
|
|
452
|
+
- knowledge 디렉토리(\`{{knowledgeDir}}/\`) 또는 적절한 도메인 폴더에 저장하고, 필요한 경우 기존 INDEX.md나 관련 노트를 함께 갱신하세요
|
|
453
|
+
|
|
454
|
+
기존 설치의 사용자 프롬프트 파일은 자동으로 바뀌지 않습니다. 새 기본 프롬프트가 필요하면 \`context update prompt\`로 명시적으로 새로고침하세요.
|
|
455
|
+
`;
|
|
456
|
+
var DEFAULT_ADR_TEMPLATE = `# ADR-NNN: [제목]
|
|
457
|
+
|
|
458
|
+
## 상태
|
|
459
|
+
|
|
460
|
+
Accepted | Deprecated | Superseded by [[ADR-YYY]]
|
|
461
|
+
|
|
462
|
+
## 맥락
|
|
463
|
+
|
|
464
|
+
이 결정을 내리게 된 배경/문제 상황
|
|
465
|
+
|
|
466
|
+
## 결정
|
|
467
|
+
|
|
468
|
+
무엇을 어떻게 하기로 했는지
|
|
469
|
+
|
|
470
|
+
## 결과
|
|
471
|
+
|
|
472
|
+
### 긍정적
|
|
473
|
+
|
|
474
|
+
- ...
|
|
475
|
+
|
|
476
|
+
### 부정적 (트레이드오프)
|
|
477
|
+
|
|
478
|
+
- ...
|
|
479
|
+
|
|
480
|
+
## 관련 노트
|
|
481
|
+
|
|
482
|
+
- [[관련-결정.md]] / [[관련-패턴.md]]
|
|
483
|
+
`;
|
|
484
|
+
var DEFAULT_PATTERN_TEMPLATE = `# Pattern: [패턴 이름]
|
|
485
|
+
|
|
486
|
+
## 문제
|
|
487
|
+
|
|
488
|
+
이 패턴이 해결하는 문제
|
|
489
|
+
|
|
490
|
+
## 해법
|
|
491
|
+
|
|
492
|
+
// 패턴의 대표적 예시 코드
|
|
493
|
+
|
|
494
|
+
## 사용 시점
|
|
495
|
+
|
|
496
|
+
- 이럴 때 사용
|
|
497
|
+
|
|
498
|
+
## 사용하지 말 것
|
|
499
|
+
|
|
500
|
+
- 이럴 때는 사용 금지 (안티패턴 경고)
|
|
501
|
+
|
|
502
|
+
## 코드베이스 내 예시
|
|
503
|
+
|
|
504
|
+
- [[경로/파일.ts]] -- 실제 적용 사례
|
|
505
|
+
|
|
506
|
+
## 관련 패턴
|
|
507
|
+
|
|
508
|
+
- [[대안-패턴.md]] / [[보완-패턴.md]]
|
|
509
|
+
`;
|
|
510
|
+
var DEFAULT_BUG_TEMPLATE = `# Bug: [간단한 설명]
|
|
511
|
+
|
|
512
|
+
## 증상
|
|
513
|
+
|
|
514
|
+
- 에러 메시지: \`...\`
|
|
515
|
+
- 관찰된 동작: ...
|
|
516
|
+
|
|
517
|
+
## 원인
|
|
518
|
+
|
|
519
|
+
실제 원인 분석
|
|
520
|
+
|
|
521
|
+
## 해결
|
|
522
|
+
|
|
523
|
+
// 수정 코드
|
|
524
|
+
|
|
525
|
+
## 예방
|
|
526
|
+
|
|
527
|
+
향후 같은 문제를 방지하는 방법
|
|
528
|
+
|
|
529
|
+
## 관련 노트
|
|
530
|
+
|
|
531
|
+
- [[유사-버그.md]] / [[예방-패턴.md]]
|
|
532
|
+
`;
|
|
533
|
+
var DEFAULT_GOTCHA_TEMPLATE = `# Gotcha: [라이브러리] -- [함정 설명]
|
|
534
|
+
|
|
535
|
+
## 예상 vs 실제
|
|
536
|
+
|
|
537
|
+
예상한 동작과 실제 동작의 차이
|
|
538
|
+
|
|
539
|
+
## 우회법
|
|
540
|
+
|
|
541
|
+
// 작동하는 해결 코드
|
|
542
|
+
|
|
543
|
+
## 원인 (알려진 경우)
|
|
544
|
+
|
|
545
|
+
왜 이렇게 동작하는지
|
|
546
|
+
|
|
547
|
+
## 관련
|
|
548
|
+
|
|
549
|
+
- 이슈: [GitHub issue / 문서 링크]
|
|
550
|
+
- [[관련-gotcha.md]]
|
|
551
|
+
`;
|
|
552
|
+
var DEFAULT_DECISION_TEMPLATE = `# Decision: [제목]
|
|
553
|
+
|
|
554
|
+
## 결정
|
|
555
|
+
|
|
556
|
+
무엇을 선택했는지
|
|
557
|
+
|
|
558
|
+
## 근거
|
|
559
|
+
|
|
560
|
+
왜 이것을 선택했는지
|
|
561
|
+
|
|
562
|
+
## 고려한 대안
|
|
563
|
+
|
|
564
|
+
- 대안 1: 탈락 이유
|
|
565
|
+
- 대안 2: 탈락 이유
|
|
566
|
+
|
|
567
|
+
## 관련 노트
|
|
568
|
+
|
|
569
|
+
- [[관련-ADR.md]] / [[관련-패턴.md]]
|
|
570
|
+
`;
|
|
571
|
+
var DEFAULT_CONTEXT_TEMPLATE = `# Context: [프로젝트/모듈명]
|
|
572
|
+
|
|
573
|
+
## 개요
|
|
574
|
+
|
|
575
|
+
무엇이고 무엇을 하는지
|
|
576
|
+
|
|
577
|
+
## 기술 스택
|
|
578
|
+
|
|
579
|
+
- 언어 / 프레임워크 / 주요 라이브러리
|
|
580
|
+
|
|
581
|
+
## 아키텍처
|
|
582
|
+
|
|
583
|
+
고수준 구조와 패턴
|
|
584
|
+
|
|
585
|
+
## 컨벤션
|
|
586
|
+
|
|
587
|
+
- 파일 구조 / 네이밍 / 테스트 방식
|
|
588
|
+
|
|
589
|
+
## 진입점
|
|
590
|
+
|
|
591
|
+
- [[src/index.ts]] / [[config.json]]
|
|
592
|
+
|
|
593
|
+
## 관련 노트
|
|
594
|
+
|
|
595
|
+
- [[관련-context.md]] / [[주요-ADR.md]]
|
|
596
|
+
`;
|
|
597
|
+
var DEFAULT_RUNBOOK_TEMPLATE = `# Runbook: [절차 이름]
|
|
598
|
+
|
|
599
|
+
## 목적
|
|
600
|
+
|
|
601
|
+
이 절차가 달성하는 것
|
|
602
|
+
|
|
603
|
+
## 사전 조건
|
|
604
|
+
|
|
605
|
+
- 필요한 것 1
|
|
606
|
+
|
|
607
|
+
## 단계
|
|
608
|
+
|
|
609
|
+
1. 첫 번째 단계
|
|
610
|
+
2. 두 번째 단계
|
|
611
|
+
|
|
612
|
+
## 확인 방법
|
|
613
|
+
|
|
614
|
+
성공했는지 확인하는 방법
|
|
615
|
+
|
|
616
|
+
## 문제 해결
|
|
617
|
+
|
|
618
|
+
| 증상 | 해결 |
|
|
619
|
+
| ------ | --------------- |
|
|
620
|
+
| 이슈 1 | [[관련-bug.md]] |
|
|
621
|
+
|
|
622
|
+
## 관련 노트
|
|
623
|
+
|
|
624
|
+
- [[관련-runbook.md]] / [[관련-context.md]]
|
|
625
|
+
`;
|
|
626
|
+
var DEFAULT_INSIGHT_TEMPLATE = `# Insight: [발견 제목]
|
|
627
|
+
|
|
628
|
+
## 발견
|
|
629
|
+
|
|
630
|
+
무엇을 알게 되었는지
|
|
631
|
+
|
|
632
|
+
## 맥락
|
|
633
|
+
|
|
634
|
+
어떻게 발견했는지 (어떤 작업 중, 어떤 실험)
|
|
635
|
+
|
|
636
|
+
## 시사점
|
|
637
|
+
|
|
638
|
+
이것이 향후 작업에 미치는 영향
|
|
639
|
+
|
|
640
|
+
## 적용
|
|
641
|
+
|
|
642
|
+
이 발견을 바탕으로 어떻게 행동을 바꿔야 하는지
|
|
643
|
+
|
|
644
|
+
## 관련 노트
|
|
645
|
+
|
|
646
|
+
- [[관련-insight.md]] / [[영향받는-패턴.md]] / [[관련-ADR.md]]
|
|
647
|
+
`;
|
|
648
|
+
var DEFAULT_INDEX_TEMPLATE = `# [Domain] Domain
|
|
649
|
+
|
|
650
|
+
Overview: [1-2 sentence description of this domain]
|
|
651
|
+
|
|
652
|
+
## Notes
|
|
653
|
+
|
|
654
|
+
| File | Summary | Read When... |
|
|
655
|
+
|------|---------|--------------|
|
|
656
|
+
| [[example.md]] | Example summary | Working on X |
|
|
657
|
+
|
|
658
|
+
## Related Domains
|
|
659
|
+
|
|
660
|
+
- [[../other-domain/INDEX.md]] -- Description
|
|
661
|
+
`;
|
|
662
|
+
var TEMPLATE_FILES = {
|
|
663
|
+
"adr.md": DEFAULT_ADR_TEMPLATE,
|
|
664
|
+
"pattern.md": DEFAULT_PATTERN_TEMPLATE,
|
|
665
|
+
"bug.md": DEFAULT_BUG_TEMPLATE,
|
|
666
|
+
"gotcha.md": DEFAULT_GOTCHA_TEMPLATE,
|
|
667
|
+
"decision.md": DEFAULT_DECISION_TEMPLATE,
|
|
668
|
+
"context.md": DEFAULT_CONTEXT_TEMPLATE,
|
|
669
|
+
"runbook.md": DEFAULT_RUNBOOK_TEMPLATE,
|
|
670
|
+
"insight.md": DEFAULT_INSIGHT_TEMPLATE,
|
|
671
|
+
"index.md": DEFAULT_INDEX_TEMPLATE
|
|
672
|
+
};
|
|
673
|
+
function scaffoldIfNeeded(projectDir) {
|
|
674
|
+
const contextDir = join4(projectDir, resolveContextDir(projectDir));
|
|
675
|
+
if (existsSync3(contextDir)) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const promptsDir = join4(contextDir, "prompts");
|
|
680
|
+
mkdirSync(promptsDir, { recursive: true });
|
|
681
|
+
const templatesDir = join4(contextDir, "templates");
|
|
682
|
+
mkdirSync(templatesDir, { recursive: true });
|
|
683
|
+
writeFileSync(join4(contextDir, "config.jsonc"), DEFAULT_CONFIG, "utf-8");
|
|
684
|
+
writeFileSync(join4(promptsDir, DEFAULTS.turnStartFile), DEFAULT_TURN_START, "utf-8");
|
|
685
|
+
writeFileSync(join4(promptsDir, DEFAULTS.turnEndFile), DEFAULT_TURN_END, "utf-8");
|
|
686
|
+
for (const [filename, content] of Object.entries(TEMPLATE_FILES)) {
|
|
687
|
+
writeFileSync(join4(templatesDir, filename), content, "utf-8");
|
|
688
|
+
}
|
|
689
|
+
writeVersion(contextDir, PLUGIN_VERSION);
|
|
690
|
+
return true;
|
|
691
|
+
} catch {
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function writeVersion(contextDir, version) {
|
|
696
|
+
writeFileSync(join4(contextDir, ".version"), version, "utf-8");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/omx/agents-md.ts
|
|
700
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
701
|
+
import { dirname } from "node:path";
|
|
702
|
+
var START_MARKER = "<!-- context:start -->";
|
|
703
|
+
var END_MARKER = "<!-- context:end -->";
|
|
704
|
+
function renderMarkerBlock(content, trailingNewline) {
|
|
705
|
+
const block = `${START_MARKER}
|
|
706
|
+
${content}
|
|
707
|
+
${END_MARKER}`;
|
|
708
|
+
if (trailingNewline) {
|
|
709
|
+
return `${block}
|
|
710
|
+
`;
|
|
711
|
+
}
|
|
712
|
+
return block;
|
|
713
|
+
}
|
|
714
|
+
function appendMarkerBlock(existingContent, content) {
|
|
715
|
+
if (existingContent.length === 0) {
|
|
716
|
+
return renderMarkerBlock(content, true);
|
|
717
|
+
}
|
|
718
|
+
const separator = existingContent.endsWith(`
|
|
719
|
+
`) ? `
|
|
720
|
+
` : `
|
|
721
|
+
|
|
722
|
+
`;
|
|
723
|
+
return `${existingContent}${separator}${renderMarkerBlock(content, true)}`;
|
|
724
|
+
}
|
|
725
|
+
function replaceMarkerBlock(existingContent, content) {
|
|
726
|
+
const startIndex = existingContent.indexOf(START_MARKER);
|
|
727
|
+
const endIndex = existingContent.indexOf(END_MARKER, startIndex + START_MARKER.length);
|
|
728
|
+
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
729
|
+
return appendMarkerBlock(existingContent, content);
|
|
730
|
+
}
|
|
731
|
+
const before = existingContent.slice(0, startIndex);
|
|
732
|
+
const after = existingContent.slice(endIndex + END_MARKER.length);
|
|
733
|
+
const replacement = renderMarkerBlock(content, false);
|
|
734
|
+
return `${before}${replacement}${after}`;
|
|
735
|
+
}
|
|
736
|
+
function writeFileAtomically(filePath, content) {
|
|
737
|
+
const tempPath = `${filePath}.tmp`;
|
|
738
|
+
writeFileSync2(tempPath, content, "utf-8");
|
|
739
|
+
renameSync(tempPath, filePath);
|
|
740
|
+
}
|
|
741
|
+
function injectIntoAgentsMd(agentsMdPath, content) {
|
|
742
|
+
mkdirSync2(dirname(agentsMdPath), { recursive: true });
|
|
743
|
+
if (!existsSync4(agentsMdPath)) {
|
|
744
|
+
writeFileAtomically(agentsMdPath, renderMarkerBlock(content, true));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const existingContent = readFileSync5(agentsMdPath, "utf-8");
|
|
748
|
+
const nextContent = existingContent.includes(START_MARKER) && existingContent.includes(END_MARKER) ? replaceMarkerBlock(existingContent, content) : appendMarkerBlock(existingContent, content);
|
|
749
|
+
writeFileAtomically(agentsMdPath, nextContent);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/omx/index.ts
|
|
753
|
+
function resolveProjectDir(event) {
|
|
754
|
+
return event.context?.projectDir ?? event.context?.directory ?? process.cwd();
|
|
755
|
+
}
|
|
756
|
+
async function onHookEvent(event, sdk) {
|
|
757
|
+
if (event.event !== "session-start") {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const projectDir = resolveProjectDir(event);
|
|
761
|
+
const contextDir = resolveContextDir(projectDir);
|
|
762
|
+
scaffoldIfNeeded(projectDir);
|
|
763
|
+
const config = loadConfig(projectDir);
|
|
764
|
+
const promptVars = { knowledgeDir: config.knowledge.dir ?? DEFAULTS.knowledgeDir };
|
|
765
|
+
const turnStartPath = join5(projectDir, contextDir, config.prompts.turnStart ?? join5("prompts", DEFAULTS.turnStartFile));
|
|
766
|
+
const turnStart = resolvePromptVariables(readPromptFile(turnStartPath), promptVars);
|
|
767
|
+
const knowledgeIndex = buildKnowledgeIndexV2(projectDir, config.knowledge);
|
|
768
|
+
const indexContent = knowledgeIndex.mode === "flat" ? formatKnowledgeIndex(knowledgeIndex.individualFiles) : formatDomainIndex(knowledgeIndex);
|
|
769
|
+
const combinedContent = [turnStart, indexContent].filter(Boolean).join(`
|
|
770
|
+
|
|
771
|
+
`);
|
|
772
|
+
if (!combinedContent) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
injectIntoAgentsMd(join5(projectDir, "AGENTS.md"), combinedContent);
|
|
776
|
+
await sdk.log.info(`Injected context into AGENTS.md for ${projectDir}`);
|
|
777
|
+
}
|
|
778
|
+
export {
|
|
779
|
+
onHookEvent
|
|
780
|
+
};
|