planmode 0.2.2 → 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 +2080 -717
- package/dist/mcp.js +798 -262
- package/package.json +2 -1
- package/src/commands/context.ts +111 -0
- package/src/commands/doctor.ts +46 -14
- package/src/commands/init.ts +95 -47
- package/src/commands/install.ts +17 -2
- package/src/commands/interactive.ts +556 -0
- package/src/commands/login.ts +50 -23
- package/src/commands/publish.ts +15 -3
- package/src/commands/record.ts +32 -8
- package/src/commands/run.ts +6 -15
- package/src/commands/search.ts +89 -18
- package/src/commands/snapshot.ts +33 -9
- package/src/commands/test.ts +43 -13
- package/src/commands/update.ts +57 -15
- package/src/index.ts +11 -2
- package/src/lib/context.ts +265 -0
- package/src/lib/installer.ts +57 -29
- package/src/lib/prompts.ts +159 -0
- package/src/lib/publisher.ts +176 -144
- 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) {
|
|
@@ -660,6 +912,118 @@ function trackDownload(packageName) {
|
|
|
660
912
|
});
|
|
661
913
|
}
|
|
662
914
|
|
|
915
|
+
// src/lib/prompts.ts
|
|
916
|
+
import * as p from "@clack/prompts";
|
|
917
|
+
function isInteractive() {
|
|
918
|
+
return Boolean(process.stdin.isTTY) && !process.env.CI;
|
|
919
|
+
}
|
|
920
|
+
function handleCancel(value) {
|
|
921
|
+
if (p.isCancel(value)) {
|
|
922
|
+
p.cancel("Cancelled.");
|
|
923
|
+
process.exit(0);
|
|
924
|
+
}
|
|
925
|
+
return value;
|
|
926
|
+
}
|
|
927
|
+
async function promptForVariable(name, def) {
|
|
928
|
+
switch (def.type) {
|
|
929
|
+
case "enum": {
|
|
930
|
+
const value = await p.select({
|
|
931
|
+
message: def.description || name,
|
|
932
|
+
options: (def.options ?? []).map((opt) => ({
|
|
933
|
+
value: opt,
|
|
934
|
+
label: opt
|
|
935
|
+
})),
|
|
936
|
+
initialValue: def.default !== void 0 ? String(def.default) : void 0
|
|
937
|
+
});
|
|
938
|
+
return handleCancel(value);
|
|
939
|
+
}
|
|
940
|
+
case "boolean": {
|
|
941
|
+
const value = await p.confirm({
|
|
942
|
+
message: def.description || name,
|
|
943
|
+
initialValue: def.default !== void 0 ? Boolean(def.default) : false
|
|
944
|
+
});
|
|
945
|
+
return handleCancel(value);
|
|
946
|
+
}
|
|
947
|
+
case "number": {
|
|
948
|
+
const value = await p.text({
|
|
949
|
+
message: def.description || name,
|
|
950
|
+
placeholder: def.default !== void 0 ? String(def.default) : void 0,
|
|
951
|
+
defaultValue: def.default !== void 0 ? String(def.default) : void 0,
|
|
952
|
+
validate(input) {
|
|
953
|
+
if (isNaN(Number(input))) return "Must be a number";
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
return Number(handleCancel(value));
|
|
957
|
+
}
|
|
958
|
+
case "string":
|
|
959
|
+
default: {
|
|
960
|
+
const value = await p.text({
|
|
961
|
+
message: def.description || name,
|
|
962
|
+
placeholder: def.default !== void 0 ? String(def.default) : void 0,
|
|
963
|
+
defaultValue: def.default !== void 0 ? String(def.default) : void 0,
|
|
964
|
+
validate(input) {
|
|
965
|
+
if (def.required && !input) return `${name} is required`;
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
return handleCancel(value);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async function promptForVariables(variableDefs, provided, noInput = false) {
|
|
973
|
+
const values = {};
|
|
974
|
+
for (const [name, def] of Object.entries(variableDefs)) {
|
|
975
|
+
if (def.type === "resolved") continue;
|
|
976
|
+
if (provided[name] !== void 0) {
|
|
977
|
+
values[name] = coerceValue2(provided[name], def);
|
|
978
|
+
} else if (def.default !== void 0) {
|
|
979
|
+
if (isInteractive() && !noInput) {
|
|
980
|
+
values[name] = await promptForVariable(name, def);
|
|
981
|
+
} else {
|
|
982
|
+
values[name] = def.default;
|
|
983
|
+
}
|
|
984
|
+
} else if (def.required) {
|
|
985
|
+
if (isInteractive() && !noInput) {
|
|
986
|
+
values[name] = await promptForVariable(name, def);
|
|
987
|
+
} else {
|
|
988
|
+
throw new Error(`Missing required variable: ${name} -- ${def.description}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return values;
|
|
993
|
+
}
|
|
994
|
+
function coerceValue2(raw, def) {
|
|
995
|
+
switch (def.type) {
|
|
996
|
+
case "number":
|
|
997
|
+
return Number(raw);
|
|
998
|
+
case "boolean":
|
|
999
|
+
return raw === "true" || raw === "1" || raw === "yes";
|
|
1000
|
+
case "enum":
|
|
1001
|
+
if (def.options && !def.options.includes(raw)) {
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
`Invalid value "${raw}" for enum variable. Options: ${def.options.join(", ")}`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
return raw;
|
|
1007
|
+
default:
|
|
1008
|
+
return raw;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async function withSpinner(message, fn, successMessage) {
|
|
1012
|
+
if (!isInteractive()) {
|
|
1013
|
+
return fn();
|
|
1014
|
+
}
|
|
1015
|
+
const s = p.spinner();
|
|
1016
|
+
s.start(message);
|
|
1017
|
+
try {
|
|
1018
|
+
const result = await fn();
|
|
1019
|
+
s.stop(successMessage ?? message);
|
|
1020
|
+
return result;
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
s.stop(`Failed: ${message}`);
|
|
1023
|
+
throw err;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
663
1027
|
// src/lib/installer.ts
|
|
664
1028
|
function getInstallDir(type) {
|
|
665
1029
|
switch (type) {
|
|
@@ -679,38 +1043,60 @@ function contentHash(content) {
|
|
|
679
1043
|
}
|
|
680
1044
|
async function installPackage(packageName, options = {}) {
|
|
681
1045
|
const projectDir = options.projectDir ?? process.cwd();
|
|
1046
|
+
const interactive = options.interactive ?? (isInteractive() && !options.noInput);
|
|
682
1047
|
const locked = getLockedVersion(packageName, projectDir);
|
|
683
1048
|
if (locked && !options.version) {
|
|
684
1049
|
logger.dim(`${packageName}@${locked.version} already installed`);
|
|
685
1050
|
return;
|
|
686
1051
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
)
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1052
|
+
const resolveAndFetch = async () => {
|
|
1053
|
+
const { version: version2, metadata: metadata2 } = await resolveVersion(packageName, options.version);
|
|
1054
|
+
const versionMeta2 = await fetchVersionMetadata(packageName, version2);
|
|
1055
|
+
return { version: version2, metadata: metadata2, versionMeta: versionMeta2 };
|
|
1056
|
+
};
|
|
1057
|
+
const { version, metadata, versionMeta } = interactive ? await withSpinner(
|
|
1058
|
+
`Resolving ${packageName}...`,
|
|
1059
|
+
resolveAndFetch,
|
|
1060
|
+
`Resolved ${packageName}`
|
|
1061
|
+
) : await (async () => {
|
|
1062
|
+
logger.info(`Resolving ${packageName}...`);
|
|
1063
|
+
return resolveAndFetch();
|
|
1064
|
+
})();
|
|
1065
|
+
const fetchContent = async () => {
|
|
1066
|
+
const basePath = versionMeta.source.path ? `${versionMeta.source.path}/` : "";
|
|
1067
|
+
const manifestRaw = await fetchFileAtTag(
|
|
703
1068
|
versionMeta.source.repository,
|
|
704
1069
|
versionMeta.source.tag,
|
|
705
|
-
`${basePath}
|
|
1070
|
+
`${basePath}planmode.yaml`
|
|
706
1071
|
);
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1072
|
+
const manifest2 = parseManifest(manifestRaw);
|
|
1073
|
+
let content2;
|
|
1074
|
+
if (manifest2.content) {
|
|
1075
|
+
content2 = manifest2.content;
|
|
1076
|
+
} else if (manifest2.content_file) {
|
|
1077
|
+
content2 = await fetchFileAtTag(
|
|
1078
|
+
versionMeta.source.repository,
|
|
1079
|
+
versionMeta.source.tag,
|
|
1080
|
+
`${basePath}${manifest2.content_file}`
|
|
1081
|
+
);
|
|
1082
|
+
} else {
|
|
1083
|
+
throw new Error("Package has no content or content_file");
|
|
1084
|
+
}
|
|
1085
|
+
return { manifest: manifest2, content: content2 };
|
|
1086
|
+
};
|
|
1087
|
+
const { manifest, content: rawContent } = interactive ? await withSpinner(
|
|
1088
|
+
`Fetching ${packageName}@${version}...`,
|
|
1089
|
+
fetchContent,
|
|
1090
|
+
`Fetched ${packageName}@${version}`
|
|
1091
|
+
) : await (async () => {
|
|
1092
|
+
logger.info(`Fetching ${packageName}@${version}...`);
|
|
1093
|
+
return fetchContent();
|
|
1094
|
+
})();
|
|
1095
|
+
let content = rawContent;
|
|
710
1096
|
if (manifest.variables && Object.keys(manifest.variables).length > 0) {
|
|
711
1097
|
const provided = options.variables ?? {};
|
|
712
|
-
if (
|
|
713
|
-
const values =
|
|
1098
|
+
if (interactive) {
|
|
1099
|
+
const values = await promptForVariables(manifest.variables, provided, false);
|
|
714
1100
|
content = renderTemplate(content, values);
|
|
715
1101
|
} else {
|
|
716
1102
|
const values = collectVariableValues(manifest.variables, provided);
|
|
@@ -772,7 +1158,8 @@ async function installPackage(packageName, options = {}) {
|
|
|
772
1158
|
await installPackage(name, {
|
|
773
1159
|
version: range === "*" ? void 0 : range,
|
|
774
1160
|
projectDir,
|
|
775
|
-
noInput: options.noInput
|
|
1161
|
+
noInput: options.noInput,
|
|
1162
|
+
interactive: options.interactive
|
|
776
1163
|
});
|
|
777
1164
|
}
|
|
778
1165
|
}
|
|
@@ -917,141 +1304,154 @@ function createPackage(options) {
|
|
|
917
1304
|
}
|
|
918
1305
|
|
|
919
1306
|
// src/lib/publisher.ts
|
|
1307
|
+
init_logger();
|
|
920
1308
|
async function publishPackage(options = {}) {
|
|
921
1309
|
const cwd = options.projectDir ?? process.cwd();
|
|
1310
|
+
const interactive = options.interactive ?? false;
|
|
922
1311
|
const token = options.token ?? getGitHubToken();
|
|
923
1312
|
if (!token) {
|
|
924
1313
|
throw new Error("Not authenticated. Run `planmode login` first.");
|
|
925
1314
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1315
|
+
const doValidate = async () => {
|
|
1316
|
+
const manifest2 = readManifest(cwd);
|
|
1317
|
+
const errors = validateManifest(manifest2, true);
|
|
1318
|
+
if (errors.length > 0) {
|
|
1319
|
+
throw new Error(`Invalid manifest:
|
|
931
1320
|
${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
932
|
-
|
|
1321
|
+
}
|
|
1322
|
+
return manifest2;
|
|
1323
|
+
};
|
|
1324
|
+
const manifest = interactive ? await withSpinner("Validating manifest...", doValidate, "Manifest valid") : await (async () => {
|
|
1325
|
+
logger.info("Reading planmode.yaml...");
|
|
1326
|
+
return doValidate();
|
|
1327
|
+
})();
|
|
933
1328
|
const remoteUrl = await getRemoteUrl(cwd);
|
|
934
1329
|
if (!remoteUrl) {
|
|
935
1330
|
throw new Error("No git remote found. Push your code to GitHub first.");
|
|
936
1331
|
}
|
|
937
1332
|
const sha = await getHeadSha(cwd);
|
|
938
1333
|
const tag = `v${manifest.version}`;
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1334
|
+
const doTag = async () => {
|
|
1335
|
+
try {
|
|
1336
|
+
await createTag(cwd, tag);
|
|
1337
|
+
} catch {
|
|
1338
|
+
}
|
|
1339
|
+
try {
|
|
1340
|
+
await pushTag(cwd, tag);
|
|
1341
|
+
} catch {
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
if (interactive) {
|
|
1345
|
+
await withSpinner(`Creating tag ${tag}...`, doTag, `Tag ${tag} ready`);
|
|
1346
|
+
} else {
|
|
1347
|
+
logger.info(`Creating tag ${tag}...`);
|
|
1348
|
+
await doTag();
|
|
947
1349
|
logger.success(`Pushed tag ${tag}`);
|
|
948
|
-
} catch {
|
|
949
|
-
logger.dim(`Tag ${tag} already pushed`);
|
|
950
1350
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
repository: repoPath,
|
|
975
|
-
category: manifest.category ?? "other",
|
|
976
|
-
tags: manifest.tags ?? [],
|
|
977
|
-
type: manifest.type,
|
|
978
|
-
models: manifest.models ?? [],
|
|
979
|
-
latest_version: manifest.version,
|
|
980
|
-
versions: [manifest.version],
|
|
981
|
-
downloads: 0,
|
|
982
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
983
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
984
|
-
dependencies: manifest.dependencies,
|
|
985
|
-
variables: manifest.variables
|
|
986
|
-
},
|
|
987
|
-
null,
|
|
988
|
-
2
|
|
989
|
-
);
|
|
990
|
-
const versionContent = JSON.stringify(
|
|
991
|
-
{
|
|
992
|
-
version: manifest.version,
|
|
993
|
-
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
994
|
-
source: {
|
|
1351
|
+
const doSubmit = async () => {
|
|
1352
|
+
const headers = {
|
|
1353
|
+
Authorization: `Bearer ${token}`,
|
|
1354
|
+
Accept: "application/vnd.github.v3+json",
|
|
1355
|
+
"User-Agent": "planmode-cli",
|
|
1356
|
+
"Content-Type": "application/json"
|
|
1357
|
+
};
|
|
1358
|
+
await fetch("https://api.github.com/repos/kaihannonen/planmode.org/forks", {
|
|
1359
|
+
method: "POST",
|
|
1360
|
+
headers
|
|
1361
|
+
});
|
|
1362
|
+
const userRes = await fetch("https://api.github.com/user", { headers });
|
|
1363
|
+
if (!userRes.ok) {
|
|
1364
|
+
throw new Error("Failed to authenticate with GitHub. Check your token.");
|
|
1365
|
+
}
|
|
1366
|
+
const user = await userRes.json();
|
|
1367
|
+
const repoPath = remoteUrl.replace(/^https?:\/\//, "").replace(/\.git$/, "");
|
|
1368
|
+
const metadataContent = JSON.stringify(
|
|
1369
|
+
{
|
|
1370
|
+
name: manifest.name,
|
|
1371
|
+
description: manifest.description,
|
|
1372
|
+
author: manifest.author,
|
|
1373
|
+
license: manifest.license,
|
|
995
1374
|
repository: repoPath,
|
|
996
|
-
|
|
997
|
-
|
|
1375
|
+
category: manifest.category ?? "other",
|
|
1376
|
+
tags: manifest.tags ?? [],
|
|
1377
|
+
type: manifest.type,
|
|
1378
|
+
models: manifest.models ?? [],
|
|
1379
|
+
latest_version: manifest.version,
|
|
1380
|
+
versions: [manifest.version],
|
|
1381
|
+
downloads: 0,
|
|
1382
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1383
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1384
|
+
dependencies: manifest.dependencies,
|
|
1385
|
+
variables: manifest.variables
|
|
998
1386
|
},
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1387
|
+
null,
|
|
1388
|
+
2
|
|
1389
|
+
);
|
|
1390
|
+
const versionContent = JSON.stringify(
|
|
1391
|
+
{
|
|
1392
|
+
version: manifest.version,
|
|
1393
|
+
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1394
|
+
source: {
|
|
1395
|
+
repository: repoPath,
|
|
1396
|
+
tag,
|
|
1397
|
+
sha
|
|
1398
|
+
},
|
|
1399
|
+
files: ["planmode.yaml", manifest.content_file ?? "inline"],
|
|
1400
|
+
content_hash: `sha256:${sha.slice(0, 16)}`
|
|
1401
|
+
},
|
|
1402
|
+
null,
|
|
1403
|
+
2
|
|
1404
|
+
);
|
|
1405
|
+
const branchName = `add-${manifest.name}-${manifest.version}`;
|
|
1406
|
+
const refRes = await fetch(
|
|
1407
|
+
`https://api.github.com/repos/${user.login}/planmode.org/git/ref/heads/main`,
|
|
1408
|
+
{ headers }
|
|
1409
|
+
);
|
|
1410
|
+
if (!refRes.ok) {
|
|
1411
|
+
throw new Error("Failed to access registry fork. Make sure the fork exists.");
|
|
1412
|
+
}
|
|
1413
|
+
const refData = await refRes.json();
|
|
1414
|
+
const baseSha = refData.object.sha;
|
|
1415
|
+
await fetch(`https://api.github.com/repos/${user.login}/planmode.org/git/refs`, {
|
|
1416
|
+
method: "POST",
|
|
1027
1417
|
headers,
|
|
1028
1418
|
body: JSON.stringify({
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
branch: branchName
|
|
1419
|
+
ref: `refs/heads/${branchName}`,
|
|
1420
|
+
sha: baseSha
|
|
1032
1421
|
})
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1422
|
+
});
|
|
1423
|
+
await fetch(
|
|
1424
|
+
`https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/metadata.json`,
|
|
1425
|
+
{
|
|
1426
|
+
method: "PUT",
|
|
1427
|
+
headers,
|
|
1428
|
+
body: JSON.stringify({
|
|
1429
|
+
message: `Add ${manifest.name}@${manifest.version}`,
|
|
1430
|
+
content: Buffer.from(metadataContent).toString("base64"),
|
|
1431
|
+
branch: branchName
|
|
1432
|
+
})
|
|
1433
|
+
}
|
|
1434
|
+
);
|
|
1435
|
+
await fetch(
|
|
1436
|
+
`https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/versions/${manifest.version}.json`,
|
|
1437
|
+
{
|
|
1438
|
+
method: "PUT",
|
|
1439
|
+
headers,
|
|
1440
|
+
body: JSON.stringify({
|
|
1441
|
+
message: `Add ${manifest.name}@${manifest.version} version metadata`,
|
|
1442
|
+
content: Buffer.from(versionContent).toString("base64"),
|
|
1443
|
+
branch: branchName
|
|
1444
|
+
})
|
|
1445
|
+
}
|
|
1446
|
+
);
|
|
1447
|
+
const prRes = await fetch("https://api.github.com/repos/kaihannonen/planmode.org/pulls", {
|
|
1448
|
+
method: "POST",
|
|
1039
1449
|
headers,
|
|
1040
1450
|
body: JSON.stringify({
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1046
|
-
);
|
|
1047
|
-
const prRes = await fetch("https://api.github.com/repos/kaihannonen/planmode.org/pulls", {
|
|
1048
|
-
method: "POST",
|
|
1049
|
-
headers,
|
|
1050
|
-
body: JSON.stringify({
|
|
1051
|
-
title: `Add ${manifest.name}@${manifest.version}`,
|
|
1052
|
-
head: `${user.login}:${branchName}`,
|
|
1053
|
-
base: "main",
|
|
1054
|
-
body: `## New package: ${manifest.name}
|
|
1451
|
+
title: `Add ${manifest.name}@${manifest.version}`,
|
|
1452
|
+
head: `${user.login}:${branchName}`,
|
|
1453
|
+
base: "main",
|
|
1454
|
+
body: `## New package: ${manifest.name}
|
|
1055
1455
|
|
|
1056
1456
|
- **Type:** ${manifest.type}
|
|
1057
1457
|
- **Version:** ${manifest.version}
|
|
@@ -1059,17 +1459,30 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
|
1059
1459
|
- **Author:** ${manifest.author}
|
|
1060
1460
|
|
|
1061
1461
|
Submitted via \`planmode publish\`.`
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1462
|
+
})
|
|
1463
|
+
});
|
|
1464
|
+
if (!prRes.ok) {
|
|
1465
|
+
const err = await prRes.text();
|
|
1466
|
+
throw new Error(`Failed to create PR: ${err}`);
|
|
1467
|
+
}
|
|
1468
|
+
const pr = await prRes.json();
|
|
1469
|
+
return pr.html_url;
|
|
1470
|
+
};
|
|
1471
|
+
let prUrl;
|
|
1472
|
+
if (interactive) {
|
|
1473
|
+
prUrl = await withSpinner(
|
|
1474
|
+
"Submitting to registry...",
|
|
1475
|
+
doSubmit,
|
|
1476
|
+
"Submitted to registry"
|
|
1477
|
+
);
|
|
1478
|
+
} else {
|
|
1479
|
+
logger.info("Submitting to registry...");
|
|
1480
|
+
prUrl = await doSubmit();
|
|
1481
|
+
logger.success(`Published ${manifest.name}@${manifest.version}`);
|
|
1482
|
+
logger.info(`PR: ${prUrl}`);
|
|
1067
1483
|
}
|
|
1068
|
-
const pr = await prRes.json();
|
|
1069
|
-
logger.success(`Published ${manifest.name}@${manifest.version}`);
|
|
1070
|
-
logger.info(`PR: ${pr.html_url}`);
|
|
1071
1484
|
return {
|
|
1072
|
-
prUrl
|
|
1485
|
+
prUrl,
|
|
1073
1486
|
packageName: manifest.name,
|
|
1074
1487
|
version: manifest.version
|
|
1075
1488
|
};
|
|
@@ -1692,6 +2105,7 @@ function detectCategory(data) {
|
|
|
1692
2105
|
}
|
|
1693
2106
|
|
|
1694
2107
|
// src/mcp.ts
|
|
2108
|
+
init_context();
|
|
1695
2109
|
function withCapture(fn) {
|
|
1696
2110
|
logger.capture();
|
|
1697
2111
|
try {
|
|
@@ -1714,9 +2128,9 @@ async function withCaptureAsync(fn) {
|
|
|
1714
2128
|
throw Object.assign(err, { capturedMessages: messages });
|
|
1715
2129
|
}
|
|
1716
2130
|
}
|
|
1717
|
-
function textResult(
|
|
2131
|
+
function textResult(text2, isError = false) {
|
|
1718
2132
|
return {
|
|
1719
|
-
content: [{ type: "text", text }],
|
|
2133
|
+
content: [{ type: "text", text: text2 }],
|
|
1720
2134
|
isError
|
|
1721
2135
|
};
|
|
1722
2136
|
}
|
|
@@ -1969,14 +2383,14 @@ server.registerTool(
|
|
|
1969
2383
|
const entry = lockfile.packages[packageName];
|
|
1970
2384
|
if (!entry) {
|
|
1971
2385
|
const candidates = [
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2386
|
+
path13.join(dir, "plans", `${packageName}.md`),
|
|
2387
|
+
path13.join(dir, ".claude", "rules", `${packageName}.md`),
|
|
2388
|
+
path13.join(dir, "prompts", `${packageName}.md`)
|
|
1975
2389
|
];
|
|
1976
2390
|
for (const candidate of candidates) {
|
|
1977
|
-
if (
|
|
1978
|
-
const content2 =
|
|
1979
|
-
const relativePath =
|
|
2391
|
+
if (fs13.existsSync(candidate)) {
|
|
2392
|
+
const content2 = fs13.readFileSync(candidate, "utf-8");
|
|
2393
|
+
const relativePath = path13.relative(dir, candidate);
|
|
1980
2394
|
return textResult(`# ${packageName}
|
|
1981
2395
|
**Location:** ${relativePath}
|
|
1982
2396
|
|
|
@@ -1990,14 +2404,14 @@ ${content2}`);
|
|
|
1990
2404
|
true
|
|
1991
2405
|
);
|
|
1992
2406
|
}
|
|
1993
|
-
const fullPath =
|
|
1994
|
-
if (!
|
|
2407
|
+
const fullPath = path13.join(dir, entry.installed_to);
|
|
2408
|
+
if (!fs13.existsSync(fullPath)) {
|
|
1995
2409
|
return textResult(
|
|
1996
2410
|
`Package '${packageName}' is in the lockfile but the file is missing at ${entry.installed_to}. Try reinstalling with planmode_install.`,
|
|
1997
2411
|
true
|
|
1998
2412
|
);
|
|
1999
2413
|
}
|
|
2000
|
-
const content =
|
|
2414
|
+
const content = fs13.readFileSync(fullPath, "utf-8");
|
|
2001
2415
|
return textResult(
|
|
2002
2416
|
`# ${packageName} (${entry.type} v${entry.version})
|
|
2003
2417
|
**Location:** ${entry.installed_to}
|
|
@@ -2184,17 +2598,17 @@ server.registerTool(
|
|
|
2184
2598
|
async ({ prompt: promptName, variables, projectDir }) => {
|
|
2185
2599
|
try {
|
|
2186
2600
|
const dir = projectDir ?? process.cwd();
|
|
2187
|
-
const localPath =
|
|
2188
|
-
const localManifestPath =
|
|
2601
|
+
const localPath = path13.join(dir, "prompts", `${promptName}.md`);
|
|
2602
|
+
const localManifestPath = path13.join(dir, "prompts", promptName, "planmode.yaml");
|
|
2189
2603
|
let content;
|
|
2190
2604
|
let manifest;
|
|
2191
|
-
if (
|
|
2192
|
-
const raw =
|
|
2605
|
+
if (fs13.existsSync(localManifestPath)) {
|
|
2606
|
+
const raw = fs13.readFileSync(localManifestPath, "utf-8");
|
|
2193
2607
|
manifest = parseManifest(raw);
|
|
2194
|
-
const promptDir =
|
|
2608
|
+
const promptDir = path13.join(dir, "prompts", promptName);
|
|
2195
2609
|
content = readPackageContent(promptDir, manifest);
|
|
2196
|
-
} else if (
|
|
2197
|
-
content =
|
|
2610
|
+
} else if (fs13.existsSync(localPath)) {
|
|
2611
|
+
content = fs13.readFileSync(localPath, "utf-8");
|
|
2198
2612
|
} else {
|
|
2199
2613
|
return textResult(
|
|
2200
2614
|
`Prompt '${promptName}' not found locally. Install it first using the planmode_install tool.`,
|
|
@@ -2376,6 +2790,128 @@ server.registerTool(
|
|
|
2376
2790
|
}
|
|
2377
2791
|
}
|
|
2378
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
|
+
}
|
|
2379
2915
|
server.registerResource(
|
|
2380
2916
|
"installed-packages",
|
|
2381
2917
|
new ResourceTemplate("planmode://packages/{name}", {
|
|
@@ -2408,10 +2944,10 @@ server.registerResource(
|
|
|
2408
2944
|
}]
|
|
2409
2945
|
};
|
|
2410
2946
|
}
|
|
2411
|
-
const fullPath =
|
|
2947
|
+
const fullPath = path13.join(process.cwd(), entry.installed_to);
|
|
2412
2948
|
let content;
|
|
2413
2949
|
try {
|
|
2414
|
-
content =
|
|
2950
|
+
content = fs13.readFileSync(fullPath, "utf-8");
|
|
2415
2951
|
} catch {
|
|
2416
2952
|
content = `File not found at ${entry.installed_to}. The package may need to be reinstalled.`;
|
|
2417
2953
|
}
|