@codebyplan/cli 2.0.1 → 2.1.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/README.md +11 -0
- package/dist/cli.js +400 -192
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,6 +71,17 @@ claude mcp add codebyplan -e CODEBYPLAN_API_KEY=your_key_here -- npx -y @codebyp
|
|
|
71
71
|
|------|-------------|
|
|
72
72
|
| `health_check` | Check server version, API connectivity, and latency |
|
|
73
73
|
|
|
74
|
+
### Settings File Model
|
|
75
|
+
|
|
76
|
+
The `sync_claude_files` tool writes a `settings.json` file to the project's `.claude/` directory. This file contains shared settings (hooks, statusLine, attribution, permissions.deny/ask/additionalDirectories) merged from global and repo-specific scopes in the database.
|
|
77
|
+
|
|
78
|
+
A separate `settings.local.json` file (not managed by sync) holds local-only configuration: `permissions.allow` and MCP server definitions. Claude Code merges both files at runtime, with `settings.local.json` taking precedence.
|
|
79
|
+
|
|
80
|
+
| File | Managed By | Contains |
|
|
81
|
+
|------|------------|----------|
|
|
82
|
+
| `settings.json` | `sync_claude_files` (synced from DB) | Shared settings: hooks, statusLine, attribution, permissions.deny/ask/additionalDirectories |
|
|
83
|
+
| `settings.local.json` | User (local-only) | Machine-specific: permissions.allow, MCP server config |
|
|
84
|
+
|
|
74
85
|
## Environment Variables
|
|
75
86
|
|
|
76
87
|
| Variable | Required | Description |
|
package/dist/cli.js
CHANGED
|
@@ -37,7 +37,7 @@ var VERSION, PACKAGE_NAME;
|
|
|
37
37
|
var init_version = __esm({
|
|
38
38
|
"src/lib/version.ts"() {
|
|
39
39
|
"use strict";
|
|
40
|
-
VERSION = "2.0
|
|
40
|
+
VERSION = "2.1.0";
|
|
41
41
|
PACKAGE_NAME = "@codebyplan/cli";
|
|
42
42
|
}
|
|
43
43
|
});
|
|
@@ -49,7 +49,52 @@ __export(setup_exports, {
|
|
|
49
49
|
});
|
|
50
50
|
import { createInterface } from "node:readline/promises";
|
|
51
51
|
import { stdin, stdout } from "node:process";
|
|
52
|
-
import {
|
|
52
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
53
|
+
import { homedir } from "node:os";
|
|
54
|
+
import { join } from "node:path";
|
|
55
|
+
function getConfigPath(scope) {
|
|
56
|
+
return scope === "user" ? join(homedir(), ".claude.json") : join(process.cwd(), ".mcp.json");
|
|
57
|
+
}
|
|
58
|
+
async function readConfig(path) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await readFile(path, "utf-8");
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
return {};
|
|
66
|
+
} catch {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function buildMcpEntry(apiKey) {
|
|
71
|
+
return {
|
|
72
|
+
command: "npx",
|
|
73
|
+
args: ["-y", PACKAGE_NAME],
|
|
74
|
+
env: { CODEBYPLAN_API_KEY: apiKey }
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function writeMcpConfig(scope, apiKey) {
|
|
78
|
+
const configPath = getConfigPath(scope);
|
|
79
|
+
const config2 = await readConfig(configPath);
|
|
80
|
+
if (typeof config2.mcpServers !== "object" || config2.mcpServers === null || Array.isArray(config2.mcpServers)) {
|
|
81
|
+
config2.mcpServers = {};
|
|
82
|
+
}
|
|
83
|
+
config2.mcpServers.codebyplan = buildMcpEntry(apiKey);
|
|
84
|
+
await writeFile(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
85
|
+
return configPath;
|
|
86
|
+
}
|
|
87
|
+
async function verifyMcpConfig(scope, apiKey) {
|
|
88
|
+
try {
|
|
89
|
+
const config2 = await readConfig(getConfigPath(scope));
|
|
90
|
+
const servers = config2.mcpServers;
|
|
91
|
+
if (!servers) return false;
|
|
92
|
+
const entry = servers.codebyplan;
|
|
93
|
+
return entry?.env?.CODEBYPLAN_API_KEY === apiKey;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
53
98
|
async function runSetup() {
|
|
54
99
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
55
100
|
console.log("\n CodeByPlan MCP Server Setup\n");
|
|
@@ -76,53 +121,38 @@ async function runSetup() {
|
|
|
76
121
|
console.log(` Warning: API returned status ${res.status}, but continuing.
|
|
77
122
|
`);
|
|
78
123
|
} else {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`CODEBYPLAN_API_KEY=${apiKey}`,
|
|
102
|
-
"--",
|
|
103
|
-
"npx",
|
|
104
|
-
"-y",
|
|
105
|
-
PACKAGE_NAME
|
|
106
|
-
],
|
|
107
|
-
(err, _stdout, stderr) => {
|
|
108
|
-
if (err) {
|
|
109
|
-
console.log(` Could not run 'claude mcp add' automatically.`);
|
|
110
|
-
console.log(` Error: ${stderr || err.message}`);
|
|
111
|
-
console.log(`
|
|
112
|
-
Run it manually:
|
|
113
|
-
${addCmd}
|
|
124
|
+
try {
|
|
125
|
+
const body = await res.json();
|
|
126
|
+
if (Array.isArray(body.data) && body.data.length === 0) {
|
|
127
|
+
console.log(" API key is valid but no repositories found.");
|
|
128
|
+
console.log(" Create one at https://codebyplan.com after setup.\n");
|
|
129
|
+
} else {
|
|
130
|
+
console.log(" API key is valid!\n");
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
console.log(" API key is valid!\n");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
console.log(" Where should the MCP server be configured?\n");
|
|
137
|
+
console.log(" 1. Global \u2014 available in all projects (~/.claude.json)");
|
|
138
|
+
console.log(" 2. Project \u2014 only this project (.mcp.json)\n");
|
|
139
|
+
const scopeInput = (await rl.question(" Select (1/2, default: 1): ")).trim();
|
|
140
|
+
const scope = scopeInput === "2" ? "project" : "user";
|
|
141
|
+
console.log("\n Configuring MCP server...");
|
|
142
|
+
const configPath = await writeMcpConfig(scope, apiKey);
|
|
143
|
+
const verified = await verifyMcpConfig(scope, apiKey);
|
|
144
|
+
if (verified) {
|
|
145
|
+
console.log(` Done! Config written to ${configPath}
|
|
114
146
|
`);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
resolve2();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
});
|
|
147
|
+
if (scope === "project") {
|
|
148
|
+
console.log(" Note: .mcp.json contains your API key \u2014 add it to .gitignore.\n");
|
|
149
|
+
}
|
|
150
|
+
console.log(" Start a new Claude Code session to begin using it.\n");
|
|
124
151
|
} else {
|
|
125
|
-
console.log("
|
|
152
|
+
console.log(" Warning: Could not verify the saved configuration.\n");
|
|
153
|
+
console.log(" You can configure manually by adding to your Claude config:\n");
|
|
154
|
+
console.log(` claude mcp add codebyplan -e CODEBYPLAN_API_KEY=${apiKey} -- npx -y ${PACKAGE_NAME}
|
|
155
|
+
`);
|
|
126
156
|
}
|
|
127
157
|
} finally {
|
|
128
158
|
rl.close();
|
|
@@ -136,8 +166,8 @@ var init_setup = __esm({
|
|
|
136
166
|
});
|
|
137
167
|
|
|
138
168
|
// src/cli/config.ts
|
|
139
|
-
import { readFile } from "node:fs/promises";
|
|
140
|
-
import { join } from "node:path";
|
|
169
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
170
|
+
import { join as join2 } from "node:path";
|
|
141
171
|
function parseFlags(startIndex) {
|
|
142
172
|
const flags = {};
|
|
143
173
|
const args = process.argv.slice(startIndex);
|
|
@@ -159,8 +189,8 @@ async function resolveConfig(flags) {
|
|
|
159
189
|
let worktreeId = flags["worktree-id"] ?? process.env.CODEBYPLAN_WORKTREE_ID;
|
|
160
190
|
if (!repoId || !worktreeId) {
|
|
161
191
|
try {
|
|
162
|
-
const configPath =
|
|
163
|
-
const raw = await
|
|
192
|
+
const configPath = join2(projectPath, ".codebyplan.json");
|
|
193
|
+
const raw = await readFile2(configPath, "utf-8");
|
|
164
194
|
const config2 = JSON.parse(raw);
|
|
165
195
|
if (!repoId) repoId = config2.repo_id;
|
|
166
196
|
if (!worktreeId) worktreeId = config2.worktree_id;
|
|
@@ -354,7 +384,36 @@ function mergeSettings(template, local) {
|
|
|
354
384
|
}
|
|
355
385
|
return merged;
|
|
356
386
|
}
|
|
357
|
-
|
|
387
|
+
function mergeGlobalAndRepoSettings(global, repo) {
|
|
388
|
+
const merged = { ...global, ...repo };
|
|
389
|
+
const globalPerms = global.permissions && typeof global.permissions === "object" ? global.permissions : {};
|
|
390
|
+
const repoPerms = repo.permissions && typeof repo.permissions === "object" ? repo.permissions : {};
|
|
391
|
+
if (Object.keys(globalPerms).length > 0 || Object.keys(repoPerms).length > 0) {
|
|
392
|
+
const mergedPerms = { ...globalPerms, ...repoPerms };
|
|
393
|
+
for (const key of ARRAY_PERMISSION_KEYS) {
|
|
394
|
+
const globalArr = Array.isArray(globalPerms[key]) ? globalPerms[key] : [];
|
|
395
|
+
const repoArr = Array.isArray(repoPerms[key]) ? repoPerms[key] : [];
|
|
396
|
+
if (globalArr.length > 0 || repoArr.length > 0) {
|
|
397
|
+
mergedPerms[key] = [.../* @__PURE__ */ new Set([...globalArr, ...repoArr])];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
merged.permissions = mergedPerms;
|
|
401
|
+
}
|
|
402
|
+
return merged;
|
|
403
|
+
}
|
|
404
|
+
function stripPermissionsAllow(settings) {
|
|
405
|
+
if (!settings.permissions || typeof settings.permissions !== "object") {
|
|
406
|
+
return settings;
|
|
407
|
+
}
|
|
408
|
+
const perms = { ...settings.permissions };
|
|
409
|
+
delete perms.allow;
|
|
410
|
+
if (Object.keys(perms).length === 0) {
|
|
411
|
+
const { permissions: _, ...rest } = settings;
|
|
412
|
+
return rest;
|
|
413
|
+
}
|
|
414
|
+
return { ...settings, permissions: perms };
|
|
415
|
+
}
|
|
416
|
+
var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS, ARRAY_PERMISSION_KEYS;
|
|
358
417
|
var init_settings_merge = __esm({
|
|
359
418
|
"src/lib/settings-merge.ts"() {
|
|
360
419
|
"use strict";
|
|
@@ -368,12 +427,13 @@ var init_settings_merge = __esm({
|
|
|
368
427
|
"ask",
|
|
369
428
|
"additionalDirectories"
|
|
370
429
|
];
|
|
430
|
+
ARRAY_PERMISSION_KEYS = ["deny", "ask"];
|
|
371
431
|
}
|
|
372
432
|
});
|
|
373
433
|
|
|
374
434
|
// src/lib/hook-registry.ts
|
|
375
|
-
import { readdir, readFile as
|
|
376
|
-
import { join as
|
|
435
|
+
import { readdir, readFile as readFile3 } from "node:fs/promises";
|
|
436
|
+
import { join as join3 } from "node:path";
|
|
377
437
|
function parseHookMeta(content) {
|
|
378
438
|
const match = content.match(/^#\s*@hook:\s*(\S+)(?:\s+(.+))?$/m);
|
|
379
439
|
if (!match) return null;
|
|
@@ -392,7 +452,7 @@ async function discoverHooks(hooksDir) {
|
|
|
392
452
|
return discovered;
|
|
393
453
|
}
|
|
394
454
|
for (const filename of filenames) {
|
|
395
|
-
const content = await
|
|
455
|
+
const content = await readFile3(join3(hooksDir, filename), "utf-8");
|
|
396
456
|
const meta = parseHookMeta(content);
|
|
397
457
|
if (meta) {
|
|
398
458
|
discovered.set(filename.replace(/\.sh$/, ""), meta);
|
|
@@ -430,6 +490,25 @@ function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hook
|
|
|
430
490
|
}
|
|
431
491
|
return merged;
|
|
432
492
|
}
|
|
493
|
+
function stripDiscoveredHooks(config2, hooksRelPath = ".claude/hooks") {
|
|
494
|
+
const prefix = `bash ${hooksRelPath}/`;
|
|
495
|
+
const stripped = {};
|
|
496
|
+
for (const [event, matchers] of Object.entries(config2)) {
|
|
497
|
+
const filteredMatchers = [];
|
|
498
|
+
for (const matcher of matchers) {
|
|
499
|
+
const filteredHooks = matcher.hooks.filter(
|
|
500
|
+
(h) => !(h.command && h.command.startsWith(prefix) && h.command.endsWith(".sh"))
|
|
501
|
+
);
|
|
502
|
+
if (filteredHooks.length > 0) {
|
|
503
|
+
filteredMatchers.push({ matcher: matcher.matcher, hooks: filteredHooks });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (filteredMatchers.length > 0) {
|
|
507
|
+
stripped[event] = filteredMatchers;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return stripped;
|
|
511
|
+
}
|
|
433
512
|
var init_hook_registry = __esm({
|
|
434
513
|
"src/lib/hook-registry.ts"() {
|
|
435
514
|
"use strict";
|
|
@@ -478,38 +557,38 @@ var init_variables = __esm({
|
|
|
478
557
|
});
|
|
479
558
|
|
|
480
559
|
// src/lib/sync-engine.ts
|
|
481
|
-
import { readdir as readdir2, readFile as
|
|
482
|
-
import { join as
|
|
560
|
+
import { readdir as readdir2, readFile as readFile4, writeFile as writeFile2, unlink, mkdir, rmdir, chmod, stat } from "node:fs/promises";
|
|
561
|
+
import { join as join4, dirname } from "node:path";
|
|
483
562
|
function getTypeDir(claudeDir, dir) {
|
|
484
|
-
if (dir === "commands") return
|
|
485
|
-
return
|
|
563
|
+
if (dir === "commands") return join4(claudeDir, dir, "cbp");
|
|
564
|
+
return join4(claudeDir, dir);
|
|
486
565
|
}
|
|
487
566
|
function getFilePath(claudeDir, typeName, file) {
|
|
488
567
|
const cfg = typeConfig[typeName];
|
|
489
568
|
const typeDir = getTypeDir(claudeDir, cfg.dir);
|
|
490
569
|
if (cfg.subfolder) {
|
|
491
|
-
return
|
|
570
|
+
return join4(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
|
|
492
571
|
}
|
|
493
572
|
if (typeName === "command" && file.category) {
|
|
494
|
-
return
|
|
573
|
+
return join4(typeDir, file.category, `${file.name}${cfg.ext}`);
|
|
495
574
|
}
|
|
496
575
|
if (typeName === "template") {
|
|
497
|
-
return
|
|
576
|
+
return join4(typeDir, file.name);
|
|
498
577
|
}
|
|
499
|
-
return
|
|
578
|
+
return join4(typeDir, `${file.name}${cfg.ext}`);
|
|
500
579
|
}
|
|
501
580
|
async function readDirRecursive(dir, base = dir) {
|
|
502
581
|
const result = /* @__PURE__ */ new Map();
|
|
503
582
|
try {
|
|
504
583
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
505
584
|
for (const entry of entries) {
|
|
506
|
-
const fullPath =
|
|
585
|
+
const fullPath = join4(dir, entry.name);
|
|
507
586
|
if (entry.isDirectory()) {
|
|
508
587
|
const sub = await readDirRecursive(fullPath, base);
|
|
509
588
|
for (const [k, v] of sub) result.set(k, v);
|
|
510
589
|
} else {
|
|
511
590
|
const relPath = fullPath.slice(base.length + 1);
|
|
512
|
-
const fileContent = await
|
|
591
|
+
const fileContent = await readFile4(fullPath, "utf-8");
|
|
513
592
|
result.set(relPath, fileContent);
|
|
514
593
|
}
|
|
515
594
|
}
|
|
@@ -519,7 +598,7 @@ async function readDirRecursive(dir, base = dir) {
|
|
|
519
598
|
}
|
|
520
599
|
async function isGitWorktree(projectPath) {
|
|
521
600
|
try {
|
|
522
|
-
const gitPath =
|
|
601
|
+
const gitPath = join4(projectPath, ".git");
|
|
523
602
|
const info = await stat(gitPath);
|
|
524
603
|
return info.isFile();
|
|
525
604
|
} catch {
|
|
@@ -546,7 +625,7 @@ async function executeSyncToLocal(options) {
|
|
|
546
625
|
const syncData = syncRes.data;
|
|
547
626
|
const repoData = repoRes.data;
|
|
548
627
|
syncData.claude_md = [];
|
|
549
|
-
const claudeDir =
|
|
628
|
+
const claudeDir = join4(projectPath, ".claude");
|
|
550
629
|
const worktree = await isGitWorktree(projectPath);
|
|
551
630
|
const byType = {};
|
|
552
631
|
const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
|
|
@@ -571,19 +650,19 @@ async function executeSyncToLocal(options) {
|
|
|
571
650
|
remotePathMap.set(relPath, { content: substituted, name: remote.name });
|
|
572
651
|
}
|
|
573
652
|
for (const [relPath, { content, name }] of remotePathMap) {
|
|
574
|
-
const fullPath =
|
|
653
|
+
const fullPath = join4(targetDir, relPath);
|
|
575
654
|
const localContent = localFiles.get(relPath);
|
|
576
655
|
if (localContent === void 0) {
|
|
577
656
|
if (!dryRun) {
|
|
578
657
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
579
|
-
await
|
|
658
|
+
await writeFile2(fullPath, content, "utf-8");
|
|
580
659
|
if (typeName === "hook") await chmod(fullPath, 493);
|
|
581
660
|
}
|
|
582
661
|
result.created.push(name);
|
|
583
662
|
totals.created++;
|
|
584
663
|
} else if (localContent !== content) {
|
|
585
664
|
if (!dryRun) {
|
|
586
|
-
await
|
|
665
|
+
await writeFile2(fullPath, content, "utf-8");
|
|
587
666
|
if (typeName === "hook") await chmod(fullPath, 493);
|
|
588
667
|
}
|
|
589
668
|
result.updated.push(name);
|
|
@@ -595,7 +674,7 @@ async function executeSyncToLocal(options) {
|
|
|
595
674
|
}
|
|
596
675
|
for (const [relPath] of localFiles) {
|
|
597
676
|
if (!remotePathMap.has(relPath)) {
|
|
598
|
-
const fullPath =
|
|
677
|
+
const fullPath = join4(targetDir, relPath);
|
|
599
678
|
if (!dryRun) {
|
|
600
679
|
await unlink(fullPath);
|
|
601
680
|
await removeEmptyParents(fullPath, targetDir);
|
|
@@ -607,9 +686,65 @@ async function executeSyncToLocal(options) {
|
|
|
607
686
|
}
|
|
608
687
|
byType[`${typeName}s`] = result;
|
|
609
688
|
}
|
|
689
|
+
{
|
|
690
|
+
const typeName = "docs_stack";
|
|
691
|
+
const syncKey = "docs_stack";
|
|
692
|
+
const targetDir = join4(projectPath, "docs", "stack");
|
|
693
|
+
const remoteFiles = syncData[syncKey] ?? [];
|
|
694
|
+
const result = { created: [], updated: [], deleted: [], unchanged: [] };
|
|
695
|
+
if (remoteFiles.length > 0 && !dryRun) {
|
|
696
|
+
await mkdir(targetDir, { recursive: true });
|
|
697
|
+
}
|
|
698
|
+
const localFiles = await readDirRecursive(targetDir);
|
|
699
|
+
const remotePathMap = /* @__PURE__ */ new Map();
|
|
700
|
+
for (const remote of remoteFiles) {
|
|
701
|
+
const relPath = remote.category ? join4(remote.category, remote.name) : remote.name;
|
|
702
|
+
const substituted = substituteVariables(remote.content, repoData);
|
|
703
|
+
remotePathMap.set(relPath, { content: substituted, name: `${remote.category ?? ""}/${remote.name}` });
|
|
704
|
+
}
|
|
705
|
+
for (const [relPath, { content, name }] of remotePathMap) {
|
|
706
|
+
const fullPath = join4(targetDir, relPath);
|
|
707
|
+
const localContent = localFiles.get(relPath);
|
|
708
|
+
if (localContent === void 0) {
|
|
709
|
+
if (!dryRun) {
|
|
710
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
711
|
+
await writeFile2(fullPath, content, "utf-8");
|
|
712
|
+
}
|
|
713
|
+
result.created.push(name);
|
|
714
|
+
totals.created++;
|
|
715
|
+
} else if (localContent !== content) {
|
|
716
|
+
if (!dryRun) {
|
|
717
|
+
await writeFile2(fullPath, content, "utf-8");
|
|
718
|
+
}
|
|
719
|
+
result.updated.push(name);
|
|
720
|
+
totals.updated++;
|
|
721
|
+
} else {
|
|
722
|
+
result.unchanged.push(name);
|
|
723
|
+
totals.unchanged++;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
for (const [relPath] of localFiles) {
|
|
727
|
+
if (!remotePathMap.has(relPath)) {
|
|
728
|
+
const fullPath = join4(targetDir, relPath);
|
|
729
|
+
if (!dryRun) {
|
|
730
|
+
await unlink(fullPath);
|
|
731
|
+
await removeEmptyParents(fullPath, targetDir);
|
|
732
|
+
}
|
|
733
|
+
result.deleted.push(relPath);
|
|
734
|
+
totals.deleted++;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
byType[typeName] = result;
|
|
738
|
+
}
|
|
739
|
+
const globalSettingsFiles = syncData.global_settings ?? [];
|
|
740
|
+
let globalSettings = {};
|
|
741
|
+
for (const gf of globalSettingsFiles) {
|
|
742
|
+
const parsed = JSON.parse(substituteVariables(gf.content, repoData));
|
|
743
|
+
globalSettings = { ...globalSettings, ...parsed };
|
|
744
|
+
}
|
|
610
745
|
const specialTypes = {
|
|
611
|
-
claude_md: () =>
|
|
612
|
-
settings: () =>
|
|
746
|
+
claude_md: () => join4(projectPath, "CLAUDE.md"),
|
|
747
|
+
settings: () => join4(projectPath, ".claude", "settings.json")
|
|
613
748
|
};
|
|
614
749
|
for (const [typeName, getPath] of Object.entries(specialTypes)) {
|
|
615
750
|
const remoteFiles = syncData[typeName] ?? [];
|
|
@@ -619,29 +754,32 @@ async function executeSyncToLocal(options) {
|
|
|
619
754
|
const remoteContent = substituteVariables(remote.content, repoData);
|
|
620
755
|
let localContent;
|
|
621
756
|
try {
|
|
622
|
-
localContent = await
|
|
757
|
+
localContent = await readFile4(targetPath, "utf-8");
|
|
623
758
|
} catch {
|
|
624
759
|
}
|
|
625
760
|
if (typeName === "settings") {
|
|
626
|
-
const
|
|
627
|
-
const
|
|
761
|
+
const repoSettings = JSON.parse(remoteContent);
|
|
762
|
+
const combinedTemplate = mergeGlobalAndRepoSettings(globalSettings, repoSettings);
|
|
763
|
+
const hooksDir = join4(projectPath, ".claude", "hooks");
|
|
628
764
|
const discovered = await discoverHooks(hooksDir);
|
|
629
765
|
if (localContent === void 0) {
|
|
766
|
+
let finalSettings = stripPermissionsAllow(combinedTemplate);
|
|
630
767
|
if (discovered.size > 0) {
|
|
631
|
-
|
|
632
|
-
|
|
768
|
+
finalSettings.hooks = mergeDiscoveredHooks(
|
|
769
|
+
finalSettings.hooks ?? {},
|
|
633
770
|
discovered
|
|
634
771
|
);
|
|
635
772
|
}
|
|
636
773
|
if (!dryRun) {
|
|
637
774
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
638
|
-
await
|
|
775
|
+
await writeFile2(targetPath, JSON.stringify(finalSettings, null, 2) + "\n", "utf-8");
|
|
639
776
|
}
|
|
640
777
|
result.created.push(remote.name);
|
|
641
778
|
totals.created++;
|
|
642
779
|
} else {
|
|
643
780
|
const localSettings = JSON.parse(localContent);
|
|
644
|
-
|
|
781
|
+
let merged = mergeSettings(combinedTemplate, localSettings);
|
|
782
|
+
merged = stripPermissionsAllow(merged);
|
|
645
783
|
if (discovered.size > 0) {
|
|
646
784
|
merged.hooks = mergeDiscoveredHooks(
|
|
647
785
|
merged.hooks ?? {},
|
|
@@ -651,7 +789,7 @@ async function executeSyncToLocal(options) {
|
|
|
651
789
|
const mergedContent = JSON.stringify(merged, null, 2) + "\n";
|
|
652
790
|
if (localContent !== mergedContent) {
|
|
653
791
|
if (!dryRun) {
|
|
654
|
-
await
|
|
792
|
+
await writeFile2(targetPath, mergedContent, "utf-8");
|
|
655
793
|
}
|
|
656
794
|
result.updated.push(remote.name);
|
|
657
795
|
totals.updated++;
|
|
@@ -664,13 +802,13 @@ async function executeSyncToLocal(options) {
|
|
|
664
802
|
if (localContent === void 0) {
|
|
665
803
|
if (!dryRun) {
|
|
666
804
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
667
|
-
await
|
|
805
|
+
await writeFile2(targetPath, remoteContent, "utf-8");
|
|
668
806
|
}
|
|
669
807
|
result.created.push(remote.name);
|
|
670
808
|
totals.created++;
|
|
671
809
|
} else if (localContent !== remoteContent) {
|
|
672
810
|
if (!dryRun) {
|
|
673
|
-
await
|
|
811
|
+
await writeFile2(targetPath, remoteContent, "utf-8");
|
|
674
812
|
}
|
|
675
813
|
result.updated.push(remote.name);
|
|
676
814
|
totals.updated++;
|
|
@@ -734,8 +872,8 @@ var init_confirm = __esm({
|
|
|
734
872
|
});
|
|
735
873
|
|
|
736
874
|
// src/lib/tech-detect.ts
|
|
737
|
-
import { readFile as
|
|
738
|
-
import { join as
|
|
875
|
+
import { readFile as readFile5, access } from "node:fs/promises";
|
|
876
|
+
import { join as join5 } from "node:path";
|
|
739
877
|
async function fileExists(filePath) {
|
|
740
878
|
try {
|
|
741
879
|
await access(filePath);
|
|
@@ -747,7 +885,7 @@ async function fileExists(filePath) {
|
|
|
747
885
|
async function detectTechStack(projectPath) {
|
|
748
886
|
const seen = /* @__PURE__ */ new Map();
|
|
749
887
|
try {
|
|
750
|
-
const raw = await
|
|
888
|
+
const raw = await readFile5(join5(projectPath, "package.json"), "utf-8");
|
|
751
889
|
const pkg = JSON.parse(raw);
|
|
752
890
|
const allDeps = {
|
|
753
891
|
...pkg.dependencies,
|
|
@@ -766,7 +904,7 @@ async function detectTechStack(projectPath) {
|
|
|
766
904
|
}
|
|
767
905
|
for (const { file, rule } of CONFIG_FILE_MAP) {
|
|
768
906
|
const key = rule.name.toLowerCase();
|
|
769
|
-
if (!seen.has(key) && await fileExists(
|
|
907
|
+
if (!seen.has(key) && await fileExists(join5(projectPath, file))) {
|
|
770
908
|
seen.set(key, { name: rule.name, category: rule.category });
|
|
771
909
|
}
|
|
772
910
|
}
|
|
@@ -1022,19 +1160,20 @@ var init_pull = __esm({
|
|
|
1022
1160
|
});
|
|
1023
1161
|
|
|
1024
1162
|
// src/cli/fileMapper.ts
|
|
1025
|
-
import { readdir as readdir3, readFile as
|
|
1026
|
-
import { join as
|
|
1163
|
+
import { readdir as readdir3, readFile as readFile6 } from "node:fs/promises";
|
|
1164
|
+
import { join as join6, extname } from "node:path";
|
|
1027
1165
|
function compositeKey(type, name, category) {
|
|
1028
1166
|
return category ? `${type}:${category}/${name}` : `${type}:${name}`;
|
|
1029
1167
|
}
|
|
1030
|
-
async function scanLocalFiles(claudeDir) {
|
|
1168
|
+
async function scanLocalFiles(claudeDir, projectPath) {
|
|
1031
1169
|
const result = /* @__PURE__ */ new Map();
|
|
1032
|
-
await scanCommands(
|
|
1033
|
-
await scanSubfolderType(
|
|
1034
|
-
await scanSubfolderType(
|
|
1035
|
-
await scanFlatType(
|
|
1036
|
-
await scanFlatType(
|
|
1037
|
-
await scanTemplates(
|
|
1170
|
+
await scanCommands(join6(claudeDir, "commands", "cbp"), result);
|
|
1171
|
+
await scanSubfolderType(join6(claudeDir, "agents"), "agent", "AGENT.md", result);
|
|
1172
|
+
await scanSubfolderType(join6(claudeDir, "skills"), "skill", "SKILL.md", result);
|
|
1173
|
+
await scanFlatType(join6(claudeDir, "rules"), "rule", ".md", result);
|
|
1174
|
+
await scanFlatType(join6(claudeDir, "hooks"), "hook", ".sh", result);
|
|
1175
|
+
await scanTemplates(join6(claudeDir, "templates"), result);
|
|
1176
|
+
await scanSettings(claudeDir, projectPath, result);
|
|
1038
1177
|
return result;
|
|
1039
1178
|
}
|
|
1040
1179
|
async function scanCommands(dir, result) {
|
|
@@ -1049,10 +1188,10 @@ async function scanCommandsRecursive(baseDir, currentDir, result) {
|
|
|
1049
1188
|
}
|
|
1050
1189
|
for (const entry of entries) {
|
|
1051
1190
|
if (entry.isDirectory()) {
|
|
1052
|
-
await scanCommandsRecursive(baseDir,
|
|
1191
|
+
await scanCommandsRecursive(baseDir, join6(currentDir, entry.name), result);
|
|
1053
1192
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1054
1193
|
const name = entry.name.slice(0, -3);
|
|
1055
|
-
const content = await
|
|
1194
|
+
const content = await readFile6(join6(currentDir, entry.name), "utf-8");
|
|
1056
1195
|
const relDir = currentDir.slice(baseDir.length + 1);
|
|
1057
1196
|
const category = relDir || null;
|
|
1058
1197
|
const key = compositeKey("command", name, category);
|
|
@@ -1069,9 +1208,9 @@ async function scanSubfolderType(dir, type, fileName, result) {
|
|
|
1069
1208
|
}
|
|
1070
1209
|
for (const entry of entries) {
|
|
1071
1210
|
if (entry.isDirectory()) {
|
|
1072
|
-
const filePath =
|
|
1211
|
+
const filePath = join6(dir, entry.name, fileName);
|
|
1073
1212
|
try {
|
|
1074
|
-
const content = await
|
|
1213
|
+
const content = await readFile6(filePath, "utf-8");
|
|
1075
1214
|
const key = compositeKey(type, entry.name, null);
|
|
1076
1215
|
result.set(key, { type, name: entry.name, category: null, content });
|
|
1077
1216
|
} catch {
|
|
@@ -1089,7 +1228,7 @@ async function scanFlatType(dir, type, ext, result) {
|
|
|
1089
1228
|
for (const entry of entries) {
|
|
1090
1229
|
if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
1091
1230
|
const name = entry.name.slice(0, -ext.length);
|
|
1092
|
-
const content = await
|
|
1231
|
+
const content = await readFile6(join6(dir, entry.name), "utf-8");
|
|
1093
1232
|
const key = compositeKey(type, name, null);
|
|
1094
1233
|
result.set(key, { type, name, category: null, content });
|
|
1095
1234
|
}
|
|
@@ -1104,15 +1243,49 @@ async function scanTemplates(dir, result) {
|
|
|
1104
1243
|
}
|
|
1105
1244
|
for (const entry of entries) {
|
|
1106
1245
|
if (entry.isFile() && extname(entry.name)) {
|
|
1107
|
-
const content = await
|
|
1246
|
+
const content = await readFile6(join6(dir, entry.name), "utf-8");
|
|
1108
1247
|
const key = compositeKey("template", entry.name, null);
|
|
1109
1248
|
result.set(key, { type: "template", name: entry.name, category: null, content });
|
|
1110
1249
|
}
|
|
1111
1250
|
}
|
|
1112
1251
|
}
|
|
1252
|
+
async function scanSettings(claudeDir, projectPath, result) {
|
|
1253
|
+
const settingsPath = join6(claudeDir, "settings.json");
|
|
1254
|
+
let raw;
|
|
1255
|
+
try {
|
|
1256
|
+
raw = await readFile6(settingsPath, "utf-8");
|
|
1257
|
+
} catch {
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
let parsed;
|
|
1261
|
+
try {
|
|
1262
|
+
parsed = JSON.parse(raw);
|
|
1263
|
+
} catch {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
parsed = stripPermissionsAllow(parsed);
|
|
1267
|
+
if (parsed.hooks && typeof parsed.hooks === "object") {
|
|
1268
|
+
const hooksDir = projectPath ? join6(projectPath, ".claude", "hooks") : join6(claudeDir, "hooks");
|
|
1269
|
+
const discovered = await discoverHooks(hooksDir);
|
|
1270
|
+
if (discovered.size > 0) {
|
|
1271
|
+
parsed.hooks = stripDiscoveredHooks(
|
|
1272
|
+
parsed.hooks,
|
|
1273
|
+
".claude/hooks"
|
|
1274
|
+
);
|
|
1275
|
+
if (Object.keys(parsed.hooks).length === 0) {
|
|
1276
|
+
delete parsed.hooks;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const content = JSON.stringify(parsed, null, 2) + "\n";
|
|
1281
|
+
const key = compositeKey("settings", "settings", null);
|
|
1282
|
+
result.set(key, { type: "settings", name: "settings", category: null, content });
|
|
1283
|
+
}
|
|
1113
1284
|
var init_fileMapper = __esm({
|
|
1114
1285
|
"src/cli/fileMapper.ts"() {
|
|
1115
1286
|
"use strict";
|
|
1287
|
+
init_settings_merge();
|
|
1288
|
+
init_hook_registry();
|
|
1116
1289
|
}
|
|
1117
1290
|
});
|
|
1118
1291
|
|
|
@@ -1190,11 +1363,12 @@ __export(push_exports, {
|
|
|
1190
1363
|
runPush: () => runPush
|
|
1191
1364
|
});
|
|
1192
1365
|
import { stat as stat2 } from "node:fs/promises";
|
|
1193
|
-
import { join as
|
|
1366
|
+
import { join as join7 } from "node:path";
|
|
1194
1367
|
async function runPush() {
|
|
1195
1368
|
const flags = parseFlags(3);
|
|
1196
1369
|
const dryRun = hasFlag("dry-run", 3);
|
|
1197
1370
|
const force = hasFlag("force", 3);
|
|
1371
|
+
const isGlobal = hasFlag("global", 3);
|
|
1198
1372
|
validateApiKey();
|
|
1199
1373
|
const config2 = await resolveConfig(flags);
|
|
1200
1374
|
const { repoId, projectPath } = config2;
|
|
@@ -1205,7 +1379,7 @@ async function runPush() {
|
|
|
1205
1379
|
if (dryRun) console.log(` Mode: dry-run (no changes will be made)`);
|
|
1206
1380
|
if (force) console.log(` Mode: force (no conflict prompts)`);
|
|
1207
1381
|
console.log();
|
|
1208
|
-
const claudeDir =
|
|
1382
|
+
const claudeDir = join7(projectPath, ".claude");
|
|
1209
1383
|
try {
|
|
1210
1384
|
await stat2(claudeDir);
|
|
1211
1385
|
} catch {
|
|
@@ -1213,7 +1387,7 @@ async function runPush() {
|
|
|
1213
1387
|
return;
|
|
1214
1388
|
}
|
|
1215
1389
|
console.log(" Scanning local files...");
|
|
1216
|
-
const localFiles = await scanLocalFiles(claudeDir);
|
|
1390
|
+
const localFiles = await scanLocalFiles(claudeDir, projectPath);
|
|
1217
1391
|
console.log(` Found ${localFiles.size} local files.`);
|
|
1218
1392
|
console.log(" Fetching remote state...");
|
|
1219
1393
|
const [syncRes, repoRes] = await Promise.all([
|
|
@@ -1302,7 +1476,8 @@ async function runPush() {
|
|
|
1302
1476
|
type: f.type,
|
|
1303
1477
|
name: f.name,
|
|
1304
1478
|
category: f.category,
|
|
1305
|
-
content: f.content
|
|
1479
|
+
content: f.content,
|
|
1480
|
+
...f.type === "settings" ? { scope: isGlobal ? "global" : "repo" } : {}
|
|
1306
1481
|
})),
|
|
1307
1482
|
delete_keys: toDelete
|
|
1308
1483
|
});
|
|
@@ -1336,7 +1511,8 @@ function flattenSyncData(data) {
|
|
|
1336
1511
|
skills: "skill",
|
|
1337
1512
|
rules: "rule",
|
|
1338
1513
|
hooks: "hook",
|
|
1339
|
-
templates: "template"
|
|
1514
|
+
templates: "template",
|
|
1515
|
+
settings: "settings"
|
|
1340
1516
|
};
|
|
1341
1517
|
for (const [syncKey, typeName] of Object.entries(typeMap)) {
|
|
1342
1518
|
const files = data[syncKey] ?? [];
|
|
@@ -1377,8 +1553,8 @@ __export(init_exports, {
|
|
|
1377
1553
|
});
|
|
1378
1554
|
import { createInterface as createInterface4 } from "node:readline/promises";
|
|
1379
1555
|
import { stdin as stdin4, stdout as stdout4 } from "node:process";
|
|
1380
|
-
import { writeFile as
|
|
1381
|
-
import { join as
|
|
1556
|
+
import { writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
1557
|
+
import { join as join8, dirname as dirname2 } from "node:path";
|
|
1382
1558
|
async function runInit() {
|
|
1383
1559
|
const flags = parseFlags(3);
|
|
1384
1560
|
const projectPath = flags["path"] ?? process.cwd();
|
|
@@ -1417,27 +1593,27 @@ async function runInit() {
|
|
|
1417
1593
|
if (match) worktreeId = match.id;
|
|
1418
1594
|
} catch {
|
|
1419
1595
|
}
|
|
1420
|
-
const configPath =
|
|
1596
|
+
const configPath = join8(projectPath, ".codebyplan.json");
|
|
1421
1597
|
const configData = { repo_id: repoId };
|
|
1422
1598
|
if (worktreeId) configData.worktree_id = worktreeId;
|
|
1423
1599
|
const configContent = JSON.stringify(configData, null, 2) + "\n";
|
|
1424
|
-
await
|
|
1600
|
+
await writeFile3(configPath, configContent, "utf-8");
|
|
1425
1601
|
console.log(` Created ${configPath}`);
|
|
1426
1602
|
const seedAnswer = (await rl.question("\n Seed with CodeByPlan defaults? (Y/n): ")).trim().toLowerCase();
|
|
1427
1603
|
if (seedAnswer === "" || seedAnswer === "y" || seedAnswer === "yes") {
|
|
1428
1604
|
let getFilePath3 = function(typeName, file) {
|
|
1429
1605
|
const cfg = typeConfig2[typeName];
|
|
1430
|
-
const typeDir = typeName === "command" ?
|
|
1606
|
+
const typeDir = typeName === "command" ? join8(claudeDir, cfg.dir, "cbp") : join8(claudeDir, cfg.dir);
|
|
1431
1607
|
if (cfg.subfolder) {
|
|
1432
|
-
return
|
|
1608
|
+
return join8(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
|
|
1433
1609
|
}
|
|
1434
1610
|
if (typeName === "command" && file.category) {
|
|
1435
|
-
return
|
|
1611
|
+
return join8(typeDir, file.category, `${file.name}${cfg.ext}`);
|
|
1436
1612
|
}
|
|
1437
1613
|
if (typeName === "template") {
|
|
1438
|
-
return
|
|
1614
|
+
return join8(typeDir, file.name);
|
|
1439
1615
|
}
|
|
1440
|
-
return
|
|
1616
|
+
return join8(typeDir, `${file.name}${cfg.ext}`);
|
|
1441
1617
|
};
|
|
1442
1618
|
var getFilePath2 = getFilePath3;
|
|
1443
1619
|
console.log("\n Fetching default files...");
|
|
@@ -1452,7 +1628,7 @@ async function runInit() {
|
|
|
1452
1628
|
printNextSteps(projectPath);
|
|
1453
1629
|
return;
|
|
1454
1630
|
}
|
|
1455
|
-
const claudeDir =
|
|
1631
|
+
const claudeDir = join8(projectPath, ".claude");
|
|
1456
1632
|
let written = 0;
|
|
1457
1633
|
const typeConfig2 = {
|
|
1458
1634
|
command: { dir: "commands", ext: ".md" },
|
|
@@ -1475,7 +1651,7 @@ async function runInit() {
|
|
|
1475
1651
|
for (const file of files) {
|
|
1476
1652
|
const filePath = getFilePath3(typeName, file);
|
|
1477
1653
|
await mkdir2(dirname2(filePath), { recursive: true });
|
|
1478
|
-
await
|
|
1654
|
+
await writeFile3(filePath, file.content, "utf-8");
|
|
1479
1655
|
if (typeName === "hook") await chmod2(filePath, 493);
|
|
1480
1656
|
written++;
|
|
1481
1657
|
}
|
|
@@ -1484,16 +1660,16 @@ async function runInit() {
|
|
|
1484
1660
|
...defaultsData.claude_md ?? []
|
|
1485
1661
|
];
|
|
1486
1662
|
for (const file of specialFiles) {
|
|
1487
|
-
const targetPath =
|
|
1663
|
+
const targetPath = join8(projectPath, "CLAUDE.md");
|
|
1488
1664
|
await mkdir2(dirname2(targetPath), { recursive: true });
|
|
1489
|
-
await
|
|
1665
|
+
await writeFile3(targetPath, file.content, "utf-8");
|
|
1490
1666
|
written++;
|
|
1491
1667
|
}
|
|
1492
1668
|
const settingsFiles = defaultsData.settings ?? [];
|
|
1493
1669
|
for (const file of settingsFiles) {
|
|
1494
|
-
const targetPath =
|
|
1670
|
+
const targetPath = join8(claudeDir, "settings.json");
|
|
1495
1671
|
await mkdir2(dirname2(targetPath), { recursive: true });
|
|
1496
|
-
await
|
|
1672
|
+
await writeFile3(targetPath, file.content, "utf-8");
|
|
1497
1673
|
written++;
|
|
1498
1674
|
}
|
|
1499
1675
|
console.log(` Wrote ${written} files to .claude/
|
|
@@ -1516,7 +1692,7 @@ async function runInit() {
|
|
|
1516
1692
|
allFiles.push({ type: "claude_md", name: file.name, content: file.content });
|
|
1517
1693
|
}
|
|
1518
1694
|
for (const file of settingsFiles) {
|
|
1519
|
-
allFiles.push({ type: "settings", name: file.name, content: file.content });
|
|
1695
|
+
allFiles.push({ type: "settings", name: file.name, content: file.content, scope: file.scope ?? "repo" });
|
|
1520
1696
|
}
|
|
1521
1697
|
if (allFiles.length > 0) {
|
|
1522
1698
|
await apiPost("/sync/files", {
|
|
@@ -23300,21 +23476,6 @@ function registerReadTools(server) {
|
|
|
23300
23476
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23301
23477
|
}
|
|
23302
23478
|
});
|
|
23303
|
-
server.registerTool("get_handoff", {
|
|
23304
|
-
description: "Get handoff state for a repo (status, summary, resume command/context).",
|
|
23305
|
-
inputSchema: {
|
|
23306
|
-
repo_id: external_exports.string().uuid().describe("The repo UUID")
|
|
23307
|
-
}
|
|
23308
|
-
}, async ({ repo_id }) => {
|
|
23309
|
-
try {
|
|
23310
|
-
const res = await apiGet(`/repos/${repo_id}`);
|
|
23311
|
-
const { id, name, handoff_status, handoff_summary, handoff_resume_command, handoff_resume_context, handoff_updated_at } = res.data;
|
|
23312
|
-
const data = { id, name, handoff_status, handoff_summary, handoff_resume_command, handoff_resume_context, handoff_updated_at };
|
|
23313
|
-
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
23314
|
-
} catch (err) {
|
|
23315
|
-
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23316
|
-
}
|
|
23317
|
-
});
|
|
23318
23479
|
server.registerTool("get_sync_status", {
|
|
23319
23480
|
description: "Get cross-repo sync status. Shows which repos need a claude files sync based on latest updates.",
|
|
23320
23481
|
inputSchema: {}
|
|
@@ -23326,6 +23487,22 @@ function registerReadTools(server) {
|
|
|
23326
23487
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23327
23488
|
}
|
|
23328
23489
|
});
|
|
23490
|
+
server.registerTool("get_next_action", {
|
|
23491
|
+
description: "Compute the next action for a repo based on current workflow state. Returns command, instructions, state, and context.",
|
|
23492
|
+
inputSchema: {
|
|
23493
|
+
repo_id: external_exports.string().uuid().describe("The repo UUID"),
|
|
23494
|
+
worktree_id: external_exports.string().uuid().optional().describe("Optional worktree UUID to filter by assignment")
|
|
23495
|
+
}
|
|
23496
|
+
}, async ({ repo_id, worktree_id }) => {
|
|
23497
|
+
try {
|
|
23498
|
+
const params = {};
|
|
23499
|
+
if (worktree_id) params.worktree_id = worktree_id;
|
|
23500
|
+
const res = await apiGet(`/repos/${repo_id}/next-action`, params);
|
|
23501
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
23502
|
+
} catch (err) {
|
|
23503
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23504
|
+
}
|
|
23505
|
+
});
|
|
23329
23506
|
server.registerTool("get_worktrees", {
|
|
23330
23507
|
description: "List worktrees for a repo. Optionally filter by status.",
|
|
23331
23508
|
inputSchema: {
|
|
@@ -23434,24 +23611,37 @@ function registerWriteTools(server) {
|
|
|
23434
23611
|
description: "Create a new checkpoint for a repo. Optionally connect it to a launch via launch_id.",
|
|
23435
23612
|
inputSchema: {
|
|
23436
23613
|
repo_id: external_exports.string().uuid().describe("The repo UUID"),
|
|
23437
|
-
title: external_exports.string().describe("Checkpoint title"),
|
|
23614
|
+
title: external_exports.string().optional().describe("Checkpoint title (optional \u2014 Claude can generate if missing)"),
|
|
23438
23615
|
number: external_exports.number().int().describe("Checkpoint number (e.g. 1 for CHK-001)"),
|
|
23439
|
-
goal: external_exports.string().optional().describe("Checkpoint goal description"),
|
|
23616
|
+
goal: external_exports.string().optional().describe("Checkpoint goal description (max 300 chars, brief overview)"),
|
|
23440
23617
|
deadline: external_exports.string().optional().describe("Deadline date (ISO format)"),
|
|
23441
23618
|
status: external_exports.string().optional().describe("Initial status (default: pending). Use 'draft' for checkpoints not ready for development."),
|
|
23442
|
-
launch_id: external_exports.string().uuid().optional().describe("Optional launch UUID to connect this checkpoint to")
|
|
23443
|
-
|
|
23444
|
-
|
|
23619
|
+
launch_id: external_exports.string().uuid().optional().describe("Optional launch UUID to connect this checkpoint to"),
|
|
23620
|
+
ideas: external_exports.array(external_exports.object({
|
|
23621
|
+
description: external_exports.string().describe("Idea description"),
|
|
23622
|
+
requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
|
|
23623
|
+
images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
|
|
23624
|
+
})).optional().describe("Ideas array \u2014 each idea has description, requirements[], images[]"),
|
|
23625
|
+
context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
|
|
23626
|
+
research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
|
|
23627
|
+
qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
|
|
23628
|
+
}
|
|
23629
|
+
}, async ({ repo_id, title, number: number3, goal, deadline, status, launch_id, ideas, context, research, qa }) => {
|
|
23445
23630
|
try {
|
|
23446
|
-
const
|
|
23631
|
+
const body = {
|
|
23447
23632
|
repo_id,
|
|
23448
|
-
title,
|
|
23633
|
+
title: title ?? null,
|
|
23449
23634
|
number: number3,
|
|
23450
23635
|
goal: goal ?? null,
|
|
23451
23636
|
deadline: deadline ?? null,
|
|
23452
23637
|
status: status ?? "pending",
|
|
23453
23638
|
launch_id: launch_id ?? null
|
|
23454
|
-
}
|
|
23639
|
+
};
|
|
23640
|
+
if (ideas !== void 0) body.ideas = ideas;
|
|
23641
|
+
if (context !== void 0) body.context = context;
|
|
23642
|
+
if (research !== void 0) body.research = research;
|
|
23643
|
+
if (qa !== void 0) body.qa = qa;
|
|
23644
|
+
const res = await apiPost("/checkpoints", body);
|
|
23455
23645
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
23456
23646
|
} catch (err) {
|
|
23457
23647
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
@@ -23461,16 +23651,24 @@ function registerWriteTools(server) {
|
|
|
23461
23651
|
description: "Update an existing checkpoint. Can connect or disconnect a launch via launch_id.",
|
|
23462
23652
|
inputSchema: {
|
|
23463
23653
|
checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID"),
|
|
23464
|
-
title: external_exports.string().optional().describe("New title"),
|
|
23465
|
-
goal: external_exports.string().optional().describe("New goal"),
|
|
23654
|
+
title: external_exports.string().nullable().optional().describe("New title (or null to clear)"),
|
|
23655
|
+
goal: external_exports.string().optional().describe("New goal (max 300 chars, brief overview)"),
|
|
23466
23656
|
status: external_exports.string().optional().describe("New status (draft, pending, active, completed)"),
|
|
23467
23657
|
deadline: external_exports.string().optional().describe("New deadline (ISO format)"),
|
|
23468
23658
|
completed_at: external_exports.string().optional().describe("Completion timestamp (ISO format)"),
|
|
23469
23659
|
launch_id: external_exports.string().uuid().nullable().optional().describe("Launch UUID to connect (or null to disconnect)"),
|
|
23470
23660
|
worktree_id: external_exports.string().uuid().nullable().optional().describe("Worktree UUID to assign (or null to unassign)"),
|
|
23471
|
-
assigned_to: external_exports.string().nullable().optional().describe("Who/what claimed this checkpoint")
|
|
23472
|
-
|
|
23473
|
-
|
|
23661
|
+
assigned_to: external_exports.string().nullable().optional().describe("Who/what claimed this checkpoint"),
|
|
23662
|
+
ideas: external_exports.array(external_exports.object({
|
|
23663
|
+
description: external_exports.string().describe("Idea description"),
|
|
23664
|
+
requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
|
|
23665
|
+
images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
|
|
23666
|
+
})).optional().describe("Ideas array \u2014 each idea has description, requirements[], images[]"),
|
|
23667
|
+
context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
|
|
23668
|
+
research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
|
|
23669
|
+
qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
|
|
23670
|
+
}
|
|
23671
|
+
}, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, ideas, context, research, qa }) => {
|
|
23474
23672
|
const update = {};
|
|
23475
23673
|
if (title !== void 0) update.title = title;
|
|
23476
23674
|
if (goal !== void 0) update.goal = goal;
|
|
@@ -23480,6 +23678,10 @@ function registerWriteTools(server) {
|
|
|
23480
23678
|
if (launch_id !== void 0) update.launch_id = launch_id;
|
|
23481
23679
|
if (worktree_id !== void 0) update.worktree_id = worktree_id;
|
|
23482
23680
|
if (assigned_to !== void 0) update.assigned_to = assigned_to;
|
|
23681
|
+
if (ideas !== void 0) update.ideas = ideas;
|
|
23682
|
+
if (context !== void 0) update.context = context;
|
|
23683
|
+
if (research !== void 0) update.research = research;
|
|
23684
|
+
if (qa !== void 0) update.qa = qa;
|
|
23483
23685
|
if (Object.keys(update).length === 0) {
|
|
23484
23686
|
return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
|
|
23485
23687
|
}
|
|
@@ -23515,17 +23717,24 @@ function registerWriteTools(server) {
|
|
|
23515
23717
|
title: external_exports.string().describe("Task title"),
|
|
23516
23718
|
number: external_exports.number().int().describe("Task number (e.g. 1 for TASK-1)"),
|
|
23517
23719
|
requirements: external_exports.string().optional().describe("Task requirements text"),
|
|
23518
|
-
status: external_exports.string().optional().describe("Initial status (default: pending)")
|
|
23720
|
+
status: external_exports.string().optional().describe("Initial status (default: pending)"),
|
|
23721
|
+
context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints)"),
|
|
23722
|
+
qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
|
|
23723
|
+
research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)")
|
|
23519
23724
|
}
|
|
23520
|
-
}, async ({ checkpoint_id, title, number: number3, requirements, status }) => {
|
|
23725
|
+
}, async ({ checkpoint_id, title, number: number3, requirements, status, context, qa, research }) => {
|
|
23521
23726
|
try {
|
|
23522
|
-
const
|
|
23727
|
+
const body = {
|
|
23523
23728
|
checkpoint_id,
|
|
23524
23729
|
title,
|
|
23525
23730
|
number: number3,
|
|
23526
23731
|
requirements: requirements ?? null,
|
|
23527
23732
|
status: status ?? "pending"
|
|
23528
|
-
}
|
|
23733
|
+
};
|
|
23734
|
+
if (context !== void 0) body.context = context;
|
|
23735
|
+
if (qa !== void 0) body.qa = qa;
|
|
23736
|
+
if (research !== void 0) body.research = research;
|
|
23737
|
+
const res = await apiPost("/tasks", body);
|
|
23529
23738
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
23530
23739
|
} catch (err) {
|
|
23531
23740
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
@@ -23541,17 +23750,25 @@ function registerWriteTools(server) {
|
|
|
23541
23750
|
files_changed: external_exports.array(external_exports.object({
|
|
23542
23751
|
path: external_exports.string().describe("File path relative to repo root"),
|
|
23543
23752
|
action: external_exports.string().describe("File action (new, modified, deleted)"),
|
|
23544
|
-
status: external_exports.string().describe("Approval status (approved, not_approved)")
|
|
23753
|
+
status: external_exports.string().describe("Approval status (approved, not_approved)"),
|
|
23754
|
+
claude_approved: external_exports.boolean().optional().describe("Whether Claude's automated checks passed for this file"),
|
|
23755
|
+
user_approved: external_exports.boolean().optional().describe("Whether the user has approved this file (via git add or web UI)")
|
|
23545
23756
|
})).optional().describe("Files changed across all rounds"),
|
|
23546
|
-
claim_worktree_id: external_exports.string().uuid().optional().describe("Worktree UUID to auto-claim the parent checkpoint when setting status to in_progress")
|
|
23757
|
+
claim_worktree_id: external_exports.string().uuid().optional().describe("Worktree UUID to auto-claim the parent checkpoint when setting status to in_progress"),
|
|
23758
|
+
context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints)"),
|
|
23759
|
+
qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
|
|
23760
|
+
research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)")
|
|
23547
23761
|
}
|
|
23548
|
-
}, async ({ task_id, title, requirements, status, files_changed, claim_worktree_id }) => {
|
|
23762
|
+
}, async ({ task_id, title, requirements, status, files_changed, claim_worktree_id, context, qa, research }) => {
|
|
23549
23763
|
const update = {};
|
|
23550
23764
|
if (title !== void 0) update.title = title;
|
|
23551
23765
|
if (requirements !== void 0) update.requirements = requirements;
|
|
23552
23766
|
if (status !== void 0) update.status = status;
|
|
23553
23767
|
if (files_changed !== void 0) update.files_changed = files_changed;
|
|
23554
23768
|
if (claim_worktree_id !== void 0) update.claim_worktree_id = claim_worktree_id;
|
|
23769
|
+
if (context !== void 0) update.context = context;
|
|
23770
|
+
if (qa !== void 0) update.qa = qa;
|
|
23771
|
+
if (research !== void 0) update.research = research;
|
|
23555
23772
|
if (Object.keys(update).length === 0) {
|
|
23556
23773
|
return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
|
|
23557
23774
|
}
|
|
@@ -23585,17 +23802,22 @@ function registerWriteTools(server) {
|
|
|
23585
23802
|
number: external_exports.number().int().describe("Round number"),
|
|
23586
23803
|
requirements: external_exports.string().optional().describe("Round requirements text"),
|
|
23587
23804
|
status: external_exports.string().optional().describe("Initial status (default: pending)"),
|
|
23588
|
-
started_at: external_exports.string().optional().describe("Start timestamp (ISO format)")
|
|
23805
|
+
started_at: external_exports.string().optional().describe("Start timestamp (ISO format)"),
|
|
23806
|
+
context: external_exports.any().optional().describe("Context JSONB"),
|
|
23807
|
+
qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
|
|
23589
23808
|
}
|
|
23590
|
-
}, async ({ task_id, number: number3, requirements, status, started_at }) => {
|
|
23809
|
+
}, async ({ task_id, number: number3, requirements, status, started_at, context, qa }) => {
|
|
23591
23810
|
try {
|
|
23592
|
-
const
|
|
23811
|
+
const body = {
|
|
23593
23812
|
task_id,
|
|
23594
23813
|
number: number3,
|
|
23595
23814
|
requirements: requirements ?? null,
|
|
23596
23815
|
status: status ?? "pending",
|
|
23597
23816
|
started_at: started_at ?? null
|
|
23598
|
-
}
|
|
23817
|
+
};
|
|
23818
|
+
if (context !== void 0) body.context = context;
|
|
23819
|
+
if (qa !== void 0) body.qa = qa;
|
|
23820
|
+
const res = await apiPost("/rounds", body);
|
|
23599
23821
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
23600
23822
|
} catch (err) {
|
|
23601
23823
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
@@ -23613,10 +23835,14 @@ function registerWriteTools(server) {
|
|
|
23613
23835
|
files_changed: external_exports.array(external_exports.object({
|
|
23614
23836
|
path: external_exports.string().describe("File path relative to repo root"),
|
|
23615
23837
|
action: external_exports.string().describe("File action (new, modified, deleted)"),
|
|
23616
|
-
status: external_exports.string().describe("Approval status (approved, not_approved)")
|
|
23617
|
-
|
|
23618
|
-
|
|
23619
|
-
|
|
23838
|
+
status: external_exports.string().describe("Approval status (approved, not_approved)"),
|
|
23839
|
+
claude_approved: external_exports.boolean().optional().describe("Whether Claude's automated checks passed for this file"),
|
|
23840
|
+
user_approved: external_exports.boolean().optional().describe("Whether the user has approved this file (via git add or web UI)")
|
|
23841
|
+
})).optional().describe("Files changed in this round with approval status"),
|
|
23842
|
+
context: external_exports.any().optional().describe("Context JSONB"),
|
|
23843
|
+
qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
|
|
23844
|
+
}
|
|
23845
|
+
}, async ({ round_id, requirements, status, started_at, completed_at, duration_minutes, files_changed, context, qa }) => {
|
|
23620
23846
|
const update = {};
|
|
23621
23847
|
if (requirements !== void 0) update.requirements = requirements;
|
|
23622
23848
|
if (status !== void 0) update.status = status;
|
|
@@ -23624,6 +23850,8 @@ function registerWriteTools(server) {
|
|
|
23624
23850
|
if (completed_at !== void 0) update.completed_at = completed_at;
|
|
23625
23851
|
if (duration_minutes !== void 0) update.duration_minutes = duration_minutes;
|
|
23626
23852
|
if (files_changed !== void 0) update.files_changed = files_changed;
|
|
23853
|
+
if (context !== void 0) update.context = context;
|
|
23854
|
+
if (qa !== void 0) update.qa = qa;
|
|
23627
23855
|
if (Object.keys(update).length === 0) {
|
|
23628
23856
|
return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
|
|
23629
23857
|
}
|
|
@@ -23783,7 +24011,7 @@ function registerWriteTools(server) {
|
|
|
23783
24011
|
}
|
|
23784
24012
|
});
|
|
23785
24013
|
server.registerTool("sync_claude_files", {
|
|
23786
|
-
description: "Sync .claude infrastructure from CodeByPlan DB to local project. Uses aggregated defaults (latest version across all repos) for shared files (commands, agents, skills, rules, hooks, templates). Repo-specific files (CLAUDE.md, settings) are not overwritten.",
|
|
24014
|
+
description: "Sync .claude infrastructure from CodeByPlan DB to local project. Uses aggregated defaults (latest version across all repos) for shared files (commands, agents, skills, rules, hooks, templates, stack docs). Repo-specific files (CLAUDE.md, settings) are not overwritten.",
|
|
23787
24015
|
inputSchema: {
|
|
23788
24016
|
repo_id: external_exports.string().uuid().describe("Repository ID to sync files for"),
|
|
23789
24017
|
project_path: external_exports.string().describe("Absolute path to the project root directory")
|
|
@@ -23839,31 +24067,6 @@ function registerWriteTools(server) {
|
|
|
23839
24067
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23840
24068
|
}
|
|
23841
24069
|
});
|
|
23842
|
-
server.registerTool("update_handoff", {
|
|
23843
|
-
description: "Update handoff state for a repo (status, summary, resume command/context). Automatically sets handoff_updated_at.",
|
|
23844
|
-
inputSchema: {
|
|
23845
|
-
repo_id: external_exports.string().uuid().describe("The repo UUID"),
|
|
23846
|
-
status: external_exports.string().optional().describe("Handoff status"),
|
|
23847
|
-
summary: external_exports.string().optional().describe("Handoff summary"),
|
|
23848
|
-
resume_command: external_exports.string().optional().describe("Resume command"),
|
|
23849
|
-
resume_context: external_exports.string().optional().describe("Resume context")
|
|
23850
|
-
}
|
|
23851
|
-
}, async ({ repo_id, status, summary, resume_command, resume_context }) => {
|
|
23852
|
-
const body = {};
|
|
23853
|
-
if (status !== void 0) body.status = status;
|
|
23854
|
-
if (summary !== void 0) body.summary = summary;
|
|
23855
|
-
if (resume_command !== void 0) body.resume_command = resume_command;
|
|
23856
|
-
if (resume_context !== void 0) body.resume_context = resume_context;
|
|
23857
|
-
if (Object.keys(body).length === 0) {
|
|
23858
|
-
return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
|
|
23859
|
-
}
|
|
23860
|
-
try {
|
|
23861
|
-
const res = await apiPatch(`/repos/${repo_id}/handoff`, body);
|
|
23862
|
-
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
23863
|
-
} catch (err) {
|
|
23864
|
-
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23865
|
-
}
|
|
23866
|
-
});
|
|
23867
24070
|
server.registerTool("create_worktree", {
|
|
23868
24071
|
description: "Create a new worktree for a repo.",
|
|
23869
24072
|
inputSchema: {
|
|
@@ -24048,6 +24251,11 @@ if (!process.env.CODEBYPLAN_API_KEY) {
|
|
|
24048
24251
|
} catch {
|
|
24049
24252
|
}
|
|
24050
24253
|
}
|
|
24254
|
+
if (process.env.CODEBYPLAN_API_KEY?.startsWith("CODEBYPLAN_API_KEY=")) {
|
|
24255
|
+
process.env.CODEBYPLAN_API_KEY = process.env.CODEBYPLAN_API_KEY.slice(
|
|
24256
|
+
"CODEBYPLAN_API_KEY=".length
|
|
24257
|
+
);
|
|
24258
|
+
}
|
|
24051
24259
|
var arg = process.argv[2];
|
|
24052
24260
|
if (arg === "--version" || arg === "-v") {
|
|
24053
24261
|
console.log(VERSION);
|