@ksm0709/context 0.0.22 → 0.0.25
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 +10 -118
- package/dist/index.js +62 -1240
- package/dist/mcp.js +33090 -0
- package/dist/omx/index.mjs +58 -361
- 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.25",
|
|
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
|
## 상태
|
|
@@ -666,13 +386,14 @@ turn_id={{turnId}}
|
|
|
666
386
|
`;
|
|
667
387
|
var DEFAULT_DAILY_NOTE_GUIDE = `# 데일리 노트 기록 가이드
|
|
668
388
|
|
|
669
|
-
- [ ] \`
|
|
389
|
+
- [ ] \`context-mcp_append_daily_note\` 도구를 사용하여 기록을 추가하세요.
|
|
670
390
|
- [ ] **주의**: 데일리 노트의 기존 내용은 절대 수정하거나 삭제하지 마세요.
|
|
671
|
-
- [ ]
|
|
391
|
+
- [ ] 기록은 다음과 같은 형식으로 추가됩니다:
|
|
672
392
|
\`[{{currentTimestamp}}] <기억 할 내용>\`
|
|
673
393
|
- [ ] \`<기억 할 내용>\`에는 완벽한 컨텍스트 인계를 위해 오늘 완료한 핵심 작업 요약, 미해결 이슈(TODO), 중요 메모, 지식 노트 \`[[wikilink]]\` 등을 포함하세요.`;
|
|
674
394
|
var DEFAULT_NOTE_GUIDE = `# 지식 노트 작성 및 관리 가이드
|
|
675
395
|
|
|
396
|
+
- [ ] **노트 생성**: \`context-mcp_create_knowledge_note\` 도구를 사용하여 생성하세요.
|
|
676
397
|
- [ ] 제텔카스텐(Zettelkasten) 3대 원칙 준수:
|
|
677
398
|
- [ ] 원자성: 한 노트당 한 주제
|
|
678
399
|
- [ ] 연결: 고립된 노트 방지
|
|
@@ -745,25 +466,21 @@ var TEMPLATE_FILES = {
|
|
|
745
466
|
"work-complete.txt": DEFAULT_WORK_COMPLETE_TEMPLATE
|
|
746
467
|
};
|
|
747
468
|
function scaffoldIfNeeded(projectDir) {
|
|
748
|
-
const contextDir =
|
|
749
|
-
if (
|
|
469
|
+
const contextDir = join3(projectDir, resolveContextDir(projectDir));
|
|
470
|
+
if (existsSync2(contextDir)) {
|
|
750
471
|
return false;
|
|
751
472
|
}
|
|
752
473
|
try {
|
|
753
|
-
const
|
|
754
|
-
mkdirSync(promptsDir, { recursive: true });
|
|
755
|
-
const templatesDir = join4(contextDir, "templates");
|
|
474
|
+
const templatesDir = join3(contextDir, "templates");
|
|
756
475
|
mkdirSync(templatesDir, { recursive: true });
|
|
757
|
-
const guidesDir =
|
|
476
|
+
const guidesDir = join3(contextDir, "guides");
|
|
758
477
|
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");
|
|
478
|
+
writeFileSync(join3(contextDir, "config.jsonc"), DEFAULT_CONFIG, "utf-8");
|
|
762
479
|
for (const [filename, content] of Object.entries(TEMPLATE_FILES)) {
|
|
763
|
-
writeFileSync(
|
|
480
|
+
writeFileSync(join3(templatesDir, filename), content, "utf-8");
|
|
764
481
|
}
|
|
765
482
|
for (const [filename, content] of Object.entries(GUIDE_FILES)) {
|
|
766
|
-
writeFileSync(
|
|
483
|
+
writeFileSync(join3(guidesDir, filename), content, "utf-8");
|
|
767
484
|
}
|
|
768
485
|
writeVersion(contextDir, PLUGIN_VERSION);
|
|
769
486
|
return true;
|
|
@@ -772,11 +489,11 @@ function scaffoldIfNeeded(projectDir) {
|
|
|
772
489
|
}
|
|
773
490
|
}
|
|
774
491
|
function writeVersion(contextDir, version) {
|
|
775
|
-
writeFileSync(
|
|
492
|
+
writeFileSync(join3(contextDir, ".version"), version, "utf-8");
|
|
776
493
|
}
|
|
777
494
|
|
|
778
495
|
// src/omx/agents-md.ts
|
|
779
|
-
import { existsSync as
|
|
496
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
780
497
|
import { dirname } from "node:path";
|
|
781
498
|
var START_MARKER = "<!-- context:start -->";
|
|
782
499
|
var END_MARKER = "<!-- context:end -->";
|
|
@@ -819,11 +536,11 @@ function writeFileAtomically(filePath, content) {
|
|
|
819
536
|
}
|
|
820
537
|
function injectIntoAgentsMd(agentsMdPath, content) {
|
|
821
538
|
mkdirSync2(dirname(agentsMdPath), { recursive: true });
|
|
822
|
-
if (!
|
|
539
|
+
if (!existsSync3(agentsMdPath)) {
|
|
823
540
|
writeFileAtomically(agentsMdPath, renderMarkerBlock(content, true));
|
|
824
541
|
return;
|
|
825
542
|
}
|
|
826
|
-
const existingContent =
|
|
543
|
+
const existingContent = readFileSync3(agentsMdPath, "utf-8");
|
|
827
544
|
const nextContent = existingContent.includes(START_MARKER) && existingContent.includes(END_MARKER) ? replaceMarkerBlock(existingContent, content) : appendMarkerBlock(existingContent, content);
|
|
828
545
|
writeFileAtomically(agentsMdPath, nextContent);
|
|
829
546
|
}
|
|
@@ -894,21 +611,6 @@ function clearCurrentFollowupScope(pendingScopes, scopeKey) {
|
|
|
894
611
|
function resolveProjectDir(event) {
|
|
895
612
|
return event.context?.projectDir ?? event.context?.directory ?? process.cwd();
|
|
896
613
|
}
|
|
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
614
|
function resolveFollowupScopeKey(event) {
|
|
913
615
|
if (event.session_id && event.session_id.trim().length > 0) {
|
|
914
616
|
return `session:${event.session_id.trim()}`;
|
|
@@ -925,27 +627,44 @@ async function logWarn(sdk, message, meta = {}) {
|
|
|
925
627
|
}
|
|
926
628
|
await sdk.log.info(message, meta);
|
|
927
629
|
}
|
|
630
|
+
var STATIC_KNOWLEDGE_CONTEXT = `## Knowledge Context
|
|
631
|
+
|
|
632
|
+
이 프로젝트는 **제텔카스텐(Zettelkasten)** 방식으로 지식을 관리합니다.
|
|
633
|
+
세션 간 컨텍스트를 보존하여, 이전 세션의 결정/패턴/실수가 다음 세션에서 재활용됩니다.
|
|
634
|
+
|
|
635
|
+
### 제텔카스텐 핵심 원칙
|
|
636
|
+
1. **원자성** -- 하나의 노트 = 하나의 주제. 여러 주제를 섞지 마세요.
|
|
637
|
+
2. **연결** -- 모든 노트는 [[wikilink]]로 관련 노트에 연결. 고립된 노트는 발견되지 않습니다.
|
|
638
|
+
3. **자기 언어** -- 복사-붙여넣기가 아닌, 핵심을 이해하고 간결하게 서술하세요.
|
|
639
|
+
|
|
640
|
+
### MCP Tools
|
|
641
|
+
- **지식 관리**: \`context-mcp_search_knowledge\`, \`context-mcp_read_knowledge\`, \`context-mcp_create_knowledge_note\`, \`context-mcp_update_knowledge_note\`
|
|
642
|
+
- **데일리 노트**: \`context-mcp_read_daily_note\`, \`context-mcp_append_daily_note\`
|
|
643
|
+
- **작업 완료**: \`context-mcp_submit_turn_complete\` (작업 종료 시 필수 호출)
|
|
644
|
+
|
|
645
|
+
### 작업 전 필수
|
|
646
|
+
- **데일리 노트 확인**: 가장 최근의 데일리 노트를 읽고 이전 세션의 컨텍스트와 미해결 이슈를 파악하세요.
|
|
647
|
+
- **작업 의도 선언**: 작업 시작 전, 현재 세션의 목표와 작업 의도를 명확히 파악하고 선언하세요.
|
|
648
|
+
- **지식 검색**: 작업과 관련된 문서를 **직접 먼저** 검색하고 읽으세요.
|
|
649
|
+
- 지식 파일에 기록된 아키텍처 결정, 패턴, 제약사항을 반드시 따르세요.
|
|
650
|
+
|
|
651
|
+
### 개발 원칙
|
|
652
|
+
- **TDD** (Test-Driven Development): 테스트를 먼저 작성하고(RED), 구현하여 통과시킨 뒤(GREEN), 리팩토링하세요.
|
|
653
|
+
- **DDD** (Domain-Driven Design): 도메인 개념을 코드 구조에 반영하세요.
|
|
654
|
+
- **테스트 커버리지**: 새로 작성하거나 변경한 코드는 테스트 커버리지 80% 이상을 목표로 하세요.
|
|
655
|
+
|
|
656
|
+
### 우선순위
|
|
657
|
+
- AGENTS.md의 지시사항이 항상 최우선
|
|
658
|
+
- 지식 노트의 결정사항 > 일반적 관행
|
|
659
|
+
- 지식 노트에 없는 새로운 결정이나 반복 가치가 있는 발견은 작업 메모나 지식 노트 후보로 기록하세요.`;
|
|
928
660
|
async function onSessionStart(event, sdk) {
|
|
929
661
|
const projectDir = resolveProjectDir(event);
|
|
930
|
-
const contextDir = resolveContextDir(projectDir);
|
|
931
662
|
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);
|
|
663
|
+
injectIntoAgentsMd(join4(projectDir, "AGENTS.md"), STATIC_KNOWLEDGE_CONTEXT);
|
|
944
664
|
await sdk.log.info(`Injected context into AGENTS.md for ${projectDir}`);
|
|
945
665
|
}
|
|
946
666
|
async function onTurnComplete(event, sdk) {
|
|
947
667
|
const projectDir = resolveProjectDir(event);
|
|
948
|
-
const contextDir = resolveContextDir(projectDir);
|
|
949
668
|
const config = loadConfig(projectDir);
|
|
950
669
|
const strategy = config.omx?.turnEnd?.strategy ?? "off";
|
|
951
670
|
if (strategy !== "turn-complete-sendkeys") {
|
|
@@ -968,9 +687,9 @@ async function onTurnComplete(event, sdk) {
|
|
|
968
687
|
}
|
|
969
688
|
const followupScopeKey = resolveFollowupScopeKey(event);
|
|
970
689
|
let pendingFollowupScopes = typeof sdk.state?.read === "function" ? await sdk.state.read(TURN_END_PENDING_SKIP_KEY, {}) ?? {} : {};
|
|
971
|
-
const workCompleteFile =
|
|
972
|
-
if (
|
|
973
|
-
const content =
|
|
690
|
+
const workCompleteFile = join4(projectDir, DEFAULTS.workCompleteFile);
|
|
691
|
+
if (existsSync4(workCompleteFile)) {
|
|
692
|
+
const content = readFileSync4(workCompleteFile, "utf-8");
|
|
974
693
|
const { sessionId: fileSessionId, turnId: fileTurnId } = parseWorkComplete(content);
|
|
975
694
|
const currentScopeId = event.session_id ?? event.thread_id ?? "";
|
|
976
695
|
if (!fileSessionId || fileSessionId === currentScopeId) {
|
|
@@ -1022,28 +741,6 @@ async function onTurnComplete(event, sdk) {
|
|
|
1022
741
|
});
|
|
1023
742
|
return;
|
|
1024
743
|
}
|
|
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
744
|
if (typeof sdk.tmux?.sendKeys !== "function") {
|
|
1048
745
|
await logWarn(sdk, "turn_end_sendkeys_failed", {
|
|
1049
746
|
event: event.event,
|
|
@@ -1053,7 +750,7 @@ async function onTurnComplete(event, sdk) {
|
|
|
1053
750
|
});
|
|
1054
751
|
return;
|
|
1055
752
|
}
|
|
1056
|
-
const turnEnd =
|
|
753
|
+
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
754
|
const reminderText = `<system-reminder>
|
|
1058
755
|
${turnEnd}
|
|
1059
756
|
</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.25",
|
|
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": {
|