@ksm0709/context 0.0.21 → 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 +8 -116
- package/dist/index.js +65 -1227
- package/dist/mcp.js +33061 -0
- package/dist/omx/index.mjs +53 -352
- 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,12 +163,8 @@ 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
|
-
"dir": "
|
|
167
|
+
"dir": ".context/memory",
|
|
401
168
|
"sources": ["AGENTS.md"]
|
|
402
169
|
},
|
|
403
170
|
"omx": {
|
|
@@ -407,52 +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
|
-
1. **계속 작업**: 기존 작업이 완료되지 않았고 아직 아래 액션을 취할 단계가 아니라면 작업 속개.
|
|
448
|
-
2. **데일리 노트 기록**: [.context/guides/daily-note-guide.md] 데일리 노트에 중요한 컨텍스트를 기록하여 다음 세션이나 에이전트 팀이 참고할 수 있도록 하세요. 기존 내용 수정은 불가하며, 새로운 메모를 추가 하는것만 가능합니다. 간략한 한 두 문장으로 작성하여 핵심 컨텍스트가 명확히 전달되도록 하세요.
|
|
449
|
-
3. **지식 노트 작성**: [.context/guides/note-guide.md] 작업기억(데일리노트, 세션 컨텍스트)보다 오래 기억되어야 하는 중요한 결정, 패턴, 실수, 발견은 지식 노트로 기록하여 프로젝트의 집단 지식으로 남기세요.
|
|
450
|
-
4. **노트/스킬 검색 및 읽기**: [.context/guides/search-guide.md] 어려움에 처했다면 현재 진행 상황에 필요한 지식이나 스킬이 있는지 확인하고, 관련 노트를 읽어보세요. 새로운 아이디어나 해결책이 떠오를 수 있습니다.
|
|
451
|
-
5. **작업 경로 리뷰**: [.context/guides/scope-review.md] 사용자가 의도한 작업 범위를 벗어나지 않았는지, 작업이 너무 크거나 복잡해지지는 않았는지 검토하세요.
|
|
452
|
-
6. **체크포인트 커밋**: [.context/guides/commit-guide.md] 작업이 길어질 경우, 중요한 단계마다 체크포인트 커밋을 하여 작업 내용을 안전하게 저장하고, 필요 시 이전 상태로 돌아갈 수 있도록 하세요.
|
|
453
|
-
7. **퀄리티 검증**: [.context/guides/quality-check.md] **작업 완료 전에 반드시 수행하세요**. 코드 린트, 포맷터, 테스트, 빌드, 코드리뷰를 실행하여 작업 결과물이 프로젝트의 품질 기준을 충족하는지 확인하세요.
|
|
454
|
-
8. **작업 완료**: [.context/guides/complete-guide.md] 모든 작업이 완료되었다면, 이 가이드를 따르세요. 이 작업 지침이 더이상 트리거되지 않을 것입니다.
|
|
455
|
-
`;
|
|
456
177
|
var DEFAULT_ADR_TEMPLATE = `# ADR-NNN: [제목]
|
|
457
178
|
|
|
458
179
|
## 상태
|
|
@@ -744,25 +465,21 @@ var TEMPLATE_FILES = {
|
|
|
744
465
|
"work-complete.txt": DEFAULT_WORK_COMPLETE_TEMPLATE
|
|
745
466
|
};
|
|
746
467
|
function scaffoldIfNeeded(projectDir) {
|
|
747
|
-
const contextDir =
|
|
748
|
-
if (
|
|
468
|
+
const contextDir = join3(projectDir, resolveContextDir(projectDir));
|
|
469
|
+
if (existsSync2(contextDir)) {
|
|
749
470
|
return false;
|
|
750
471
|
}
|
|
751
472
|
try {
|
|
752
|
-
const
|
|
753
|
-
mkdirSync(promptsDir, { recursive: true });
|
|
754
|
-
const templatesDir = join4(contextDir, "templates");
|
|
473
|
+
const templatesDir = join3(contextDir, "templates");
|
|
755
474
|
mkdirSync(templatesDir, { recursive: true });
|
|
756
|
-
const guidesDir =
|
|
475
|
+
const guidesDir = join3(contextDir, "guides");
|
|
757
476
|
mkdirSync(guidesDir, { recursive: true });
|
|
758
|
-
writeFileSync(
|
|
759
|
-
writeFileSync(join4(promptsDir, DEFAULTS.turnStartFile), DEFAULT_TURN_START, "utf-8");
|
|
760
|
-
writeFileSync(join4(promptsDir, DEFAULTS.turnEndFile), DEFAULT_TURN_END, "utf-8");
|
|
477
|
+
writeFileSync(join3(contextDir, "config.jsonc"), DEFAULT_CONFIG, "utf-8");
|
|
761
478
|
for (const [filename, content] of Object.entries(TEMPLATE_FILES)) {
|
|
762
|
-
writeFileSync(
|
|
479
|
+
writeFileSync(join3(templatesDir, filename), content, "utf-8");
|
|
763
480
|
}
|
|
764
481
|
for (const [filename, content] of Object.entries(GUIDE_FILES)) {
|
|
765
|
-
writeFileSync(
|
|
482
|
+
writeFileSync(join3(guidesDir, filename), content, "utf-8");
|
|
766
483
|
}
|
|
767
484
|
writeVersion(contextDir, PLUGIN_VERSION);
|
|
768
485
|
return true;
|
|
@@ -771,11 +488,11 @@ function scaffoldIfNeeded(projectDir) {
|
|
|
771
488
|
}
|
|
772
489
|
}
|
|
773
490
|
function writeVersion(contextDir, version) {
|
|
774
|
-
writeFileSync(
|
|
491
|
+
writeFileSync(join3(contextDir, ".version"), version, "utf-8");
|
|
775
492
|
}
|
|
776
493
|
|
|
777
494
|
// src/omx/agents-md.ts
|
|
778
|
-
import { existsSync as
|
|
495
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
779
496
|
import { dirname } from "node:path";
|
|
780
497
|
var START_MARKER = "<!-- context:start -->";
|
|
781
498
|
var END_MARKER = "<!-- context:end -->";
|
|
@@ -818,11 +535,11 @@ function writeFileAtomically(filePath, content) {
|
|
|
818
535
|
}
|
|
819
536
|
function injectIntoAgentsMd(agentsMdPath, content) {
|
|
820
537
|
mkdirSync2(dirname(agentsMdPath), { recursive: true });
|
|
821
|
-
if (!
|
|
538
|
+
if (!existsSync3(agentsMdPath)) {
|
|
822
539
|
writeFileAtomically(agentsMdPath, renderMarkerBlock(content, true));
|
|
823
540
|
return;
|
|
824
541
|
}
|
|
825
|
-
const existingContent =
|
|
542
|
+
const existingContent = readFileSync3(agentsMdPath, "utf-8");
|
|
826
543
|
const nextContent = existingContent.includes(START_MARKER) && existingContent.includes(END_MARKER) ? replaceMarkerBlock(existingContent, content) : appendMarkerBlock(existingContent, content);
|
|
827
544
|
writeFileAtomically(agentsMdPath, nextContent);
|
|
828
545
|
}
|
|
@@ -893,21 +610,6 @@ function clearCurrentFollowupScope(pendingScopes, scopeKey) {
|
|
|
893
610
|
function resolveProjectDir(event) {
|
|
894
611
|
return event.context?.projectDir ?? event.context?.directory ?? process.cwd();
|
|
895
612
|
}
|
|
896
|
-
function resolvePromptPath(directory, contextDir, promptPath) {
|
|
897
|
-
if (isAbsolute(promptPath))
|
|
898
|
-
return promptPath;
|
|
899
|
-
if (promptPath.startsWith(".context/") || promptPath.startsWith(".opencode/")) {
|
|
900
|
-
return join5(directory, promptPath);
|
|
901
|
-
}
|
|
902
|
-
return join5(directory, contextDir, promptPath);
|
|
903
|
-
}
|
|
904
|
-
function buildPromptVars(config) {
|
|
905
|
-
return { knowledgeDir: config.knowledge.dir ?? DEFAULTS.knowledgeDir };
|
|
906
|
-
}
|
|
907
|
-
function buildKnowledgeContent(projectDir, config) {
|
|
908
|
-
const knowledgeIndex = buildKnowledgeIndexV2(projectDir, config.knowledge);
|
|
909
|
-
return knowledgeIndex.mode === "flat" ? formatKnowledgeIndex(knowledgeIndex.individualFiles) : formatDomainIndex(knowledgeIndex);
|
|
910
|
-
}
|
|
911
613
|
function resolveFollowupScopeKey(event) {
|
|
912
614
|
if (event.session_id && event.session_id.trim().length > 0) {
|
|
913
615
|
return `session:${event.session_id.trim()}`;
|
|
@@ -924,27 +626,39 @@ async function logWarn(sdk, message, meta = {}) {
|
|
|
924
626
|
}
|
|
925
627
|
await sdk.log.info(message, meta);
|
|
926
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)`;
|
|
927
654
|
async function onSessionStart(event, sdk) {
|
|
928
655
|
const projectDir = resolveProjectDir(event);
|
|
929
|
-
const contextDir = resolveContextDir(projectDir);
|
|
930
656
|
scaffoldIfNeeded(projectDir);
|
|
931
|
-
|
|
932
|
-
const promptVars = buildPromptVars(config);
|
|
933
|
-
const turnStartPath = resolvePromptPath(projectDir, contextDir, config.prompts.turnStart ?? join5(DEFAULTS.promptDir, DEFAULTS.turnStartFile));
|
|
934
|
-
const turnStart = resolvePromptVariables(readPromptFile(turnStartPath), promptVars);
|
|
935
|
-
const indexContent = buildKnowledgeContent(projectDir, config);
|
|
936
|
-
const combinedContent = [turnStart, indexContent].filter(Boolean).join(`
|
|
937
|
-
|
|
938
|
-
`);
|
|
939
|
-
if (!combinedContent) {
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
injectIntoAgentsMd(join5(projectDir, "AGENTS.md"), combinedContent);
|
|
657
|
+
injectIntoAgentsMd(join4(projectDir, "AGENTS.md"), STATIC_KNOWLEDGE_CONTEXT);
|
|
943
658
|
await sdk.log.info(`Injected context into AGENTS.md for ${projectDir}`);
|
|
944
659
|
}
|
|
945
660
|
async function onTurnComplete(event, sdk) {
|
|
946
661
|
const projectDir = resolveProjectDir(event);
|
|
947
|
-
const contextDir = resolveContextDir(projectDir);
|
|
948
662
|
const config = loadConfig(projectDir);
|
|
949
663
|
const strategy = config.omx?.turnEnd?.strategy ?? "off";
|
|
950
664
|
if (strategy !== "turn-complete-sendkeys") {
|
|
@@ -967,9 +681,9 @@ async function onTurnComplete(event, sdk) {
|
|
|
967
681
|
}
|
|
968
682
|
const followupScopeKey = resolveFollowupScopeKey(event);
|
|
969
683
|
let pendingFollowupScopes = typeof sdk.state?.read === "function" ? await sdk.state.read(TURN_END_PENDING_SKIP_KEY, {}) ?? {} : {};
|
|
970
|
-
const workCompleteFile =
|
|
971
|
-
if (
|
|
972
|
-
const content =
|
|
684
|
+
const workCompleteFile = join4(projectDir, DEFAULTS.workCompleteFile);
|
|
685
|
+
if (existsSync4(workCompleteFile)) {
|
|
686
|
+
const content = readFileSync4(workCompleteFile, "utf-8");
|
|
973
687
|
const { sessionId: fileSessionId, turnId: fileTurnId } = parseWorkComplete(content);
|
|
974
688
|
const currentScopeId = event.session_id ?? event.thread_id ?? "";
|
|
975
689
|
if (!fileSessionId || fileSessionId === currentScopeId) {
|
|
@@ -1021,21 +735,6 @@ async function onTurnComplete(event, sdk) {
|
|
|
1021
735
|
});
|
|
1022
736
|
return;
|
|
1023
737
|
}
|
|
1024
|
-
const promptVars = {
|
|
1025
|
-
...buildPromptVars(config),
|
|
1026
|
-
sessionId: event.session_id ?? event.thread_id ?? "unknown",
|
|
1027
|
-
turnId: event.turn_id ?? ""
|
|
1028
|
-
};
|
|
1029
|
-
const turnEndPath = resolvePromptPath(projectDir, contextDir, config.prompts.turnEnd ?? join5(DEFAULTS.promptDir, DEFAULTS.turnEndFile));
|
|
1030
|
-
const turnEndRaw = readPromptFile(turnEndPath);
|
|
1031
|
-
if (!turnEndRaw) {
|
|
1032
|
-
await sdk.log.info("turn_end_skipped_empty_prompt", {
|
|
1033
|
-
event: event.event,
|
|
1034
|
-
session_id: event.session_id,
|
|
1035
|
-
turn_id: event.turn_id
|
|
1036
|
-
});
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
738
|
if (typeof sdk.tmux?.sendKeys !== "function") {
|
|
1040
739
|
await logWarn(sdk, "turn_end_sendkeys_failed", {
|
|
1041
740
|
event: event.event,
|
|
@@ -1045,11 +744,13 @@ async function onTurnComplete(event, sdk) {
|
|
|
1045
744
|
});
|
|
1046
745
|
return;
|
|
1047
746
|
}
|
|
1048
|
-
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.";
|
|
1049
748
|
const reminderText = `<system-reminder>
|
|
1050
749
|
${turnEnd}
|
|
1051
750
|
</system-reminder>`;
|
|
1052
751
|
const sessionName = typeof event.context?.session_name === "string" && event.context.session_name.trim().length > 0 ? event.context.session_name.trim() : undefined;
|
|
752
|
+
await new Promise((resolve) => globalThis.setTimeout(resolve, 500));
|
|
753
|
+
if (typeof sdk.tmux?.sendKeys === "function") {}
|
|
1053
754
|
const result = await sdk.tmux.sendKeys({
|
|
1054
755
|
sessionName,
|
|
1055
756
|
text: reminderText,
|
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": {
|