planmode 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +408 -7
- package/dist/mcp.js +491 -115
- package/package.json +1 -1
- package/src/commands/context.ts +111 -0
- package/src/commands/interactive.ts +107 -0
- package/src/index.ts +3 -1
- package/src/lib/context.ts +265 -0
- package/src/mcp.ts +146 -0
- package/src/types/index.ts +28 -0
package/dist/mcp.js
CHANGED
|
@@ -1,112 +1,361 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
9
11
|
|
|
10
12
|
// src/lib/logger.ts
|
|
11
|
-
var RESET = "\x1B[0m";
|
|
12
|
-
var RED = "\x1B[31m";
|
|
13
|
-
var GREEN = "\x1B[32m";
|
|
14
|
-
var YELLOW = "\x1B[33m";
|
|
15
|
-
var CYAN = "\x1B[36m";
|
|
16
|
-
var DIM = "\x1B[2m";
|
|
17
|
-
var BOLD = "\x1B[1m";
|
|
18
13
|
function stripAnsi(str) {
|
|
19
14
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
20
15
|
}
|
|
21
|
-
var capturing
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
var RESET, RED, GREEN, YELLOW, CYAN, DIM, BOLD, capturing, captured, logger;
|
|
17
|
+
var init_logger = __esm({
|
|
18
|
+
"src/lib/logger.ts"() {
|
|
19
|
+
"use strict";
|
|
20
|
+
RESET = "\x1B[0m";
|
|
21
|
+
RED = "\x1B[31m";
|
|
22
|
+
GREEN = "\x1B[32m";
|
|
23
|
+
YELLOW = "\x1B[33m";
|
|
24
|
+
CYAN = "\x1B[36m";
|
|
25
|
+
DIM = "\x1B[2m";
|
|
26
|
+
BOLD = "\x1B[1m";
|
|
31
27
|
capturing = false;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
28
|
+
captured = [];
|
|
29
|
+
logger = {
|
|
30
|
+
capture() {
|
|
31
|
+
capturing = true;
|
|
32
|
+
captured = [];
|
|
33
|
+
},
|
|
34
|
+
flush() {
|
|
35
|
+
const messages = captured;
|
|
36
|
+
captured = [];
|
|
37
|
+
capturing = false;
|
|
38
|
+
return messages;
|
|
39
|
+
},
|
|
40
|
+
isCapturing() {
|
|
41
|
+
return capturing;
|
|
42
|
+
},
|
|
43
|
+
info(msg) {
|
|
44
|
+
const text2 = `info ${msg}`;
|
|
45
|
+
if (capturing) {
|
|
46
|
+
captured.push(stripAnsi(text2));
|
|
47
|
+
} else {
|
|
48
|
+
console.log(`${CYAN}info${RESET} ${msg}`);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
success(msg) {
|
|
52
|
+
const text2 = `\u2713 ${msg}`;
|
|
53
|
+
if (capturing) {
|
|
54
|
+
captured.push(stripAnsi(text2));
|
|
55
|
+
} else {
|
|
56
|
+
console.log(`${GREEN}\u2713${RESET} ${msg}`);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
warn(msg) {
|
|
60
|
+
const text2 = `warn ${msg}`;
|
|
61
|
+
if (capturing) {
|
|
62
|
+
captured.push(stripAnsi(text2));
|
|
63
|
+
} else {
|
|
64
|
+
console.log(`${YELLOW}warn${RESET} ${msg}`);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
error(msg) {
|
|
68
|
+
const text2 = `error ${msg}`;
|
|
69
|
+
if (capturing) {
|
|
70
|
+
captured.push(stripAnsi(text2));
|
|
71
|
+
} else {
|
|
72
|
+
console.error(`${RED}error${RESET} ${msg}`);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
dim(msg) {
|
|
76
|
+
if (capturing) {
|
|
77
|
+
captured.push(msg);
|
|
78
|
+
} else {
|
|
79
|
+
console.log(`${DIM}${msg}${RESET}`);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
bold(msg) {
|
|
83
|
+
if (capturing) {
|
|
84
|
+
captured.push(msg);
|
|
85
|
+
} else {
|
|
86
|
+
console.log(`${BOLD}${msg}${RESET}`);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
table(headers, rows) {
|
|
90
|
+
const colWidths = headers.map(
|
|
91
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
92
|
+
);
|
|
93
|
+
const header = headers.map((h, i) => h.toUpperCase().padEnd(colWidths[i])).join(" ");
|
|
94
|
+
if (capturing) {
|
|
95
|
+
captured.push(` ${header}`);
|
|
96
|
+
for (const row of rows) {
|
|
97
|
+
const line = row.map((cell, i) => cell.padEnd(colWidths[i])).join(" ");
|
|
98
|
+
captured.push(` ${line}`);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
console.log(` ${DIM}${header}${RESET}`);
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const line = row.map((cell, i) => cell.padEnd(colWidths[i])).join(" ");
|
|
104
|
+
console.log(` ${line}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
blank() {
|
|
109
|
+
if (capturing) {
|
|
110
|
+
captured.push("");
|
|
111
|
+
} else {
|
|
112
|
+
console.log();
|
|
113
|
+
}
|
|
93
114
|
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// src/lib/context.ts
|
|
120
|
+
var context_exports = {};
|
|
121
|
+
__export(context_exports, {
|
|
122
|
+
addContextRepo: () => addContextRepo,
|
|
123
|
+
formatSize: () => formatSize,
|
|
124
|
+
getContextSummary: () => getContextSummary,
|
|
125
|
+
readContextIndex: () => readContextIndex,
|
|
126
|
+
reindexContext: () => reindexContext,
|
|
127
|
+
removeContextRepo: () => removeContextRepo,
|
|
128
|
+
walkDirectory: () => walkDirectory,
|
|
129
|
+
writeContextIndex: () => writeContextIndex
|
|
130
|
+
});
|
|
131
|
+
import fs12 from "fs";
|
|
132
|
+
import path12 from "path";
|
|
133
|
+
import { parse as parse4, stringify as stringify6 } from "yaml";
|
|
134
|
+
function getContextPath(projectDir) {
|
|
135
|
+
return path12.join(projectDir, CONTEXT_DIR, CONTEXT_FILE);
|
|
136
|
+
}
|
|
137
|
+
function emptyIndex() {
|
|
138
|
+
return { version: 1, repos: [] };
|
|
139
|
+
}
|
|
140
|
+
function readContextIndex(projectDir = process.cwd()) {
|
|
141
|
+
const filePath = getContextPath(projectDir);
|
|
142
|
+
try {
|
|
143
|
+
const raw = fs12.readFileSync(filePath, "utf-8");
|
|
144
|
+
const data = parse4(raw);
|
|
145
|
+
return data ?? emptyIndex();
|
|
146
|
+
} catch {
|
|
147
|
+
return emptyIndex();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function writeContextIndex(index, projectDir = process.cwd()) {
|
|
151
|
+
const dirPath = path12.join(projectDir, CONTEXT_DIR);
|
|
152
|
+
fs12.mkdirSync(dirPath, { recursive: true });
|
|
153
|
+
const filePath = getContextPath(projectDir);
|
|
154
|
+
fs12.writeFileSync(filePath, stringify6(index), "utf-8");
|
|
155
|
+
}
|
|
156
|
+
function walkDirectory(dirPath) {
|
|
157
|
+
const files = [];
|
|
158
|
+
function walk(currentPath) {
|
|
159
|
+
let entries;
|
|
160
|
+
try {
|
|
161
|
+
entries = fs12.readdirSync(currentPath, { withFileTypes: true });
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (entry.name.startsWith(".") && IGNORED_DIRS.has(entry.name)) continue;
|
|
167
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
168
|
+
const fullPath = path12.join(currentPath, entry.name);
|
|
169
|
+
if (entry.isDirectory()) {
|
|
170
|
+
walk(fullPath);
|
|
171
|
+
} else if (entry.isFile()) {
|
|
172
|
+
const ext = path12.extname(entry.name).toLowerCase();
|
|
173
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
|
|
174
|
+
try {
|
|
175
|
+
const stat = fs12.statSync(fullPath);
|
|
176
|
+
const relativePath = path12.relative(dirPath, fullPath);
|
|
177
|
+
files.push({
|
|
178
|
+
path: relativePath,
|
|
179
|
+
extension: ext,
|
|
180
|
+
size: stat.size,
|
|
181
|
+
modified_at: stat.mtime.toISOString()
|
|
182
|
+
});
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
99
185
|
}
|
|
100
186
|
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
187
|
+
}
|
|
188
|
+
walk(dirPath);
|
|
189
|
+
return files;
|
|
190
|
+
}
|
|
191
|
+
function addContextRepo(repoPath, options = {}) {
|
|
192
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
193
|
+
const absolutePath = path12.resolve(projectDir, repoPath);
|
|
194
|
+
if (!fs12.existsSync(absolutePath)) {
|
|
195
|
+
throw new Error(`Directory not found: ${repoPath}`);
|
|
196
|
+
}
|
|
197
|
+
if (!fs12.statSync(absolutePath).isDirectory()) {
|
|
198
|
+
throw new Error(`Not a directory: ${repoPath}`);
|
|
199
|
+
}
|
|
200
|
+
const index = readContextIndex(projectDir);
|
|
201
|
+
const relative = path12.relative(projectDir, absolutePath);
|
|
202
|
+
const isInsideProject = !relative.startsWith("..") && !path12.isAbsolute(relative);
|
|
203
|
+
const storedPath = isInsideProject ? relative : absolutePath;
|
|
204
|
+
const existing = index.repos.find(
|
|
205
|
+
(r) => r.repo.path === storedPath || r.repo.name === options.name
|
|
206
|
+
);
|
|
207
|
+
if (existing) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Context repo already exists: ${existing.repo.name ?? existing.repo.path}. Use \`planmode context reindex\` to refresh.`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
logger.info(`Scanning ${absolutePath}...`);
|
|
213
|
+
const files = walkDirectory(absolutePath);
|
|
214
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
215
|
+
const repoIndex = {
|
|
216
|
+
repo: {
|
|
217
|
+
path: storedPath,
|
|
218
|
+
name: options.name,
|
|
219
|
+
added_at: now
|
|
220
|
+
},
|
|
221
|
+
files,
|
|
222
|
+
indexed_at: now,
|
|
223
|
+
file_count: files.length,
|
|
224
|
+
total_size: files.reduce((sum, f) => sum + f.size, 0)
|
|
225
|
+
};
|
|
226
|
+
index.repos.push(repoIndex);
|
|
227
|
+
writeContextIndex(index, projectDir);
|
|
228
|
+
logger.success(`Added "${options.name ?? storedPath}" \u2014 ${files.length} file(s), ${formatSize(repoIndex.total_size)}`);
|
|
229
|
+
const breakdown = getTypeBreakdown(files);
|
|
230
|
+
if (breakdown.length > 0) {
|
|
231
|
+
logger.dim(` ${breakdown.join(", ")}`);
|
|
232
|
+
}
|
|
233
|
+
return repoIndex;
|
|
234
|
+
}
|
|
235
|
+
function removeContextRepo(pathOrName, projectDir = process.cwd()) {
|
|
236
|
+
const index = readContextIndex(projectDir);
|
|
237
|
+
const idx = index.repos.findIndex(
|
|
238
|
+
(r) => r.repo.path === pathOrName || r.repo.name === pathOrName
|
|
239
|
+
);
|
|
240
|
+
if (idx === -1) {
|
|
241
|
+
throw new Error(`Context repo not found: ${pathOrName}`);
|
|
242
|
+
}
|
|
243
|
+
const removed = index.repos[idx];
|
|
244
|
+
index.repos.splice(idx, 1);
|
|
245
|
+
writeContextIndex(index, projectDir);
|
|
246
|
+
logger.success(`Removed "${removed.repo.name ?? removed.repo.path}"`);
|
|
247
|
+
}
|
|
248
|
+
function reindexContext(pathOrName, projectDir = process.cwd()) {
|
|
249
|
+
const index = readContextIndex(projectDir);
|
|
250
|
+
if (index.repos.length === 0) {
|
|
251
|
+
throw new Error("No context repos configured. Use `planmode context add <path>` first.");
|
|
252
|
+
}
|
|
253
|
+
const targets = pathOrName ? index.repos.filter(
|
|
254
|
+
(r) => r.repo.path === pathOrName || r.repo.name === pathOrName
|
|
255
|
+
) : index.repos;
|
|
256
|
+
if (pathOrName && targets.length === 0) {
|
|
257
|
+
throw new Error(`Context repo not found: ${pathOrName}`);
|
|
258
|
+
}
|
|
259
|
+
for (const repo of targets) {
|
|
260
|
+
const absolutePath = path12.resolve(projectDir, repo.repo.path);
|
|
261
|
+
if (!fs12.existsSync(absolutePath)) {
|
|
262
|
+
logger.warn(`Directory not found, skipping: ${repo.repo.path}`);
|
|
263
|
+
continue;
|
|
107
264
|
}
|
|
265
|
+
logger.info(`Re-scanning ${repo.repo.name ?? repo.repo.path}...`);
|
|
266
|
+
const files = walkDirectory(absolutePath);
|
|
267
|
+
repo.files = files;
|
|
268
|
+
repo.indexed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
269
|
+
repo.file_count = files.length;
|
|
270
|
+
repo.total_size = files.reduce((sum, f) => sum + f.size, 0);
|
|
271
|
+
logger.success(`Reindexed "${repo.repo.name ?? repo.repo.path}" \u2014 ${files.length} file(s), ${formatSize(repo.total_size)}`);
|
|
108
272
|
}
|
|
109
|
-
|
|
273
|
+
writeContextIndex(index, projectDir);
|
|
274
|
+
}
|
|
275
|
+
function getContextSummary(projectDir = process.cwd()) {
|
|
276
|
+
const index = readContextIndex(projectDir);
|
|
277
|
+
return {
|
|
278
|
+
totalRepos: index.repos.length,
|
|
279
|
+
totalFiles: index.repos.reduce((sum, r) => sum + r.file_count, 0),
|
|
280
|
+
totalSize: index.repos.reduce((sum, r) => sum + r.total_size, 0),
|
|
281
|
+
repos: index.repos.map((r) => ({
|
|
282
|
+
name: r.repo.name ?? r.repo.path,
|
|
283
|
+
path: r.repo.path,
|
|
284
|
+
fileCount: r.file_count,
|
|
285
|
+
totalSize: r.total_size,
|
|
286
|
+
typeBreakdown: getTypeBreakdown(r.files),
|
|
287
|
+
indexedAt: r.indexed_at
|
|
288
|
+
}))
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function getTypeBreakdown(files) {
|
|
292
|
+
const counts = /* @__PURE__ */ new Map();
|
|
293
|
+
for (const file of files) {
|
|
294
|
+
counts.set(file.extension, (counts.get(file.extension) ?? 0) + 1);
|
|
295
|
+
}
|
|
296
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]).map(([ext, count]) => `${ext}: ${count}`);
|
|
297
|
+
}
|
|
298
|
+
function formatSize(bytes) {
|
|
299
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
300
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
301
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
302
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
303
|
+
}
|
|
304
|
+
var CONTEXT_DIR, CONTEXT_FILE, SUPPORTED_EXTENSIONS, IGNORED_DIRS;
|
|
305
|
+
var init_context = __esm({
|
|
306
|
+
"src/lib/context.ts"() {
|
|
307
|
+
"use strict";
|
|
308
|
+
init_logger();
|
|
309
|
+
CONTEXT_DIR = ".planmode";
|
|
310
|
+
CONTEXT_FILE = "context.yaml";
|
|
311
|
+
SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
312
|
+
".txt",
|
|
313
|
+
".md",
|
|
314
|
+
".markdown",
|
|
315
|
+
".pdf",
|
|
316
|
+
".rtf",
|
|
317
|
+
".doc",
|
|
318
|
+
".docx",
|
|
319
|
+
".csv",
|
|
320
|
+
".tsv",
|
|
321
|
+
".json",
|
|
322
|
+
".yaml",
|
|
323
|
+
".yml",
|
|
324
|
+
".xml",
|
|
325
|
+
".html",
|
|
326
|
+
".htm",
|
|
327
|
+
".rst",
|
|
328
|
+
".org",
|
|
329
|
+
".tex",
|
|
330
|
+
".log"
|
|
331
|
+
]);
|
|
332
|
+
IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
333
|
+
"node_modules",
|
|
334
|
+
".git",
|
|
335
|
+
"dist",
|
|
336
|
+
"build",
|
|
337
|
+
".next",
|
|
338
|
+
"__pycache__",
|
|
339
|
+
".venv",
|
|
340
|
+
"venv",
|
|
341
|
+
".tox",
|
|
342
|
+
"target",
|
|
343
|
+
"out",
|
|
344
|
+
".cache",
|
|
345
|
+
".turbo",
|
|
346
|
+
"coverage",
|
|
347
|
+
".nyc_output"
|
|
348
|
+
]);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// src/mcp.ts
|
|
353
|
+
init_logger();
|
|
354
|
+
import fs13 from "fs";
|
|
355
|
+
import path13 from "path";
|
|
356
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
357
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
358
|
+
import { z } from "zod/v4";
|
|
110
359
|
|
|
111
360
|
// src/lib/registry.ts
|
|
112
361
|
import fs2 from "fs";
|
|
@@ -651,6 +900,9 @@ function coerceValue(raw, def) {
|
|
|
651
900
|
}
|
|
652
901
|
}
|
|
653
902
|
|
|
903
|
+
// src/lib/installer.ts
|
|
904
|
+
init_logger();
|
|
905
|
+
|
|
654
906
|
// src/lib/analytics.ts
|
|
655
907
|
var API_BASE = "https://api.planmode.org";
|
|
656
908
|
function trackDownload(packageName) {
|
|
@@ -1052,6 +1304,7 @@ function createPackage(options) {
|
|
|
1052
1304
|
}
|
|
1053
1305
|
|
|
1054
1306
|
// src/lib/publisher.ts
|
|
1307
|
+
init_logger();
|
|
1055
1308
|
async function publishPackage(options = {}) {
|
|
1056
1309
|
const cwd = options.projectDir ?? process.cwd();
|
|
1057
1310
|
const interactive = options.interactive ?? false;
|
|
@@ -1852,6 +2105,7 @@ function detectCategory(data) {
|
|
|
1852
2105
|
}
|
|
1853
2106
|
|
|
1854
2107
|
// src/mcp.ts
|
|
2108
|
+
init_context();
|
|
1855
2109
|
function withCapture(fn) {
|
|
1856
2110
|
logger.capture();
|
|
1857
2111
|
try {
|
|
@@ -2129,14 +2383,14 @@ server.registerTool(
|
|
|
2129
2383
|
const entry = lockfile.packages[packageName];
|
|
2130
2384
|
if (!entry) {
|
|
2131
2385
|
const candidates = [
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2386
|
+
path13.join(dir, "plans", `${packageName}.md`),
|
|
2387
|
+
path13.join(dir, ".claude", "rules", `${packageName}.md`),
|
|
2388
|
+
path13.join(dir, "prompts", `${packageName}.md`)
|
|
2135
2389
|
];
|
|
2136
2390
|
for (const candidate of candidates) {
|
|
2137
|
-
if (
|
|
2138
|
-
const content2 =
|
|
2139
|
-
const relativePath =
|
|
2391
|
+
if (fs13.existsSync(candidate)) {
|
|
2392
|
+
const content2 = fs13.readFileSync(candidate, "utf-8");
|
|
2393
|
+
const relativePath = path13.relative(dir, candidate);
|
|
2140
2394
|
return textResult(`# ${packageName}
|
|
2141
2395
|
**Location:** ${relativePath}
|
|
2142
2396
|
|
|
@@ -2150,14 +2404,14 @@ ${content2}`);
|
|
|
2150
2404
|
true
|
|
2151
2405
|
);
|
|
2152
2406
|
}
|
|
2153
|
-
const fullPath =
|
|
2154
|
-
if (!
|
|
2407
|
+
const fullPath = path13.join(dir, entry.installed_to);
|
|
2408
|
+
if (!fs13.existsSync(fullPath)) {
|
|
2155
2409
|
return textResult(
|
|
2156
2410
|
`Package '${packageName}' is in the lockfile but the file is missing at ${entry.installed_to}. Try reinstalling with planmode_install.`,
|
|
2157
2411
|
true
|
|
2158
2412
|
);
|
|
2159
2413
|
}
|
|
2160
|
-
const content =
|
|
2414
|
+
const content = fs13.readFileSync(fullPath, "utf-8");
|
|
2161
2415
|
return textResult(
|
|
2162
2416
|
`# ${packageName} (${entry.type} v${entry.version})
|
|
2163
2417
|
**Location:** ${entry.installed_to}
|
|
@@ -2344,17 +2598,17 @@ server.registerTool(
|
|
|
2344
2598
|
async ({ prompt: promptName, variables, projectDir }) => {
|
|
2345
2599
|
try {
|
|
2346
2600
|
const dir = projectDir ?? process.cwd();
|
|
2347
|
-
const localPath =
|
|
2348
|
-
const localManifestPath =
|
|
2601
|
+
const localPath = path13.join(dir, "prompts", `${promptName}.md`);
|
|
2602
|
+
const localManifestPath = path13.join(dir, "prompts", promptName, "planmode.yaml");
|
|
2349
2603
|
let content;
|
|
2350
2604
|
let manifest;
|
|
2351
|
-
if (
|
|
2352
|
-
const raw =
|
|
2605
|
+
if (fs13.existsSync(localManifestPath)) {
|
|
2606
|
+
const raw = fs13.readFileSync(localManifestPath, "utf-8");
|
|
2353
2607
|
manifest = parseManifest(raw);
|
|
2354
|
-
const promptDir =
|
|
2608
|
+
const promptDir = path13.join(dir, "prompts", promptName);
|
|
2355
2609
|
content = readPackageContent(promptDir, manifest);
|
|
2356
|
-
} else if (
|
|
2357
|
-
content =
|
|
2610
|
+
} else if (fs13.existsSync(localPath)) {
|
|
2611
|
+
content = fs13.readFileSync(localPath, "utf-8");
|
|
2358
2612
|
} else {
|
|
2359
2613
|
return textResult(
|
|
2360
2614
|
`Prompt '${promptName}' not found locally. Install it first using the planmode_install tool.`,
|
|
@@ -2536,6 +2790,128 @@ server.registerTool(
|
|
|
2536
2790
|
}
|
|
2537
2791
|
}
|
|
2538
2792
|
);
|
|
2793
|
+
server.registerTool(
|
|
2794
|
+
"planmode_context_add",
|
|
2795
|
+
{
|
|
2796
|
+
description: "Add a document directory to the project context. Indexes all text-based files (md, pdf, txt, json, yaml, csv, html, etc.) and stores their metadata in .planmode/context.yaml. The AI can then see what documents are available and read them on demand.",
|
|
2797
|
+
inputSchema: {
|
|
2798
|
+
path: z.string().describe("Path to the document directory (relative to project or absolute)"),
|
|
2799
|
+
name: z.string().optional().describe("Human-readable label for this directory"),
|
|
2800
|
+
projectDir: z.string().optional().describe("Project directory (default: current working directory)")
|
|
2801
|
+
}
|
|
2802
|
+
},
|
|
2803
|
+
async ({ path: dirPath, name, projectDir }) => {
|
|
2804
|
+
try {
|
|
2805
|
+
const { result, messages } = withCapture(
|
|
2806
|
+
() => addContextRepo(dirPath, { name, projectDir })
|
|
2807
|
+
);
|
|
2808
|
+
const breakdown = result.files.length > 0 ? `
|
|
2809
|
+
Types: ${getTypeBreakdownText(result)}` : "";
|
|
2810
|
+
return textResult(
|
|
2811
|
+
formatMessages(
|
|
2812
|
+
messages,
|
|
2813
|
+
`Added "${name ?? result.repo.path}" \u2014 ${result.file_count} file(s), ${formatSize(result.total_size)}${breakdown}`
|
|
2814
|
+
)
|
|
2815
|
+
);
|
|
2816
|
+
} catch (err) {
|
|
2817
|
+
return errorResult("Error adding context repo", err);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
);
|
|
2821
|
+
server.registerTool(
|
|
2822
|
+
"planmode_context_remove",
|
|
2823
|
+
{
|
|
2824
|
+
description: "Remove a document directory from the project context",
|
|
2825
|
+
inputSchema: {
|
|
2826
|
+
pathOrName: z.string().describe("Path or name of the context repo to remove"),
|
|
2827
|
+
projectDir: z.string().optional().describe("Project directory (default: current working directory)")
|
|
2828
|
+
}
|
|
2829
|
+
},
|
|
2830
|
+
async ({ pathOrName, projectDir }) => {
|
|
2831
|
+
try {
|
|
2832
|
+
const { messages } = withCapture(
|
|
2833
|
+
() => removeContextRepo(pathOrName, projectDir)
|
|
2834
|
+
);
|
|
2835
|
+
return textResult(formatMessages(messages) || `Removed "${pathOrName}" from context.`);
|
|
2836
|
+
} catch (err) {
|
|
2837
|
+
return errorResult("Error removing context repo", err);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
);
|
|
2841
|
+
server.registerTool(
|
|
2842
|
+
"planmode_context_list",
|
|
2843
|
+
{
|
|
2844
|
+
description: "List all document directories in the project context with file counts, sizes, and type breakdowns. Use this to see what reference documents are available for the project.",
|
|
2845
|
+
inputSchema: {
|
|
2846
|
+
detailed: z.boolean().optional().describe("Include full file listings for each repo (default: false, shows only summaries)"),
|
|
2847
|
+
projectDir: z.string().optional().describe("Project directory (default: current working directory)")
|
|
2848
|
+
}
|
|
2849
|
+
},
|
|
2850
|
+
async ({ detailed, projectDir }) => {
|
|
2851
|
+
try {
|
|
2852
|
+
const summary = getContextSummary(projectDir);
|
|
2853
|
+
if (summary.totalRepos === 0) {
|
|
2854
|
+
return textResult("No context repos configured. Use planmode_context_add to add a document directory.");
|
|
2855
|
+
}
|
|
2856
|
+
const lines = [
|
|
2857
|
+
`**${summary.totalRepos} context repo(s)** \u2014 ${summary.totalFiles} file(s), ${formatSize(summary.totalSize)}`,
|
|
2858
|
+
""
|
|
2859
|
+
];
|
|
2860
|
+
for (const repo of summary.repos) {
|
|
2861
|
+
lines.push(`### ${repo.name}`);
|
|
2862
|
+
lines.push(`- **Path:** ${repo.path}`);
|
|
2863
|
+
lines.push(`- **Files:** ${repo.fileCount} (${formatSize(repo.totalSize)})`);
|
|
2864
|
+
if (repo.typeBreakdown.length > 0) {
|
|
2865
|
+
lines.push(`- **Types:** ${repo.typeBreakdown.join(", ")}`);
|
|
2866
|
+
}
|
|
2867
|
+
lines.push(`- **Indexed:** ${repo.indexedAt}`);
|
|
2868
|
+
if (detailed) {
|
|
2869
|
+
const index = (await Promise.resolve().then(() => (init_context(), context_exports))).readContextIndex(projectDir);
|
|
2870
|
+
const repoIndex = index.repos.find(
|
|
2871
|
+
(r) => r.repo.path === repo.path || r.repo.name === repo.name
|
|
2872
|
+
);
|
|
2873
|
+
if (repoIndex && repoIndex.files.length > 0) {
|
|
2874
|
+
lines.push("", "**Files:**");
|
|
2875
|
+
for (const file of repoIndex.files) {
|
|
2876
|
+
lines.push(`- \`${file.path}\` (${file.extension}, ${formatSize(file.size)})`);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
lines.push("");
|
|
2881
|
+
}
|
|
2882
|
+
return textResult(lines.join("\n"));
|
|
2883
|
+
} catch (err) {
|
|
2884
|
+
return errorResult("Error listing context", err);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
);
|
|
2888
|
+
server.registerTool(
|
|
2889
|
+
"planmode_context_reindex",
|
|
2890
|
+
{
|
|
2891
|
+
description: "Re-scan files in one or all context directories to update the file index",
|
|
2892
|
+
inputSchema: {
|
|
2893
|
+
pathOrName: z.string().optional().describe("Path or name of a specific repo to reindex (omit to reindex all)"),
|
|
2894
|
+
projectDir: z.string().optional().describe("Project directory (default: current working directory)")
|
|
2895
|
+
}
|
|
2896
|
+
},
|
|
2897
|
+
async ({ pathOrName, projectDir }) => {
|
|
2898
|
+
try {
|
|
2899
|
+
const { messages } = withCapture(
|
|
2900
|
+
() => reindexContext(pathOrName, projectDir)
|
|
2901
|
+
);
|
|
2902
|
+
return textResult(formatMessages(messages) || "Reindex complete.");
|
|
2903
|
+
} catch (err) {
|
|
2904
|
+
return errorResult("Error reindexing context", err);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
);
|
|
2908
|
+
function getTypeBreakdownText(repoIndex) {
|
|
2909
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2910
|
+
for (const file of repoIndex.files) {
|
|
2911
|
+
counts.set(file.extension, (counts.get(file.extension) ?? 0) + 1);
|
|
2912
|
+
}
|
|
2913
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]).map(([ext, count]) => `${ext}: ${count}`).join(", ");
|
|
2914
|
+
}
|
|
2539
2915
|
server.registerResource(
|
|
2540
2916
|
"installed-packages",
|
|
2541
2917
|
new ResourceTemplate("planmode://packages/{name}", {
|
|
@@ -2568,10 +2944,10 @@ server.registerResource(
|
|
|
2568
2944
|
}]
|
|
2569
2945
|
};
|
|
2570
2946
|
}
|
|
2571
|
-
const fullPath =
|
|
2947
|
+
const fullPath = path13.join(process.cwd(), entry.installed_to);
|
|
2572
2948
|
let content;
|
|
2573
2949
|
try {
|
|
2574
|
-
content =
|
|
2950
|
+
content = fs13.readFileSync(fullPath, "utf-8");
|
|
2575
2951
|
} catch {
|
|
2576
2952
|
content = `File not found at ${entry.installed_to}. The package may need to be reinstalled.`;
|
|
2577
2953
|
}
|