@ksm0709/context 0.0.22 → 0.0.23
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 +7 -116
- package/dist/index.js +59 -1238
- package/dist/mcp.js +33061 -0
- package/dist/omx/index.mjs +50 -359
- package/package.json +5 -4
package/dist/omx/index.mjs
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
// src/omx/index.ts
|
|
2
|
-
import { existsSync as
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "node:fs";
|
|
3
|
+
import { join as join4 } from "node:path";
|
|
4
4
|
|
|
5
5
|
// src/constants.ts
|
|
6
6
|
var DEFAULTS = {
|
|
7
7
|
configPath: ".context/config.jsonc",
|
|
8
|
-
promptDir: ".context/prompts",
|
|
9
|
-
turnStartFile: "turn-start.md",
|
|
10
|
-
turnEndFile: "turn-end.md",
|
|
11
8
|
knowledgeSources: ["AGENTS.md"],
|
|
12
9
|
templateDir: ".context/templates",
|
|
13
10
|
indexFilename: "INDEX.md",
|
|
@@ -48,10 +45,6 @@ function resolveContextDir(projectDir) {
|
|
|
48
45
|
// src/lib/config.ts
|
|
49
46
|
function getDefaultConfig() {
|
|
50
47
|
return {
|
|
51
|
-
prompts: {
|
|
52
|
-
turnStart: join2(DEFAULTS.promptDir, DEFAULTS.turnStartFile),
|
|
53
|
-
turnEnd: join2(DEFAULTS.promptDir, DEFAULTS.turnEndFile)
|
|
54
|
-
},
|
|
55
48
|
knowledge: {
|
|
56
49
|
dir: "docs",
|
|
57
50
|
sources: [...DEFAULTS.knowledgeSources],
|
|
@@ -69,10 +62,6 @@ function getDefaultConfig() {
|
|
|
69
62
|
function mergeWithDefaults(partial) {
|
|
70
63
|
const defaults = getDefaultConfig();
|
|
71
64
|
return {
|
|
72
|
-
prompts: {
|
|
73
|
-
turnStart: partial.prompts?.turnStart ?? defaults.prompts.turnStart,
|
|
74
|
-
turnEnd: partial.prompts?.turnEnd ?? defaults.prompts.turnEnd
|
|
75
|
-
},
|
|
76
65
|
knowledge: {
|
|
77
66
|
dir: partial.knowledge?.dir ?? defaults.knowledge.dir,
|
|
78
67
|
sources: partial.knowledge?.sources ?? defaults.knowledge.sources,
|
|
@@ -100,232 +89,13 @@ function loadConfig(projectDir) {
|
|
|
100
89
|
}
|
|
101
90
|
}
|
|
102
91
|
|
|
103
|
-
// src/lib/knowledge-index.ts
|
|
104
|
-
import { readdirSync, readFileSync as readFileSync2, statSync, existsSync as existsSync2 } from "node:fs";
|
|
105
|
-
import { join as join3, relative, extname } from "node:path";
|
|
106
|
-
function extractSummary(filePath) {
|
|
107
|
-
try {
|
|
108
|
-
const content = readFileSync2(filePath, "utf-8");
|
|
109
|
-
const firstNonEmpty = content.split(`
|
|
110
|
-
`).find((line) => line.trim().length > 0);
|
|
111
|
-
if (!firstNonEmpty)
|
|
112
|
-
return "";
|
|
113
|
-
return firstNonEmpty.trim().slice(0, LIMITS.maxSummaryLength);
|
|
114
|
-
} catch {
|
|
115
|
-
return "";
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
function scanDir(dir, projectDir, depth, entries) {
|
|
119
|
-
if (depth > LIMITS.maxScanDepth)
|
|
120
|
-
return;
|
|
121
|
-
if (entries.length >= LIMITS.maxIndexEntries)
|
|
122
|
-
return;
|
|
123
|
-
try {
|
|
124
|
-
const items = readdirSync(dir);
|
|
125
|
-
for (const item of items) {
|
|
126
|
-
if (entries.length >= LIMITS.maxIndexEntries)
|
|
127
|
-
break;
|
|
128
|
-
const fullPath = join3(dir, item);
|
|
129
|
-
try {
|
|
130
|
-
const stat = statSync(fullPath);
|
|
131
|
-
if (stat.isDirectory()) {
|
|
132
|
-
scanDir(fullPath, projectDir, depth + 1, entries);
|
|
133
|
-
} else if (stat.isFile() && extname(item) === ".md") {
|
|
134
|
-
entries.push({
|
|
135
|
-
filename: relative(projectDir, fullPath),
|
|
136
|
-
summary: extractSummary(fullPath)
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
} catch {}
|
|
140
|
-
}
|
|
141
|
-
} catch {}
|
|
142
|
-
}
|
|
143
|
-
function buildKnowledgeIndex(projectDir, sources) {
|
|
144
|
-
const entries = [];
|
|
145
|
-
for (const source of sources) {
|
|
146
|
-
if (entries.length >= LIMITS.maxIndexEntries)
|
|
147
|
-
break;
|
|
148
|
-
const fullPath = join3(projectDir, source);
|
|
149
|
-
if (!existsSync2(fullPath))
|
|
150
|
-
continue;
|
|
151
|
-
try {
|
|
152
|
-
const stat = statSync(fullPath);
|
|
153
|
-
if (stat.isFile() && extname(source) === ".md") {
|
|
154
|
-
entries.push({
|
|
155
|
-
filename: source,
|
|
156
|
-
summary: extractSummary(fullPath)
|
|
157
|
-
});
|
|
158
|
-
} else if (stat.isDirectory()) {
|
|
159
|
-
scanDir(fullPath, projectDir, 1, entries);
|
|
160
|
-
}
|
|
161
|
-
} catch {}
|
|
162
|
-
}
|
|
163
|
-
return entries;
|
|
164
|
-
}
|
|
165
|
-
function formatKnowledgeIndex(entries) {
|
|
166
|
-
if (entries.length === 0)
|
|
167
|
-
return "";
|
|
168
|
-
const lines = ["## Available Knowledge", ""];
|
|
169
|
-
for (const entry of entries) {
|
|
170
|
-
lines.push(`- ${entry.filename}${entry.summary ? ` — ${entry.summary}` : ""}`);
|
|
171
|
-
}
|
|
172
|
-
return lines.join(`
|
|
173
|
-
`);
|
|
174
|
-
}
|
|
175
|
-
function countMdFiles(dir, indexFilename) {
|
|
176
|
-
try {
|
|
177
|
-
const items = readdirSync(dir);
|
|
178
|
-
return items.filter((item) => extname(item) === ".md" && item !== indexFilename && statSync(join3(dir, item)).isFile()).length;
|
|
179
|
-
} catch {
|
|
180
|
-
return 0;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
function scanDomainsRecursive(baseDir, projectDir, indexFilename, currentDepth, maxDepth, results) {
|
|
184
|
-
if (currentDepth > maxDepth)
|
|
185
|
-
return;
|
|
186
|
-
try {
|
|
187
|
-
const items = readdirSync(baseDir);
|
|
188
|
-
for (const item of items) {
|
|
189
|
-
const fullPath = join3(baseDir, item);
|
|
190
|
-
try {
|
|
191
|
-
if (!statSync(fullPath).isDirectory())
|
|
192
|
-
continue;
|
|
193
|
-
const indexPath = join3(fullPath, indexFilename);
|
|
194
|
-
if (existsSync2(indexPath) && statSync(indexPath).isFile()) {
|
|
195
|
-
const rawContent = readFileSync2(indexPath, "utf-8");
|
|
196
|
-
const indexContent = rawContent.slice(0, LIMITS.maxIndexFileSize);
|
|
197
|
-
results.push({
|
|
198
|
-
domain: item,
|
|
199
|
-
path: relative(projectDir, fullPath),
|
|
200
|
-
indexContent,
|
|
201
|
-
noteCount: countMdFiles(fullPath, indexFilename)
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
scanDomainsRecursive(fullPath, projectDir, indexFilename, currentDepth + 1, maxDepth, results);
|
|
205
|
-
} catch {}
|
|
206
|
-
}
|
|
207
|
-
} catch {}
|
|
208
|
-
}
|
|
209
|
-
function scanDomains(projectDir, knowledgeDir, indexFilename, maxDepth) {
|
|
210
|
-
const baseDir = join3(projectDir, knowledgeDir);
|
|
211
|
-
if (!existsSync2(baseDir))
|
|
212
|
-
return [];
|
|
213
|
-
const results = [];
|
|
214
|
-
scanDomainsRecursive(baseDir, projectDir, indexFilename, 1, maxDepth, results);
|
|
215
|
-
return results;
|
|
216
|
-
}
|
|
217
|
-
function detectKnowledgeMode(projectDir, knowledgeDir, indexFilename, configMode) {
|
|
218
|
-
if (configMode !== "auto")
|
|
219
|
-
return configMode;
|
|
220
|
-
const domains = scanDomains(projectDir, knowledgeDir, indexFilename, 1);
|
|
221
|
-
return domains.length > 0 ? "domain" : "flat";
|
|
222
|
-
}
|
|
223
|
-
function formatDomainIndex(index) {
|
|
224
|
-
const hasDomains = index.domains.length > 0;
|
|
225
|
-
const hasFiles = index.individualFiles.length > 0;
|
|
226
|
-
if (!hasDomains && !hasFiles)
|
|
227
|
-
return "";
|
|
228
|
-
const lines = ["## Available Knowledge", ""];
|
|
229
|
-
if (hasDomains) {
|
|
230
|
-
lines.push("### Domains", "");
|
|
231
|
-
for (const domain of index.domains) {
|
|
232
|
-
lines.push(`#### ${domain.path}/ (${domain.noteCount} notes)`, "");
|
|
233
|
-
lines.push(domain.indexContent, "");
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (hasFiles) {
|
|
237
|
-
if (hasDomains) {
|
|
238
|
-
lines.push("### Individual Files", "");
|
|
239
|
-
}
|
|
240
|
-
for (const file of index.individualFiles) {
|
|
241
|
-
lines.push(`- ${file.filename}${file.summary ? ` — ${file.summary}` : ""}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return lines.join(`
|
|
245
|
-
`);
|
|
246
|
-
}
|
|
247
|
-
function collectRootFiles(projectDir, knowledgeDir, indexFilename) {
|
|
248
|
-
const baseDir = join3(projectDir, knowledgeDir);
|
|
249
|
-
if (!existsSync2(baseDir))
|
|
250
|
-
return [];
|
|
251
|
-
const entries = [];
|
|
252
|
-
try {
|
|
253
|
-
const items = readdirSync(baseDir);
|
|
254
|
-
for (const item of items) {
|
|
255
|
-
const fullPath = join3(baseDir, item);
|
|
256
|
-
try {
|
|
257
|
-
const stat = statSync(fullPath);
|
|
258
|
-
if (stat.isFile() && extname(item) === ".md" && item !== indexFilename) {
|
|
259
|
-
entries.push({
|
|
260
|
-
filename: relative(projectDir, fullPath),
|
|
261
|
-
summary: extractSummary(fullPath)
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
} catch {}
|
|
265
|
-
}
|
|
266
|
-
} catch {}
|
|
267
|
-
return entries;
|
|
268
|
-
}
|
|
269
|
-
function buildKnowledgeIndexV2(projectDir, knowledgeConfig) {
|
|
270
|
-
const dir = knowledgeConfig.dir ?? "docs";
|
|
271
|
-
const indexFilename = knowledgeConfig.indexFilename ?? "INDEX.md";
|
|
272
|
-
const maxDepth = knowledgeConfig.maxDomainDepth ?? 2;
|
|
273
|
-
const configMode = knowledgeConfig.mode ?? "auto";
|
|
274
|
-
const mode = detectKnowledgeMode(projectDir, dir, indexFilename, configMode);
|
|
275
|
-
if (mode === "flat") {
|
|
276
|
-
const allSources = [dir, ...knowledgeConfig.sources].filter(Boolean);
|
|
277
|
-
const entries = buildKnowledgeIndex(projectDir, allSources);
|
|
278
|
-
return { mode: "flat", domains: [], individualFiles: entries };
|
|
279
|
-
}
|
|
280
|
-
const domains = scanDomains(projectDir, dir, indexFilename, maxDepth);
|
|
281
|
-
const rootFiles = collectRootFiles(projectDir, dir, indexFilename);
|
|
282
|
-
const sourcesEntries = [];
|
|
283
|
-
for (const source of knowledgeConfig.sources) {
|
|
284
|
-
const fullPath = join3(projectDir, source);
|
|
285
|
-
if (!existsSync2(fullPath))
|
|
286
|
-
continue;
|
|
287
|
-
try {
|
|
288
|
-
const stat = statSync(fullPath);
|
|
289
|
-
if (stat.isFile() && extname(source) === ".md") {
|
|
290
|
-
sourcesEntries.push({
|
|
291
|
-
filename: source,
|
|
292
|
-
summary: extractSummary(fullPath)
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
} catch {}
|
|
296
|
-
}
|
|
297
|
-
const individualFiles = [...rootFiles, ...sourcesEntries];
|
|
298
|
-
return { mode: "domain", domains, individualFiles };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// src/lib/prompt-reader.ts
|
|
302
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
303
|
-
function readPromptFile(filePath) {
|
|
304
|
-
try {
|
|
305
|
-
const content = readFileSync3(filePath, "utf-8");
|
|
306
|
-
if (content.length > LIMITS.maxPromptFileSize) {
|
|
307
|
-
return content.slice(0, LIMITS.maxPromptFileSize);
|
|
308
|
-
}
|
|
309
|
-
return content;
|
|
310
|
-
} catch {
|
|
311
|
-
return "";
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
function resolvePromptVariables(content, vars) {
|
|
315
|
-
const normalized = (vars.knowledgeDir || "docs").replace(/\\/g, "/").replace(/\/+$/, "");
|
|
316
|
-
let resolved = content.replaceAll("{{knowledgeDir}}", normalized);
|
|
317
|
-
resolved = resolved.replaceAll("{{sessionId}}", vars.sessionId ?? "");
|
|
318
|
-
resolved = resolved.replaceAll("{{turnId}}", vars.turnId ?? "");
|
|
319
|
-
return resolved;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
92
|
// src/lib/scaffold.ts
|
|
323
|
-
import { existsSync as
|
|
324
|
-
import { join as
|
|
93
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
94
|
+
import { join as join3 } from "node:path";
|
|
325
95
|
// package.json
|
|
326
96
|
var package_default = {
|
|
327
97
|
name: "@ksm0709/context",
|
|
328
|
-
version: "0.0.
|
|
98
|
+
version: "0.0.23",
|
|
329
99
|
author: {
|
|
330
100
|
name: "TaehoKang",
|
|
331
101
|
email: "ksm07091@gmail.com"
|
|
@@ -355,10 +125,10 @@ var package_default = {
|
|
|
355
125
|
access: "public"
|
|
356
126
|
},
|
|
357
127
|
scripts: {
|
|
358
|
-
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",
|
|
128
|
+
build: "bun build ./src/index.ts --outdir dist --target bun && bun build ./src/mcp.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",
|
|
359
129
|
test: "vitest run",
|
|
360
130
|
lint: "eslint src --ext .ts",
|
|
361
|
-
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"
|
|
131
|
+
prepublishOnly: "bun build ./src/index.ts --outdir dist --target bun && bun build ./src/mcp.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"
|
|
362
132
|
},
|
|
363
133
|
files: [
|
|
364
134
|
"dist"
|
|
@@ -367,7 +137,8 @@ var package_default = {
|
|
|
367
137
|
"@opencode-ai/plugin": ">=1.0.0"
|
|
368
138
|
},
|
|
369
139
|
dependencies: {
|
|
370
|
-
"@ksm0709/context": "^0.0.
|
|
140
|
+
"@ksm0709/context": "^0.0.22",
|
|
141
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
371
142
|
"jsonc-parser": "^3.0.0"
|
|
372
143
|
},
|
|
373
144
|
devDependencies: {
|
|
@@ -392,10 +163,6 @@ var PLUGIN_VERSION = package_default.version;
|
|
|
392
163
|
var DEFAULT_CONFIG = `{
|
|
393
164
|
// Context Plugin Configuration
|
|
394
165
|
// See: https://github.com/ksm0709/context
|
|
395
|
-
"prompts": {
|
|
396
|
-
"turnStart": "prompts/turn-start.md",
|
|
397
|
-
"turnEnd": "prompts/turn-end.md"
|
|
398
|
-
},
|
|
399
166
|
"knowledge": {
|
|
400
167
|
"dir": ".context/memory",
|
|
401
168
|
"sources": ["AGENTS.md"]
|
|
@@ -407,53 +174,6 @@ var DEFAULT_CONFIG = `{
|
|
|
407
174
|
}
|
|
408
175
|
}
|
|
409
176
|
}`;
|
|
410
|
-
var DEFAULT_TURN_START = `## Knowledge Context
|
|
411
|
-
|
|
412
|
-
이 프로젝트는 **제텔카스텐(Zettelkasten)** 방식으로 지식을 관리합니다.
|
|
413
|
-
세션 간 컨텍스트를 보존하여, 이전 세션의 결정/패턴/실수가 다음 세션에서 재활용됩니다.
|
|
414
|
-
|
|
415
|
-
### 제텔카스텐 핵심 원칙
|
|
416
|
-
|
|
417
|
-
1. **원자성** -- 하나의 노트 = 하나의 주제. 여러 주제를 섞지 마세요.
|
|
418
|
-
2. **연결** -- 모든 노트는 [[wikilink]]로 관련 노트에 연결. 고립된 노트는 발견되지 않습니다.
|
|
419
|
-
3. **자기 언어** -- 복사-붙여넣기가 아닌, 핵심을 이해하고 간결하게 서술하세요.
|
|
420
|
-
|
|
421
|
-
### 작업 전 필수
|
|
422
|
-
|
|
423
|
-
- **데일리 노트 확인**: 가장 최근의 데일리 노트(\`{{knowledgeDir}}/daily/YYYY-MM-DD.md\`)를 읽고 이전 세션의 컨텍스트와 미해결 이슈를 파악하세요
|
|
424
|
-
- **작업 의도 선언**: 작업 시작 전, 현재 세션의 목표와 작업 의도를 명확히 파악하고 선언하세요 (추후 작업 경로 검증 시 기준이 됩니다)
|
|
425
|
-
- 메인 에이전트가 아래 **Available Knowledge** 목록에서 현재 작업과 관련된 문서를 **직접 먼저** 읽으세요
|
|
426
|
-
- 도메인 폴더 구조가 있다면 INDEX.md의 요약을 참고하여 필요한 노트만 선택적으로 읽으세요
|
|
427
|
-
- 문서 내 [[링크]]를 따라가며 관련 노트를 탐색하세요 -- 링크를 놓치면 중요한 맥락을 잃습니다
|
|
428
|
-
- 지식 파일에 기록된 아키텍처 결정, 패턴, 제약사항을 반드시 따르세요
|
|
429
|
-
- 읽은 지식을 현재 작업의 설계, 구현, 검증에 직접 반영하세요
|
|
430
|
-
|
|
431
|
-
### 개발 원칙
|
|
432
|
-
|
|
433
|
-
- **TDD** (Test-Driven Development): 테스트를 먼저 작성하고(RED), 구현하여 통과시킨 뒤(GREEN), 리팩토링하세요
|
|
434
|
-
- **DDD** (Domain-Driven Design): 도메인 개념을 코드 구조에 반영하세요. 타입과 모듈은 비즈니스 도메인을 기준으로 분리하세요
|
|
435
|
-
- **테스트 커버리지**: 새로 작성하거나 변경한 코드는 테스트 커버리지 80% 이상을 목표로 하세요. 구현 전에 테스트부터 작성하면 자연스럽게 달성됩니다
|
|
436
|
-
|
|
437
|
-
### 우선순위
|
|
438
|
-
|
|
439
|
-
- AGENTS.md의 지시사항이 항상 최우선
|
|
440
|
-
- 지식 노트의 결정사항 > 일반적 관행
|
|
441
|
-
- 지식 노트에 없는 새로운 결정이나 반복 가치가 있는 발견은 작업 메모나 지식 노트 후보로 기록하세요
|
|
442
|
-
`;
|
|
443
|
-
var DEFAULT_TURN_END = `## TURN END 작업 지침
|
|
444
|
-
이 지침은 에이전트를 위한 것이며, 사용자에게 직접적으로 노출하지 않습니다.
|
|
445
|
-
아래 메뉴 중 하나를 선택해 진행 상황에 맞게 수행하세요.
|
|
446
|
-
**반드시 링크된 가이드를 참고하여 정확히 수행해야 합니다.**
|
|
447
|
-
|
|
448
|
-
1. **계속 작업**: 기존 작업이 완료되지 않았고 아직 아래 액션을 취할 단계가 아니라면 작업 속개.
|
|
449
|
-
2. **데일리 노트 기록**: [.context/guides/daily-note-guide.md] 데일리 노트에 중요한 컨텍스트를 기록하여 다음 세션이나 에이전트 팀이 참고할 수 있도록 하세요. 기존 내용 수정은 불가하며, 새로운 메모를 추가 하는것만 가능합니다. 간략한 한 두 문장으로 작성하여 핵심 컨텍스트가 명확히 전달되도록 하세요.
|
|
450
|
-
3. **지식 노트 작성**: [.context/guides/note-guide.md] 작업기억(데일리노트, 세션 컨텍스트)보다 오래 기억되어야 하는 중요한 결정, 패턴, 실수, 발견은 지식 노트로 기록하여 프로젝트의 집단 지식으로 남기세요.
|
|
451
|
-
4. **노트/스킬 검색 및 읽기**: [.context/guides/search-guide.md] 어려움에 처했다면 현재 진행 상황에 필요한 지식이나 스킬이 있는지 확인하고, 관련 노트를 읽어보세요. 새로운 아이디어나 해결책이 떠오를 수 있습니다.
|
|
452
|
-
5. **작업 경로 리뷰**: [.context/guides/scope-review.md] 사용자가 의도한 작업 범위를 벗어나지 않았는지, 작업이 너무 크거나 복잡해지지는 않았는지 검토하세요.
|
|
453
|
-
6. **체크포인트 커밋**: [.context/guides/commit-guide.md] 작업이 길어질 경우, 중요한 단계마다 체크포인트 커밋을 하여 작업 내용을 안전하게 저장하고, 필요 시 이전 상태로 돌아갈 수 있도록 하세요.
|
|
454
|
-
7. **퀄리티 검증**: [.context/guides/quality-check.md] **작업 완료 전에 반드시 수행하세요**. 코드 린트, 포맷터, 테스트, 빌드, 코드리뷰를 실행하여 작업 결과물이 프로젝트의 품질 기준을 충족하는지 확인하세요.
|
|
455
|
-
8. **작업 완료**: [.context/guides/complete-guide.md] 모든 작업이 완료되었다면, 이 가이드를 따르세요. 이 작업 지침이 더이상 트리거되지 않을 것입니다.
|
|
456
|
-
`;
|
|
457
177
|
var DEFAULT_ADR_TEMPLATE = `# ADR-NNN: [제목]
|
|
458
178
|
|
|
459
179
|
## 상태
|
|
@@ -745,25 +465,21 @@ var TEMPLATE_FILES = {
|
|
|
745
465
|
"work-complete.txt": DEFAULT_WORK_COMPLETE_TEMPLATE
|
|
746
466
|
};
|
|
747
467
|
function scaffoldIfNeeded(projectDir) {
|
|
748
|
-
const contextDir =
|
|
749
|
-
if (
|
|
468
|
+
const contextDir = join3(projectDir, resolveContextDir(projectDir));
|
|
469
|
+
if (existsSync2(contextDir)) {
|
|
750
470
|
return false;
|
|
751
471
|
}
|
|
752
472
|
try {
|
|
753
|
-
const
|
|
754
|
-
mkdirSync(promptsDir, { recursive: true });
|
|
755
|
-
const templatesDir = join4(contextDir, "templates");
|
|
473
|
+
const templatesDir = join3(contextDir, "templates");
|
|
756
474
|
mkdirSync(templatesDir, { recursive: true });
|
|
757
|
-
const guidesDir =
|
|
475
|
+
const guidesDir = join3(contextDir, "guides");
|
|
758
476
|
mkdirSync(guidesDir, { recursive: true });
|
|
759
|
-
writeFileSync(
|
|
760
|
-
writeFileSync(join4(promptsDir, DEFAULTS.turnStartFile), DEFAULT_TURN_START, "utf-8");
|
|
761
|
-
writeFileSync(join4(promptsDir, DEFAULTS.turnEndFile), DEFAULT_TURN_END, "utf-8");
|
|
477
|
+
writeFileSync(join3(contextDir, "config.jsonc"), DEFAULT_CONFIG, "utf-8");
|
|
762
478
|
for (const [filename, content] of Object.entries(TEMPLATE_FILES)) {
|
|
763
|
-
writeFileSync(
|
|
479
|
+
writeFileSync(join3(templatesDir, filename), content, "utf-8");
|
|
764
480
|
}
|
|
765
481
|
for (const [filename, content] of Object.entries(GUIDE_FILES)) {
|
|
766
|
-
writeFileSync(
|
|
482
|
+
writeFileSync(join3(guidesDir, filename), content, "utf-8");
|
|
767
483
|
}
|
|
768
484
|
writeVersion(contextDir, PLUGIN_VERSION);
|
|
769
485
|
return true;
|
|
@@ -772,11 +488,11 @@ function scaffoldIfNeeded(projectDir) {
|
|
|
772
488
|
}
|
|
773
489
|
}
|
|
774
490
|
function writeVersion(contextDir, version) {
|
|
775
|
-
writeFileSync(
|
|
491
|
+
writeFileSync(join3(contextDir, ".version"), version, "utf-8");
|
|
776
492
|
}
|
|
777
493
|
|
|
778
494
|
// src/omx/agents-md.ts
|
|
779
|
-
import { existsSync as
|
|
495
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
780
496
|
import { dirname } from "node:path";
|
|
781
497
|
var START_MARKER = "<!-- context:start -->";
|
|
782
498
|
var END_MARKER = "<!-- context:end -->";
|
|
@@ -819,11 +535,11 @@ function writeFileAtomically(filePath, content) {
|
|
|
819
535
|
}
|
|
820
536
|
function injectIntoAgentsMd(agentsMdPath, content) {
|
|
821
537
|
mkdirSync2(dirname(agentsMdPath), { recursive: true });
|
|
822
|
-
if (!
|
|
538
|
+
if (!existsSync3(agentsMdPath)) {
|
|
823
539
|
writeFileAtomically(agentsMdPath, renderMarkerBlock(content, true));
|
|
824
540
|
return;
|
|
825
541
|
}
|
|
826
|
-
const existingContent =
|
|
542
|
+
const existingContent = readFileSync3(agentsMdPath, "utf-8");
|
|
827
543
|
const nextContent = existingContent.includes(START_MARKER) && existingContent.includes(END_MARKER) ? replaceMarkerBlock(existingContent, content) : appendMarkerBlock(existingContent, content);
|
|
828
544
|
writeFileAtomically(agentsMdPath, nextContent);
|
|
829
545
|
}
|
|
@@ -894,21 +610,6 @@ function clearCurrentFollowupScope(pendingScopes, scopeKey) {
|
|
|
894
610
|
function resolveProjectDir(event) {
|
|
895
611
|
return event.context?.projectDir ?? event.context?.directory ?? process.cwd();
|
|
896
612
|
}
|
|
897
|
-
function resolvePromptPath(directory, contextDir, promptPath) {
|
|
898
|
-
if (isAbsolute(promptPath))
|
|
899
|
-
return promptPath;
|
|
900
|
-
if (promptPath.startsWith(".context/") || promptPath.startsWith(".opencode/")) {
|
|
901
|
-
return join5(directory, promptPath);
|
|
902
|
-
}
|
|
903
|
-
return join5(directory, contextDir, promptPath);
|
|
904
|
-
}
|
|
905
|
-
function buildPromptVars(config) {
|
|
906
|
-
return { knowledgeDir: config.knowledge.dir ?? DEFAULTS.knowledgeDir };
|
|
907
|
-
}
|
|
908
|
-
function buildKnowledgeContent(projectDir, config) {
|
|
909
|
-
const knowledgeIndex = buildKnowledgeIndexV2(projectDir, config.knowledge);
|
|
910
|
-
return knowledgeIndex.mode === "flat" ? formatKnowledgeIndex(knowledgeIndex.individualFiles) : formatDomainIndex(knowledgeIndex);
|
|
911
|
-
}
|
|
912
613
|
function resolveFollowupScopeKey(event) {
|
|
913
614
|
if (event.session_id && event.session_id.trim().length > 0) {
|
|
914
615
|
return `session:${event.session_id.trim()}`;
|
|
@@ -925,27 +626,39 @@ async function logWarn(sdk, message, meta = {}) {
|
|
|
925
626
|
}
|
|
926
627
|
await sdk.log.info(message, meta);
|
|
927
628
|
}
|
|
629
|
+
var STATIC_KNOWLEDGE_CONTEXT = `## Knowledge Context
|
|
630
|
+
|
|
631
|
+
이 프로젝트는 **제텔카스텐(Zettelkasten)** 방식으로 지식을 관리합니다.
|
|
632
|
+
세션 간 컨텍스트를 보존하여, 이전 세션의 결정/패턴/실수가 다음 세션에서 재활용됩니다.
|
|
633
|
+
|
|
634
|
+
### 제텔카스텐 핵심 원칙
|
|
635
|
+
1. **원자성** -- 하나의 노트 = 하나의 주제. 여러 주제를 섞지 마세요.
|
|
636
|
+
2. **연결** -- 모든 노트는 [[wikilink]]로 관련 노트에 연결. 고립된 노트는 발견되지 않습니다.
|
|
637
|
+
3. **자기 언어** -- 복사-붙여넣기가 아닌, 핵심을 이해하고 간결하게 서술하세요.
|
|
638
|
+
|
|
639
|
+
### 작업 전 필수
|
|
640
|
+
- **데일리 노트 확인**: 가장 최근의 데일리 노트를 읽고 이전 세션의 컨텍스트와 미해결 이슈를 파악하세요. (Use \`read_daily_note\` tool)
|
|
641
|
+
- **작업 의도 선언**: 작업 시작 전, 현재 세션의 목표와 작업 의도를 명확히 파악하고 선언하세요.
|
|
642
|
+
- **지식 검색**: 작업과 관련된 문서를 **직접 먼저** 검색하고 읽으세요. (Use \`search_knowledge\` and \`read_knowledge\` tools)
|
|
643
|
+
- 지식 파일에 기록된 아키텍처 결정, 패턴, 제약사항을 반드시 따르세요.
|
|
644
|
+
|
|
645
|
+
### 개발 원칙
|
|
646
|
+
- **TDD** (Test-Driven Development): 테스트를 먼저 작성하고(RED), 구현하여 통과시킨 뒤(GREEN), 리팩토링하세요.
|
|
647
|
+
- **DDD** (Domain-Driven Design): 도메인 개념을 코드 구조에 반영하세요.
|
|
648
|
+
- **테스트 커버리지**: 새로 작성하거나 변경한 코드는 테스트 커버리지 80% 이상을 목표로 하세요.
|
|
649
|
+
|
|
650
|
+
### 우선순위
|
|
651
|
+
- AGENTS.md의 지시사항이 항상 최우선
|
|
652
|
+
- 지식 노트의 결정사항 > 일반적 관행
|
|
653
|
+
- 지식 노트에 없는 새로운 결정이나 반복 가치가 있는 발견은 작업 메모나 지식 노트 후보로 기록하세요. (Use \`create_knowledge_note\` tool)`;
|
|
928
654
|
async function onSessionStart(event, sdk) {
|
|
929
655
|
const projectDir = resolveProjectDir(event);
|
|
930
|
-
const contextDir = resolveContextDir(projectDir);
|
|
931
656
|
scaffoldIfNeeded(projectDir);
|
|
932
|
-
|
|
933
|
-
const promptVars = buildPromptVars(config);
|
|
934
|
-
const turnStartPath = resolvePromptPath(projectDir, contextDir, config.prompts.turnStart ?? join5(DEFAULTS.promptDir, DEFAULTS.turnStartFile));
|
|
935
|
-
const turnStart = resolvePromptVariables(readPromptFile(turnStartPath), promptVars);
|
|
936
|
-
const indexContent = buildKnowledgeContent(projectDir, config);
|
|
937
|
-
const combinedContent = [turnStart, indexContent].filter(Boolean).join(`
|
|
938
|
-
|
|
939
|
-
`);
|
|
940
|
-
if (!combinedContent) {
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
injectIntoAgentsMd(join5(projectDir, "AGENTS.md"), combinedContent);
|
|
657
|
+
injectIntoAgentsMd(join4(projectDir, "AGENTS.md"), STATIC_KNOWLEDGE_CONTEXT);
|
|
944
658
|
await sdk.log.info(`Injected context into AGENTS.md for ${projectDir}`);
|
|
945
659
|
}
|
|
946
660
|
async function onTurnComplete(event, sdk) {
|
|
947
661
|
const projectDir = resolveProjectDir(event);
|
|
948
|
-
const contextDir = resolveContextDir(projectDir);
|
|
949
662
|
const config = loadConfig(projectDir);
|
|
950
663
|
const strategy = config.omx?.turnEnd?.strategy ?? "off";
|
|
951
664
|
if (strategy !== "turn-complete-sendkeys") {
|
|
@@ -968,9 +681,9 @@ async function onTurnComplete(event, sdk) {
|
|
|
968
681
|
}
|
|
969
682
|
const followupScopeKey = resolveFollowupScopeKey(event);
|
|
970
683
|
let pendingFollowupScopes = typeof sdk.state?.read === "function" ? await sdk.state.read(TURN_END_PENDING_SKIP_KEY, {}) ?? {} : {};
|
|
971
|
-
const workCompleteFile =
|
|
972
|
-
if (
|
|
973
|
-
const content =
|
|
684
|
+
const workCompleteFile = join4(projectDir, DEFAULTS.workCompleteFile);
|
|
685
|
+
if (existsSync4(workCompleteFile)) {
|
|
686
|
+
const content = readFileSync4(workCompleteFile, "utf-8");
|
|
974
687
|
const { sessionId: fileSessionId, turnId: fileTurnId } = parseWorkComplete(content);
|
|
975
688
|
const currentScopeId = event.session_id ?? event.thread_id ?? "";
|
|
976
689
|
if (!fileSessionId || fileSessionId === currentScopeId) {
|
|
@@ -1022,28 +735,6 @@ async function onTurnComplete(event, sdk) {
|
|
|
1022
735
|
});
|
|
1023
736
|
return;
|
|
1024
737
|
}
|
|
1025
|
-
const promptVars = {
|
|
1026
|
-
...buildPromptVars(config),
|
|
1027
|
-
sessionId: event.session_id ?? event.thread_id ?? "unknown",
|
|
1028
|
-
turnId: event.turn_id ?? ""
|
|
1029
|
-
};
|
|
1030
|
-
const turnEndPath = resolvePromptPath(projectDir, contextDir, config.prompts.turnEnd ?? join5(DEFAULTS.promptDir, DEFAULTS.turnEndFile));
|
|
1031
|
-
let turnEndRaw = readPromptFile(turnEndPath);
|
|
1032
|
-
if (!turnEndRaw) {
|
|
1033
|
-
await sdk.log.info("turn_end_skipped_empty_prompt", {
|
|
1034
|
-
event: event.event,
|
|
1035
|
-
session_id: event.session_id,
|
|
1036
|
-
turn_id: event.turn_id
|
|
1037
|
-
});
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
if (turnEndRaw.includes("<!-- context:start -->") || turnEndRaw.includes("## Knowledge Context")) {
|
|
1041
|
-
const defaultTurnEndPath = join5(projectDir, DEFAULTS.promptDir, DEFAULTS.turnEndFile);
|
|
1042
|
-
turnEndRaw = readPromptFile(defaultTurnEndPath);
|
|
1043
|
-
if (!turnEndRaw) {
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
738
|
if (typeof sdk.tmux?.sendKeys !== "function") {
|
|
1048
739
|
await logWarn(sdk, "turn_end_sendkeys_failed", {
|
|
1049
740
|
event: event.event,
|
|
@@ -1053,7 +744,7 @@ async function onTurnComplete(event, sdk) {
|
|
|
1053
744
|
});
|
|
1054
745
|
return;
|
|
1055
746
|
}
|
|
1056
|
-
const turnEnd =
|
|
747
|
+
const turnEnd = "TURN END. You MUST call the 'submit_turn_complete' MCP tool to finalize your work and record notes. Do not wait for user input.";
|
|
1057
748
|
const reminderText = `<system-reminder>
|
|
1058
749
|
${turnEnd}
|
|
1059
750
|
</system-reminder>`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksm0709/context",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "TaehoKang",
|
|
6
6
|
"email": "ksm07091@gmail.com"
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"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",
|
|
33
|
+
"build": "bun build ./src/index.ts --outdir dist --target bun && bun build ./src/mcp.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",
|
|
34
34
|
"test": "vitest run",
|
|
35
35
|
"lint": "eslint src --ext .ts",
|
|
36
|
-
"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"
|
|
36
|
+
"prepublishOnly": "bun build ./src/index.ts --outdir dist --target bun && bun build ./src/mcp.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"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"dist"
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"@opencode-ai/plugin": ">=1.0.0"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@ksm0709/context": "^0.0.
|
|
45
|
+
"@ksm0709/context": "^0.0.22",
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
46
47
|
"jsonc-parser": "^3.0.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|