@docyrus/docyrus 0.0.42 → 0.0.44
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 +116 -83
- package/agent-loader.js +11 -2
- package/agent-loader.js.map +2 -2
- package/main.js +1238 -1189
- package/main.js.map +4 -4
- package/package.json +1 -1
- package/resources/pi-agent/extensions/architect.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/package.json +22 -22
- package/resources/pi-agent/extensions/plan.ts +183 -41
- package/resources/pi-agent/extensions/tasks.ts +41 -12
- package/resources/pi-agent/prompts/agent-system.md +13 -0
- package/resources/pi-agent/prompts/coder-system.md +19 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/SKILL.md +17 -13
- package/server-loader.js +653 -432
- package/server-loader.js.map +3 -3
package/server-loader.js
CHANGED
|
@@ -22018,6 +22018,23 @@ function normalizeDeploymentName(params) {
|
|
|
22018
22018
|
return params.defaultModel;
|
|
22019
22019
|
}
|
|
22020
22020
|
async function resolveKnowledgeProviderFromAgentRuntime(params) {
|
|
22021
|
+
if (params.docyrusApi) {
|
|
22022
|
+
const { apiBaseUrl, accessToken } = params.docyrusApi;
|
|
22023
|
+
const gatewayBase = apiBaseUrl.replace(/\/+$/u, "") + "/ai/gateway";
|
|
22024
|
+
return {
|
|
22025
|
+
provider: {
|
|
22026
|
+
providerId: "docyrus-api",
|
|
22027
|
+
apiBase: gatewayBase,
|
|
22028
|
+
model: DEFAULT_EMBEDDING_MODEL,
|
|
22029
|
+
dimensions: DEFAULT_DIMENSIONS,
|
|
22030
|
+
key: accessToken,
|
|
22031
|
+
headers: (key) => ({
|
|
22032
|
+
Authorization: `Bearer ${key}`,
|
|
22033
|
+
"Content-Type": "application/json"
|
|
22034
|
+
})
|
|
22035
|
+
}
|
|
22036
|
+
};
|
|
22037
|
+
}
|
|
22021
22038
|
const agentRootPath = resolveDocyrusPiAgentRootPath(params.settingsRootPath);
|
|
22022
22039
|
const authState = readJsonFile((0, import_node_path10.join)(agentRootPath, "auth.json")) || {};
|
|
22023
22040
|
const envStore = new AgentEnvStore((0, import_node_path10.join)(agentRootPath, "env.json"));
|
|
@@ -22560,9 +22577,9 @@ async function checkKnowledgeSections(knowledgeDir) {
|
|
|
22560
22577
|
}
|
|
22561
22578
|
|
|
22562
22579
|
// src/knowledge/init.ts
|
|
22563
|
-
var
|
|
22564
|
-
var
|
|
22565
|
-
var
|
|
22580
|
+
var import_node_fs7 = require("node:fs");
|
|
22581
|
+
var import_promises9 = require("node:fs/promises");
|
|
22582
|
+
var import_node_path13 = require("node:path");
|
|
22566
22583
|
init_graph();
|
|
22567
22584
|
|
|
22568
22585
|
// src/knowledge/bootstrap.ts
|
|
@@ -22974,29 +22991,265 @@ async function generateInitialKnowledge(params) {
|
|
|
22974
22991
|
};
|
|
22975
22992
|
}
|
|
22976
22993
|
|
|
22994
|
+
// src/knowledge/init.ts
|
|
22995
|
+
var MARKER_BEGIN = "%% docyrus-knowledge:begin %%";
|
|
22996
|
+
var MARKER_END = "%% docyrus-knowledge:end %%";
|
|
22997
|
+
var HOOK_MARKER_BEGIN = "# docyrus-knowledge:begin";
|
|
22998
|
+
var HOOK_MARKER_END = "# docyrus-knowledge:end";
|
|
22999
|
+
function wrapManagedBlock(content3) {
|
|
23000
|
+
return `${MARKER_BEGIN}
|
|
23001
|
+
${content3}${content3.endsWith("\n") ? "" : "\n"}${MARKER_END}
|
|
23002
|
+
`;
|
|
23003
|
+
}
|
|
23004
|
+
async function ensureDirectory(pathValue) {
|
|
23005
|
+
await (0, import_promises9.mkdir)(pathValue, {
|
|
23006
|
+
recursive: true,
|
|
23007
|
+
mode: 493
|
|
23008
|
+
});
|
|
23009
|
+
}
|
|
23010
|
+
async function writeIfMissing(filePath, content3) {
|
|
23011
|
+
if ((0, import_node_fs7.existsSync)(filePath)) {
|
|
23012
|
+
return "kept";
|
|
23013
|
+
}
|
|
23014
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23015
|
+
await (0, import_promises9.writeFile)(filePath, content3, "utf8");
|
|
23016
|
+
return "created";
|
|
23017
|
+
}
|
|
23018
|
+
async function writeOrUpdate(filePath, content3) {
|
|
23019
|
+
if (!(0, import_node_fs7.existsSync)(filePath)) {
|
|
23020
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23021
|
+
await (0, import_promises9.writeFile)(filePath, content3, "utf8");
|
|
23022
|
+
return "created";
|
|
23023
|
+
}
|
|
23024
|
+
const current = await (0, import_promises9.readFile)(filePath, "utf8");
|
|
23025
|
+
if (current === content3) {
|
|
23026
|
+
return "kept";
|
|
23027
|
+
}
|
|
23028
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23029
|
+
await (0, import_promises9.writeFile)(filePath, content3, "utf8");
|
|
23030
|
+
return "updated";
|
|
23031
|
+
}
|
|
23032
|
+
async function upsertManagedBlock(filePath, content3) {
|
|
23033
|
+
const wrapped = wrapManagedBlock(content3);
|
|
23034
|
+
if (!(0, import_node_fs7.existsSync)(filePath)) {
|
|
23035
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23036
|
+
await (0, import_promises9.writeFile)(filePath, wrapped, "utf8");
|
|
23037
|
+
return "created";
|
|
23038
|
+
}
|
|
23039
|
+
const current = await (0, import_promises9.readFile)(filePath, "utf8");
|
|
23040
|
+
const beginIndex = current.indexOf(MARKER_BEGIN);
|
|
23041
|
+
const endIndex = current.indexOf(MARKER_END);
|
|
23042
|
+
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
23043
|
+
const replaceEnd = current[endIndex + MARKER_END.length] === "\n" ? endIndex + MARKER_END.length + 1 : endIndex + MARKER_END.length;
|
|
23044
|
+
const updated = `${current.slice(0, beginIndex)}${wrapped}${current.slice(replaceEnd)}`;
|
|
23045
|
+
await (0, import_promises9.writeFile)(filePath, updated, "utf8");
|
|
23046
|
+
return "updated";
|
|
23047
|
+
}
|
|
23048
|
+
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
23049
|
+
await (0, import_promises9.writeFile)(filePath, `${current}${separator}${wrapped}`, "utf8");
|
|
23050
|
+
return "appended";
|
|
23051
|
+
}
|
|
23052
|
+
function normalizeObject(value2) {
|
|
23053
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) ? { ...value2 } : {};
|
|
23054
|
+
}
|
|
23055
|
+
async function updateJsonFile(filePath, updater) {
|
|
23056
|
+
const current = (0, import_node_fs7.existsSync)(filePath) ? normalizeObject(JSON.parse((0, import_node_fs7.readFileSync)(filePath, "utf8"))) : {};
|
|
23057
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23058
|
+
await (0, import_promises9.writeFile)(filePath, `${JSON.stringify(updater(current), null, 2)}
|
|
23059
|
+
`, "utf8");
|
|
23060
|
+
}
|
|
23061
|
+
function wrapHookBlock(content3) {
|
|
23062
|
+
return `${HOOK_MARKER_BEGIN}
|
|
23063
|
+
${content3}${content3.endsWith("\n") ? "" : "\n"}${HOOK_MARKER_END}
|
|
23064
|
+
`;
|
|
23065
|
+
}
|
|
23066
|
+
async function upsertShellHook(filePath, content3, shellHeader) {
|
|
23067
|
+
const wrapped = wrapHookBlock(content3);
|
|
23068
|
+
if (!(0, import_node_fs7.existsSync)(filePath)) {
|
|
23069
|
+
await ensureDirectory((0, import_node_path13.dirname)(filePath));
|
|
23070
|
+
const initial = `${shellHeader || ""}${wrapped}`;
|
|
23071
|
+
await (0, import_promises9.writeFile)(filePath, initial, {
|
|
23072
|
+
encoding: "utf8",
|
|
23073
|
+
mode: 493
|
|
23074
|
+
});
|
|
23075
|
+
await (0, import_promises9.chmod)(filePath, 493);
|
|
23076
|
+
return "created";
|
|
23077
|
+
}
|
|
23078
|
+
const current = await (0, import_promises9.readFile)(filePath, "utf8");
|
|
23079
|
+
const beginIndex = current.indexOf(HOOK_MARKER_BEGIN);
|
|
23080
|
+
const endIndex = current.indexOf(HOOK_MARKER_END);
|
|
23081
|
+
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
23082
|
+
const replaceEnd = current[endIndex + HOOK_MARKER_END.length] === "\n" ? endIndex + HOOK_MARKER_END.length + 1 : endIndex + HOOK_MARKER_END.length;
|
|
23083
|
+
const nextContent = `${current.slice(0, beginIndex)}${wrapped}${current.slice(replaceEnd)}`;
|
|
23084
|
+
if (nextContent === current) {
|
|
23085
|
+
return "kept";
|
|
23086
|
+
}
|
|
23087
|
+
await (0, import_promises9.writeFile)(filePath, nextContent, "utf8");
|
|
23088
|
+
await (0, import_promises9.chmod)(filePath, 493);
|
|
23089
|
+
return "updated";
|
|
23090
|
+
}
|
|
23091
|
+
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
23092
|
+
await (0, import_promises9.writeFile)(filePath, `${current}${separator}${wrapped}`, "utf8");
|
|
23093
|
+
await (0, import_promises9.chmod)(filePath, 493);
|
|
23094
|
+
return "appended";
|
|
23095
|
+
}
|
|
23096
|
+
function buildKnowledgeDocumentTemplate() {
|
|
23097
|
+
return [
|
|
23098
|
+
"# Knowledge",
|
|
23099
|
+
"",
|
|
23100
|
+
"This directory captures the architecture, constraints, workflows, and test expectations that coding agents should read before changing the repo.",
|
|
23101
|
+
"",
|
|
23102
|
+
"## Working Agreement",
|
|
23103
|
+
"",
|
|
23104
|
+
"Search this knowledge graph before coding, keep it updated when behavior changes, and use \\[\\[wiki links\\]\\] plus `@docyrus` backlinks to connect decisions to implementation.",
|
|
23105
|
+
"",
|
|
23106
|
+
"## Backlinks",
|
|
23107
|
+
"",
|
|
23108
|
+
"Use `// @docyrus: \\[\\[knowledge#Section Name\\]\\]` or `# @docyrus: \\[\\[knowledge#Section Name\\]\\]` near the code or tests that implement a documented behavior.",
|
|
23109
|
+
""
|
|
23110
|
+
].join("\n");
|
|
23111
|
+
}
|
|
23112
|
+
function buildAgentsTemplate(commandPrefix) {
|
|
23113
|
+
return [
|
|
23114
|
+
"# Knowledge Graph Workflow",
|
|
23115
|
+
"",
|
|
23116
|
+
`- If \`docyrus/knowledge/\` exists, run \`${commandPrefix} knowledge search\` before coding so you start from documented intent instead of rediscovering it from source files.`,
|
|
23117
|
+
`- Use \`${commandPrefix} knowledge expand\` when prompts contain \`[[refs]]\` so section names resolve to real file locations and summaries.`,
|
|
23118
|
+
"- Keep `docyrus/knowledge/` in sync whenever you change functionality, architecture, tests, or behavior.",
|
|
23119
|
+
`- Before finishing, run \`${commandPrefix} knowledge check\` and fix any broken links, stale references, or missing \`@docyrus\` backlinks.`,
|
|
23120
|
+
""
|
|
23121
|
+
].join("\n");
|
|
23122
|
+
}
|
|
23123
|
+
function buildCursorRulesTemplate(commandPrefix) {
|
|
23124
|
+
return [
|
|
23125
|
+
"# Knowledge Graph Workflow",
|
|
23126
|
+
"",
|
|
23127
|
+
`- Use \`${commandPrefix} knowledge search\` to find relevant documentation before reading or editing code.`,
|
|
23128
|
+
`- Use \`${commandPrefix} knowledge section\` to read the full section once you have a relevant section id.`,
|
|
23129
|
+
`- Use \`${commandPrefix} knowledge expand\` whenever the prompt includes \`[[refs]]\`.`,
|
|
23130
|
+
`- Keep \`docyrus/knowledge/\` updated and finish with \`${commandPrefix} knowledge check\`.`,
|
|
23131
|
+
""
|
|
23132
|
+
].join("\n");
|
|
23133
|
+
}
|
|
23134
|
+
function buildClaudeHookCommand(commandPrefix, event) {
|
|
23135
|
+
return `${commandPrefix} knowledge hook claude ${event}`;
|
|
23136
|
+
}
|
|
23137
|
+
function syncClaudeHooks(settings, commandPrefix) {
|
|
23138
|
+
const hooks = normalizeObject(settings.hooks);
|
|
23139
|
+
const nextHooks = { ...hooks };
|
|
23140
|
+
for (const [eventName, entries] of Object.entries(nextHooks)) {
|
|
23141
|
+
if (!Array.isArray(entries)) {
|
|
23142
|
+
continue;
|
|
23143
|
+
}
|
|
23144
|
+
nextHooks[eventName] = entries.filter((entry) => {
|
|
23145
|
+
const hooksList = Array.isArray(entry.hooks) ? entry.hooks || [] : [];
|
|
23146
|
+
return !hooksList.some((hook) => typeof hook.command === "string" && hook.command.includes("knowledge hook claude"));
|
|
23147
|
+
});
|
|
23148
|
+
}
|
|
23149
|
+
const addHook = (eventName) => {
|
|
23150
|
+
const current = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : [];
|
|
23151
|
+
current.push({
|
|
23152
|
+
hooks: [
|
|
23153
|
+
{
|
|
23154
|
+
type: "command",
|
|
23155
|
+
command: buildClaudeHookCommand(commandPrefix, eventName)
|
|
23156
|
+
}
|
|
23157
|
+
]
|
|
23158
|
+
});
|
|
23159
|
+
nextHooks[eventName] = current;
|
|
23160
|
+
};
|
|
23161
|
+
addHook("UserPromptSubmit");
|
|
23162
|
+
addHook("Stop");
|
|
23163
|
+
return {
|
|
23164
|
+
...settings,
|
|
23165
|
+
hooks: nextHooks
|
|
23166
|
+
};
|
|
23167
|
+
}
|
|
23168
|
+
function syncCursorHooks(settings, commandPrefix) {
|
|
23169
|
+
const hooks = normalizeObject(settings.hooks);
|
|
23170
|
+
const stopEntries = Array.isArray(hooks.stop) ? hooks.stop : [];
|
|
23171
|
+
const filtered = stopEntries.filter((entry) => typeof entry.command !== "string" || !entry.command.includes("knowledge hook cursor"));
|
|
23172
|
+
return {
|
|
23173
|
+
...settings.version ? settings : { version: 1 },
|
|
23174
|
+
...settings,
|
|
23175
|
+
hooks: {
|
|
23176
|
+
...hooks,
|
|
23177
|
+
stop: [
|
|
23178
|
+
...filtered,
|
|
23179
|
+
{
|
|
23180
|
+
command: `${commandPrefix} knowledge hook cursor stop`
|
|
23181
|
+
}
|
|
23182
|
+
]
|
|
23183
|
+
}
|
|
23184
|
+
};
|
|
23185
|
+
}
|
|
23186
|
+
async function initializeKnowledgeRepo(params) {
|
|
23187
|
+
const root = (0, import_node_path13.resolve)(params.root || process.cwd());
|
|
23188
|
+
const knowledgeDir = getKnowledgeDirectory(root);
|
|
23189
|
+
const knowledgeFile = (0, import_node_path13.join)(knowledgeDir, "knowledge.md");
|
|
23190
|
+
await ensureDirectory(knowledgeDir);
|
|
23191
|
+
const knowledgeFileStatus = await writeIfMissing(knowledgeFile, `${buildKnowledgeDocumentTemplate()}
|
|
23192
|
+
`);
|
|
23193
|
+
const agentsStatus = await upsertManagedBlock((0, import_node_path13.join)(root, "AGENTS.md"), buildAgentsTemplate(params.commandPrefix));
|
|
23194
|
+
const claudeStatus = await upsertManagedBlock((0, import_node_path13.join)(root, "CLAUDE.md"), buildAgentsTemplate(params.commandPrefix));
|
|
23195
|
+
const cursorRulesStatus = await writeOrUpdate((0, import_node_path13.join)(root, ".cursor", "rules", "docyrus-knowledge.md"), `${buildCursorRulesTemplate(params.commandPrefix)}
|
|
23196
|
+
`);
|
|
23197
|
+
const bootstrap = (await generateInitialKnowledge({
|
|
23198
|
+
root,
|
|
23199
|
+
brief: params.brief
|
|
23200
|
+
})).files;
|
|
23201
|
+
await updateJsonFile((0, import_node_path13.join)(root, ".claude", "settings.json"), (value2) => syncClaudeHooks(value2, params.commandPrefix));
|
|
23202
|
+
await updateJsonFile((0, import_node_path13.join)(root, ".cursor", "hooks.json"), (value2) => syncCursorHooks(value2, params.commandPrefix));
|
|
23203
|
+
let huskyHookStatus;
|
|
23204
|
+
const huskyDir = (0, import_node_path13.join)(root, ".husky");
|
|
23205
|
+
if ((0, import_node_fs7.existsSync)(huskyDir)) {
|
|
23206
|
+
const huskyHookPath = (0, import_node_path13.join)(huskyDir, "pre-commit");
|
|
23207
|
+
const huskyHeader = (0, import_node_fs7.existsSync)((0, import_node_path13.join)(huskyDir, "_", "h")) ? '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/h"\n\n' : void 0;
|
|
23208
|
+
huskyHookStatus = await upsertShellHook(huskyHookPath, `${params.commandPrefix} knowledge pre-commit
|
|
23209
|
+
`, huskyHeader);
|
|
23210
|
+
}
|
|
23211
|
+
let gitHookStatus;
|
|
23212
|
+
if (!huskyHookStatus && params.installGitHook) {
|
|
23213
|
+
const gitHookPath = (0, import_node_path13.join)(root, ".git", "hooks", "pre-commit");
|
|
23214
|
+
gitHookStatus = await upsertShellHook(gitHookPath, `${params.commandPrefix} knowledge pre-commit
|
|
23215
|
+
`, "#!/usr/bin/env sh\n\n");
|
|
23216
|
+
}
|
|
23217
|
+
return {
|
|
23218
|
+
root,
|
|
23219
|
+
knowledgeDir,
|
|
23220
|
+
knowledgeFile,
|
|
23221
|
+
agentsStatus,
|
|
23222
|
+
claudeStatus,
|
|
23223
|
+
cursorRulesStatus,
|
|
23224
|
+
knowledgeFileStatus,
|
|
23225
|
+
bootstrap,
|
|
23226
|
+
huskyHookStatus,
|
|
23227
|
+
gitHookStatus
|
|
23228
|
+
};
|
|
23229
|
+
}
|
|
23230
|
+
|
|
22977
23231
|
// src/project-plan/graph.ts
|
|
22978
23232
|
var import_node_crypto3 = require("node:crypto");
|
|
22979
|
-
var
|
|
22980
|
-
var
|
|
22981
|
-
var
|
|
22982
|
-
init_graph();
|
|
23233
|
+
var import_node_fs9 = require("node:fs");
|
|
23234
|
+
var import_promises11 = __toESM(require("node:fs/promises"));
|
|
23235
|
+
var import_node_path15 = __toESM(require("node:path"));
|
|
22983
23236
|
|
|
22984
23237
|
// src/project-plan/localTodos.ts
|
|
22985
23238
|
var import_node_crypto2 = __toESM(require("node:crypto"));
|
|
22986
|
-
var
|
|
22987
|
-
var
|
|
22988
|
-
var
|
|
23239
|
+
var import_node_fs8 = require("node:fs");
|
|
23240
|
+
var import_promises10 = __toESM(require("node:fs/promises"));
|
|
23241
|
+
var import_node_path14 = __toESM(require("node:path"));
|
|
22989
23242
|
var TODO_DIR_NAME = ".pi/todos";
|
|
22990
23243
|
var TODO_PATH_ENV = "PI_TODO_PATH";
|
|
22991
23244
|
function resolveTodosDirectory(root) {
|
|
22992
23245
|
const overridePath = process.env[TODO_PATH_ENV]?.trim();
|
|
22993
23246
|
if (overridePath) {
|
|
22994
|
-
return
|
|
23247
|
+
return import_node_path14.default.resolve(root, overridePath);
|
|
22995
23248
|
}
|
|
22996
|
-
return
|
|
23249
|
+
return import_node_path14.default.resolve(root, TODO_DIR_NAME);
|
|
22997
23250
|
}
|
|
22998
23251
|
function getTodoFilePath(todosDir, id) {
|
|
22999
|
-
return
|
|
23252
|
+
return import_node_path14.default.join(todosDir, `${id}.md`);
|
|
23000
23253
|
}
|
|
23001
23254
|
function splitFrontMatter(content3) {
|
|
23002
23255
|
if (!content3.startsWith("{")) {
|
|
@@ -23102,7 +23355,7 @@ ${trimmedBody}
|
|
|
23102
23355
|
async function generateTodoId(todosDir) {
|
|
23103
23356
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
23104
23357
|
const id = import_node_crypto2.default.randomBytes(4).toString("hex");
|
|
23105
|
-
if (!(0,
|
|
23358
|
+
if (!(0, import_node_fs8.existsSync)(getTodoFilePath(todosDir, id))) {
|
|
23106
23359
|
return id;
|
|
23107
23360
|
}
|
|
23108
23361
|
}
|
|
@@ -23113,7 +23366,7 @@ async function listLinkedTodosByTask(root) {
|
|
|
23113
23366
|
const result = /* @__PURE__ */ new Map();
|
|
23114
23367
|
let entries = [];
|
|
23115
23368
|
try {
|
|
23116
|
-
entries = await
|
|
23369
|
+
entries = await import_promises10.default.readdir(todosDir);
|
|
23117
23370
|
} catch {
|
|
23118
23371
|
return result;
|
|
23119
23372
|
}
|
|
@@ -23124,7 +23377,7 @@ async function listLinkedTodosByTask(root) {
|
|
|
23124
23377
|
const id = entry.slice(0, -3);
|
|
23125
23378
|
const filePath = getTodoFilePath(todosDir, id);
|
|
23126
23379
|
try {
|
|
23127
|
-
const raw2 = await
|
|
23380
|
+
const raw2 = await import_promises10.default.readFile(filePath, "utf8");
|
|
23128
23381
|
const { frontMatter } = splitFrontMatter(raw2);
|
|
23129
23382
|
const parsed = parseLocalTodoFrontMatter(frontMatter, id);
|
|
23130
23383
|
if (!parsed.parent_task_id) {
|
|
@@ -23135,7 +23388,7 @@ async function listLinkedTodosByTask(root) {
|
|
|
23135
23388
|
id,
|
|
23136
23389
|
title: parsed.title,
|
|
23137
23390
|
status: parsed.status,
|
|
23138
|
-
filePath:
|
|
23391
|
+
filePath: import_node_path14.default.relative(root, filePath),
|
|
23139
23392
|
parentTaskId: parsed.parent_task_id
|
|
23140
23393
|
});
|
|
23141
23394
|
result.set(parsed.parent_task_id, existing);
|
|
@@ -23153,7 +23406,7 @@ async function listAllLinkedTodos(root) {
|
|
|
23153
23406
|
}
|
|
23154
23407
|
async function createLinkedTodo(params) {
|
|
23155
23408
|
const todosDir = resolveTodosDirectory(params.root);
|
|
23156
|
-
await
|
|
23409
|
+
await import_promises10.default.mkdir(todosDir, {
|
|
23157
23410
|
recursive: true
|
|
23158
23411
|
});
|
|
23159
23412
|
const id = await generateTodoId(todosDir);
|
|
@@ -23167,12 +23420,12 @@ async function createLinkedTodo(params) {
|
|
|
23167
23420
|
parent_task_id: params.parentTaskId,
|
|
23168
23421
|
body: params.body ?? ""
|
|
23169
23422
|
};
|
|
23170
|
-
await
|
|
23423
|
+
await import_promises10.default.writeFile(filePath, serializeLocalTodo(todo), "utf8");
|
|
23171
23424
|
return {
|
|
23172
23425
|
id,
|
|
23173
23426
|
title: todo.title,
|
|
23174
23427
|
status: todo.status,
|
|
23175
|
-
filePath:
|
|
23428
|
+
filePath: import_node_path14.default.relative(params.root, filePath),
|
|
23176
23429
|
parentTaskId: params.parentTaskId
|
|
23177
23430
|
};
|
|
23178
23431
|
}
|
|
@@ -23200,6 +23453,9 @@ var PROJECT_TASK_STATUSES = [
|
|
|
23200
23453
|
var PROJECT_PLAN_DIR_SEGMENTS = ["docyrus", "project-plan"];
|
|
23201
23454
|
var PROJECT_PLAN_JSON_FILE_NAME = "project-plan.json";
|
|
23202
23455
|
var PROJECT_PLAN_MARKDOWN_FILE_NAME = "PROJECT_PLAN.md";
|
|
23456
|
+
var KNOWLEDGE_FEATURES_PATH_SEGMENTS = ["docyrus", "knowledge", "features", "features.md"];
|
|
23457
|
+
var FEATURES_MARKER_BEGIN = "<!-- docyrus-project-plan:features:begin -->";
|
|
23458
|
+
var FEATURES_MARKER_END = "<!-- docyrus-project-plan:features:end -->";
|
|
23203
23459
|
function isRecord3(value2) {
|
|
23204
23460
|
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
23205
23461
|
}
|
|
@@ -23219,6 +23475,9 @@ function slugify(value2) {
|
|
|
23219
23475
|
function hashParts(parts2) {
|
|
23220
23476
|
return (0, import_node_crypto3.createHash)("sha1").update(parts2.join("::")).digest("hex").slice(0, 12);
|
|
23221
23477
|
}
|
|
23478
|
+
function normalizeSectionId(slug) {
|
|
23479
|
+
return `section-${hashParts([slug])}`;
|
|
23480
|
+
}
|
|
23222
23481
|
function normalizeFeatureId(sectionId, slug) {
|
|
23223
23482
|
return `feature-${hashParts([sectionId, slug])}`;
|
|
23224
23483
|
}
|
|
@@ -23231,7 +23490,7 @@ function compareStrings(left, right) {
|
|
|
23231
23490
|
function sortGraph(graph) {
|
|
23232
23491
|
return {
|
|
23233
23492
|
version: 1,
|
|
23234
|
-
sections: [...graph.sections].sort((left, right) => compareStrings(left.
|
|
23493
|
+
sections: [...graph.sections].sort((left, right) => compareStrings(left.id, right.id)),
|
|
23235
23494
|
features: [...graph.features].sort((left, right) => {
|
|
23236
23495
|
return compareStrings(left.sectionId, right.sectionId) || compareStrings(left.title, right.title) || compareStrings(left.id, right.id);
|
|
23237
23496
|
}),
|
|
@@ -23241,13 +23500,13 @@ function sortGraph(graph) {
|
|
|
23241
23500
|
};
|
|
23242
23501
|
}
|
|
23243
23502
|
function resolveProjectPlanDirectory(root) {
|
|
23244
|
-
return
|
|
23503
|
+
return import_node_path15.default.join(root, ...PROJECT_PLAN_DIR_SEGMENTS);
|
|
23245
23504
|
}
|
|
23246
23505
|
function resolveProjectPlanGraphPath(root) {
|
|
23247
|
-
return
|
|
23506
|
+
return import_node_path15.default.join(resolveProjectPlanDirectory(root), PROJECT_PLAN_JSON_FILE_NAME);
|
|
23248
23507
|
}
|
|
23249
23508
|
function resolveProjectPlanMarkdownPath(root) {
|
|
23250
|
-
return
|
|
23509
|
+
return import_node_path15.default.join(resolveProjectPlanDirectory(root), PROJECT_PLAN_MARKDOWN_FILE_NAME);
|
|
23251
23510
|
}
|
|
23252
23511
|
function createEmptyProjectPlanGraph() {
|
|
23253
23512
|
return {
|
|
@@ -23257,20 +23516,16 @@ function createEmptyProjectPlanGraph() {
|
|
|
23257
23516
|
tasks: []
|
|
23258
23517
|
};
|
|
23259
23518
|
}
|
|
23260
|
-
async function readKnowledgeSections(root) {
|
|
23261
|
-
const knowledgeDir = import_node_path14.default.join(root, "docyrus", "knowledge");
|
|
23262
|
-
if (!(0, import_node_fs8.existsSync)(knowledgeDir)) {
|
|
23263
|
-
return [];
|
|
23264
|
-
}
|
|
23265
|
-
return flattenSections(await loadAllSections(knowledgeDir));
|
|
23266
|
-
}
|
|
23267
23519
|
function parseProjectPlanGraph(rawValue) {
|
|
23268
23520
|
if (!isRecord3(rawValue)) {
|
|
23269
23521
|
return createEmptyProjectPlanGraph();
|
|
23270
23522
|
}
|
|
23271
23523
|
const sections = Array.isArray(rawValue.sections) ? rawValue.sections.filter((value2) => isRecord3(value2)).map((value2) => ({
|
|
23272
|
-
|
|
23273
|
-
|
|
23524
|
+
id: isNonEmptyString2(value2.id) ? value2.id.trim() : "",
|
|
23525
|
+
title: isNonEmptyString2(value2.title) ? value2.title.trim() : "",
|
|
23526
|
+
slug: isNonEmptyString2(value2.slug) ? value2.slug.trim() : "",
|
|
23527
|
+
summary: typeof value2.summary === "string" ? value2.summary.trim() : ""
|
|
23528
|
+
})).filter((value2) => value2.id.length > 0) : [];
|
|
23274
23529
|
const features = Array.isArray(rawValue.features) ? rawValue.features.filter((value2) => isRecord3(value2)).map((value2) => ({
|
|
23275
23530
|
id: isNonEmptyString2(value2.id) ? value2.id.trim() : "",
|
|
23276
23531
|
title: isNonEmptyString2(value2.title) ? value2.title.trim() : "",
|
|
@@ -23298,10 +23553,10 @@ function parseProjectPlanGraph(rawValue) {
|
|
|
23298
23553
|
}
|
|
23299
23554
|
async function readProjectPlanGraph(root) {
|
|
23300
23555
|
const graphPath = resolveProjectPlanGraphPath(root);
|
|
23301
|
-
if (!(0,
|
|
23556
|
+
if (!(0, import_node_fs9.existsSync)(graphPath)) {
|
|
23302
23557
|
return createEmptyProjectPlanGraph();
|
|
23303
23558
|
}
|
|
23304
|
-
const raw2 = await
|
|
23559
|
+
const raw2 = await import_promises11.default.readFile(graphPath, "utf8");
|
|
23305
23560
|
return parseProjectPlanGraph(JSON.parse(raw2));
|
|
23306
23561
|
}
|
|
23307
23562
|
function deriveFeatureStatus(tasks) {
|
|
@@ -23334,21 +23589,7 @@ function deriveSectionStatus(features) {
|
|
|
23334
23589
|
}
|
|
23335
23590
|
return "planned";
|
|
23336
23591
|
}
|
|
23337
|
-
function findKnowledgeSectionTitle(knowledgeSections, sectionId) {
|
|
23338
|
-
const section = knowledgeSections.find((item) => item.id === sectionId);
|
|
23339
|
-
if (!section) {
|
|
23340
|
-
return {
|
|
23341
|
-
heading: sectionId,
|
|
23342
|
-
filePath: ""
|
|
23343
|
-
};
|
|
23344
|
-
}
|
|
23345
|
-
return {
|
|
23346
|
-
heading: section.heading,
|
|
23347
|
-
filePath: section.filePath
|
|
23348
|
-
};
|
|
23349
|
-
}
|
|
23350
23592
|
async function buildProjectPlanHierarchy(root, graph) {
|
|
23351
|
-
const knowledgeSections = await readKnowledgeSections(root);
|
|
23352
23593
|
const currentGraph = graph ?? await readProjectPlanGraph(root);
|
|
23353
23594
|
const linkedTodos = await listLinkedTodosByTask(root);
|
|
23354
23595
|
const featureMap = /* @__PURE__ */ new Map();
|
|
@@ -23369,13 +23610,13 @@ async function buildProjectPlanHierarchy(root, graph) {
|
|
|
23369
23610
|
});
|
|
23370
23611
|
}
|
|
23371
23612
|
const sections = currentGraph.sections.map((section) => {
|
|
23372
|
-
const features = currentGraph.features.filter((feature) => feature.sectionId === section.
|
|
23373
|
-
const knowledgeMetadata = findKnowledgeSectionTitle(knowledgeSections, section.sectionId);
|
|
23613
|
+
const features = currentGraph.features.filter((feature) => feature.sectionId === section.id).map((feature) => featureMap.get(feature.id)).filter((feature) => Boolean(feature));
|
|
23374
23614
|
const taskCount = features.reduce((count, feature) => count + feature.taskCount, 0);
|
|
23375
23615
|
return {
|
|
23376
|
-
|
|
23377
|
-
|
|
23378
|
-
|
|
23616
|
+
id: section.id,
|
|
23617
|
+
title: section.title,
|
|
23618
|
+
slug: section.slug,
|
|
23619
|
+
summary: section.summary,
|
|
23379
23620
|
status: deriveSectionStatus(features),
|
|
23380
23621
|
featureCount: features.length,
|
|
23381
23622
|
taskCount,
|
|
@@ -23407,9 +23648,9 @@ async function renderProjectPlanMarkdown(root, graph) {
|
|
|
23407
23648
|
return `${lines.join("\n")}`;
|
|
23408
23649
|
}
|
|
23409
23650
|
for (const section of populatedSections) {
|
|
23410
|
-
lines.push(`## ${section.
|
|
23651
|
+
lines.push(`## ${section.title}`);
|
|
23411
23652
|
lines.push("");
|
|
23412
|
-
lines.push(`- Section ID: \`${section.
|
|
23653
|
+
lines.push(`- Section ID: \`${section.id}\``);
|
|
23413
23654
|
lines.push(`- Status: \`${section.status}\``);
|
|
23414
23655
|
lines.push(`- Features: ${section.featureCount}`);
|
|
23415
23656
|
lines.push(`- Tasks: ${section.taskCount}`);
|
|
@@ -23443,16 +23684,84 @@ async function renderProjectPlanMarkdown(root, graph) {
|
|
|
23443
23684
|
return `${lines.join("\n").trimEnd()}
|
|
23444
23685
|
`;
|
|
23445
23686
|
}
|
|
23687
|
+
function buildKnowledgeFeaturesSection(graph) {
|
|
23688
|
+
const sectionMap = new Map(graph.sections.map((section) => [section.id, section]));
|
|
23689
|
+
const featuresBySection = /* @__PURE__ */ new Map();
|
|
23690
|
+
for (const feature of graph.features) {
|
|
23691
|
+
const list2 = featuresBySection.get(feature.sectionId) || [];
|
|
23692
|
+
list2.push(feature);
|
|
23693
|
+
featuresBySection.set(feature.sectionId, list2);
|
|
23694
|
+
}
|
|
23695
|
+
const lines = [
|
|
23696
|
+
"## Planned Features",
|
|
23697
|
+
"",
|
|
23698
|
+
"Features tracked in the project plan.",
|
|
23699
|
+
""
|
|
23700
|
+
];
|
|
23701
|
+
if (graph.features.length === 0) {
|
|
23702
|
+
lines.push("No planned features yet.");
|
|
23703
|
+
lines.push("");
|
|
23704
|
+
return lines.join("\n");
|
|
23705
|
+
}
|
|
23706
|
+
for (const section of graph.sections) {
|
|
23707
|
+
const features = featuresBySection.get(section.id);
|
|
23708
|
+
if (!features || features.length === 0) {
|
|
23709
|
+
continue;
|
|
23710
|
+
}
|
|
23711
|
+
lines.push(`### ${section.title}`);
|
|
23712
|
+
lines.push("");
|
|
23713
|
+
for (const feature of features) {
|
|
23714
|
+
const summary = feature.summary ? ` \u2014 ${feature.summary}` : "";
|
|
23715
|
+
lines.push(`- **${feature.title}**${summary}`);
|
|
23716
|
+
}
|
|
23717
|
+
lines.push("");
|
|
23718
|
+
}
|
|
23719
|
+
const ungrouped = graph.features.filter((feature) => !sectionMap.has(feature.sectionId));
|
|
23720
|
+
if (ungrouped.length > 0) {
|
|
23721
|
+
for (const feature of ungrouped) {
|
|
23722
|
+
const summary = feature.summary ? ` \u2014 ${feature.summary}` : "";
|
|
23723
|
+
lines.push(`- **${feature.title}**${summary}`);
|
|
23724
|
+
}
|
|
23725
|
+
lines.push("");
|
|
23726
|
+
}
|
|
23727
|
+
return lines.join("\n");
|
|
23728
|
+
}
|
|
23729
|
+
async function syncFeaturesToKnowledge(root, graph) {
|
|
23730
|
+
const featuresPath = import_node_path15.default.join(root, ...KNOWLEDGE_FEATURES_PATH_SEGMENTS);
|
|
23731
|
+
if (!(0, import_node_fs9.existsSync)(featuresPath)) {
|
|
23732
|
+
return;
|
|
23733
|
+
}
|
|
23734
|
+
const content3 = buildKnowledgeFeaturesSection(graph);
|
|
23735
|
+
const wrapped = `${FEATURES_MARKER_BEGIN}
|
|
23736
|
+
${content3.trimEnd()}
|
|
23737
|
+
|
|
23738
|
+
${FEATURES_MARKER_END}
|
|
23739
|
+
`;
|
|
23740
|
+
const current = await import_promises11.default.readFile(featuresPath, "utf8");
|
|
23741
|
+
const beginIndex = current.indexOf(FEATURES_MARKER_BEGIN);
|
|
23742
|
+
const endIndex = current.indexOf(FEATURES_MARKER_END);
|
|
23743
|
+
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
23744
|
+
const replaceEnd = current[endIndex + FEATURES_MARKER_END.length] === "\n" ? endIndex + FEATURES_MARKER_END.length + 1 : endIndex + FEATURES_MARKER_END.length;
|
|
23745
|
+
const next = `${current.slice(0, beginIndex)}${wrapped}${current.slice(replaceEnd)}`;
|
|
23746
|
+
if (next !== current) {
|
|
23747
|
+
await import_promises11.default.writeFile(featuresPath, next, "utf8");
|
|
23748
|
+
}
|
|
23749
|
+
return;
|
|
23750
|
+
}
|
|
23751
|
+
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
23752
|
+
await import_promises11.default.writeFile(featuresPath, `${current}${separator}${wrapped}`, "utf8");
|
|
23753
|
+
}
|
|
23446
23754
|
async function writeProjectPlanFiles(root, graph) {
|
|
23447
23755
|
const graphPath = resolveProjectPlanGraphPath(root);
|
|
23448
23756
|
const markdownPath = resolveProjectPlanMarkdownPath(root);
|
|
23449
|
-
await
|
|
23757
|
+
await import_promises11.default.mkdir(import_node_path15.default.dirname(graphPath), {
|
|
23450
23758
|
recursive: true
|
|
23451
23759
|
});
|
|
23452
23760
|
const sortedGraph = sortGraph(graph);
|
|
23453
|
-
await
|
|
23761
|
+
await import_promises11.default.writeFile(graphPath, `${JSON.stringify(sortedGraph, null, 2)}
|
|
23454
23762
|
`, "utf8");
|
|
23455
|
-
await
|
|
23763
|
+
await import_promises11.default.writeFile(markdownPath, await renderProjectPlanMarkdown(root, sortedGraph), "utf8");
|
|
23764
|
+
await syncFeaturesToKnowledge(root, sortedGraph);
|
|
23456
23765
|
return {
|
|
23457
23766
|
graph: sortedGraph,
|
|
23458
23767
|
graphPath,
|
|
@@ -23469,27 +23778,24 @@ function buildProjectPlanCheckError(code, message, target) {
|
|
|
23469
23778
|
async function validateProjectPlanGraph(root, graph) {
|
|
23470
23779
|
const currentGraph = graph ?? await readProjectPlanGraph(root);
|
|
23471
23780
|
const errors = [];
|
|
23472
|
-
const knowledgeSections = await readKnowledgeSections(root);
|
|
23473
|
-
const knowledgeSectionIds = new Set(knowledgeSections.map((section) => section.id));
|
|
23474
23781
|
const sectionIds = /* @__PURE__ */ new Set();
|
|
23475
23782
|
const featureIds = /* @__PURE__ */ new Set();
|
|
23476
23783
|
const featureMap = /* @__PURE__ */ new Map();
|
|
23477
23784
|
for (const section of currentGraph.sections) {
|
|
23478
|
-
if (!section.
|
|
23479
|
-
errors.push(buildProjectPlanCheckError("section_missing_id", "Project plan section is missing `
|
|
23785
|
+
if (!section.id) {
|
|
23786
|
+
errors.push(buildProjectPlanCheckError("section_missing_id", "Project plan section is missing `id`."));
|
|
23480
23787
|
continue;
|
|
23481
23788
|
}
|
|
23482
|
-
if (sectionIds.has(section.
|
|
23483
|
-
errors.push(buildProjectPlanCheckError("duplicate_section", `Duplicate section id "${section.
|
|
23789
|
+
if (sectionIds.has(section.id)) {
|
|
23790
|
+
errors.push(buildProjectPlanCheckError("duplicate_section", `Duplicate section id "${section.id}"`, section.id));
|
|
23484
23791
|
continue;
|
|
23485
23792
|
}
|
|
23486
|
-
sectionIds.add(section.
|
|
23487
|
-
if (!
|
|
23488
|
-
errors.push(buildProjectPlanCheckError(
|
|
23489
|
-
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
));
|
|
23793
|
+
sectionIds.add(section.id);
|
|
23794
|
+
if (!section.title.trim()) {
|
|
23795
|
+
errors.push(buildProjectPlanCheckError("section_missing_title", `Section "${section.id}" is missing a title.`, section.id));
|
|
23796
|
+
}
|
|
23797
|
+
if (!section.slug.trim()) {
|
|
23798
|
+
errors.push(buildProjectPlanCheckError("section_missing_slug", `Section "${section.id}" is missing a slug.`, section.id));
|
|
23493
23799
|
}
|
|
23494
23800
|
}
|
|
23495
23801
|
const allowedTypes = new Set(PROJECT_TASK_TYPES);
|
|
@@ -23584,350 +23890,114 @@ async function validateProjectPlanGraph(root, graph) {
|
|
|
23584
23890
|
}
|
|
23585
23891
|
}
|
|
23586
23892
|
return {
|
|
23587
|
-
ok: errors.length === 0,
|
|
23588
|
-
errors
|
|
23589
|
-
};
|
|
23590
|
-
}
|
|
23591
|
-
async function resolveKnowledgeSectionIds(root) {
|
|
23592
|
-
const sections = await readKnowledgeSections(root);
|
|
23593
|
-
return sections.map((section) => section.id).sort(compareStrings);
|
|
23594
|
-
}
|
|
23595
|
-
async function syncProjectPlanSectionsFromKnowledge(root) {
|
|
23596
|
-
const currentGraph = await readProjectPlanGraph(root);
|
|
23597
|
-
const knowledgeSectionIds = await resolveKnowledgeSectionIds(root);
|
|
23598
|
-
const syncedGraph = {
|
|
23599
|
-
...currentGraph,
|
|
23600
|
-
sections: knowledgeSectionIds.map((sectionId) => ({
|
|
23601
|
-
sectionId
|
|
23602
|
-
}))
|
|
23603
|
-
};
|
|
23604
|
-
return writeProjectPlanFiles(root, syncedGraph);
|
|
23605
|
-
}
|
|
23606
|
-
async function ensureProjectPlanGraph(root) {
|
|
23607
|
-
const graphPath = resolveProjectPlanGraphPath(root);
|
|
23608
|
-
if (!(0, import_node_fs8.existsSync)(graphPath)) {
|
|
23609
|
-
return syncProjectPlanSectionsFromKnowledge(root);
|
|
23610
|
-
}
|
|
23611
|
-
const graph = await readProjectPlanGraph(root);
|
|
23612
|
-
return writeProjectPlanFiles(root, graph);
|
|
23613
|
-
}
|
|
23614
|
-
async function upsertProjectPlanFeature(params) {
|
|
23615
|
-
const state = await ensureProjectPlanGraph(params.root);
|
|
23616
|
-
const slug = slugify(params.slug?.trim() || params.title);
|
|
23617
|
-
const featureId = params.featureId?.trim() || normalizeFeatureId(params.sectionId, slug);
|
|
23618
|
-
const graph = state.graph;
|
|
23619
|
-
const existing = graph.features.find((feature) => feature.id === featureId) || graph.features.find((feature) => feature.sectionId === params.sectionId && feature.slug === slug);
|
|
23620
|
-
const nextFeature = {
|
|
23621
|
-
id: existing?.id || featureId,
|
|
23622
|
-
title: params.title.trim(),
|
|
23623
|
-
slug,
|
|
23624
|
-
summary: params.summary?.trim() || "",
|
|
23625
|
-
sectionId: params.sectionId
|
|
23626
|
-
};
|
|
23627
|
-
const nextFeatures = existing ? graph.features.map((feature) => feature.id === existing.id ? nextFeature : feature) : [...graph.features, nextFeature];
|
|
23628
|
-
await writeProjectPlanFiles(params.root, {
|
|
23629
|
-
...graph,
|
|
23630
|
-
features: nextFeatures
|
|
23631
|
-
});
|
|
23632
|
-
return nextFeature;
|
|
23633
|
-
}
|
|
23634
|
-
async function getProjectPlanTask(params) {
|
|
23635
|
-
const graph = await readProjectPlanGraph(params.root);
|
|
23636
|
-
const linkedTodos = await listLinkedTodosByTask(params.root);
|
|
23637
|
-
const task = graph.tasks.find((item) => item.id === params.taskId);
|
|
23638
|
-
if (!task) {
|
|
23639
|
-
return null;
|
|
23640
|
-
}
|
|
23641
|
-
const taskLinkedTodos = linkedTodos.get(task.id) || [];
|
|
23642
|
-
return {
|
|
23643
|
-
...task,
|
|
23644
|
-
linkedTodos: taskLinkedTodos,
|
|
23645
|
-
linkedTodoCount: taskLinkedTodos.length
|
|
23646
|
-
};
|
|
23647
|
-
}
|
|
23648
|
-
async function upsertProjectPlanTask(params) {
|
|
23649
|
-
const state = await ensureProjectPlanGraph(params.root);
|
|
23650
|
-
const graph = state.graph;
|
|
23651
|
-
const feature = graph.features.find((item) => item.id === params.featureId);
|
|
23652
|
-
if (!feature) {
|
|
23653
|
-
throw new Error(`Feature "${params.featureId}" not found.`);
|
|
23654
|
-
}
|
|
23655
|
-
const sectionId = params.sectionId?.trim() || feature.sectionId;
|
|
23656
|
-
const taskId = params.taskId?.trim() || normalizeTaskId(feature.id, params.type, params.title);
|
|
23657
|
-
const existing = graph.tasks.find((task) => task.id === taskId) || graph.tasks.find((task) => task.featureId === feature.id && task.type === params.type && task.title === params.title.trim());
|
|
23658
|
-
const nextTask = {
|
|
23659
|
-
id: existing?.id || taskId,
|
|
23660
|
-
title: params.title.trim(),
|
|
23661
|
-
summary: params.summary?.trim() || "",
|
|
23662
|
-
type: params.type,
|
|
23663
|
-
assignee: params.assignee,
|
|
23664
|
-
status: params.status || "planned",
|
|
23665
|
-
acceptanceCriteria: [...new Set((params.acceptanceCriteria || []).map((item) => item.trim()).filter(Boolean))],
|
|
23666
|
-
featureId: feature.id,
|
|
23667
|
-
sectionId
|
|
23668
|
-
};
|
|
23669
|
-
const nextTasks = existing ? graph.tasks.map((task) => task.id === existing.id ? nextTask : task) : [...graph.tasks, nextTask];
|
|
23670
|
-
await writeProjectPlanFiles(params.root, {
|
|
23671
|
-
...graph,
|
|
23672
|
-
tasks: nextTasks
|
|
23673
|
-
});
|
|
23674
|
-
return nextTask;
|
|
23675
|
-
}
|
|
23676
|
-
async function updateProjectPlanTaskStatus(params) {
|
|
23677
|
-
const graph = await readProjectPlanGraph(params.root);
|
|
23678
|
-
const task = graph.tasks.find((item) => item.id === params.taskId);
|
|
23679
|
-
if (!task) {
|
|
23680
|
-
throw new Error(`Task "${params.taskId}" not found.`);
|
|
23681
|
-
}
|
|
23682
|
-
const nextTask = {
|
|
23683
|
-
...task,
|
|
23684
|
-
status: params.status
|
|
23685
|
-
};
|
|
23686
|
-
await writeProjectPlanFiles(params.root, {
|
|
23687
|
-
...graph,
|
|
23688
|
-
tasks: graph.tasks.map((item) => item.id === task.id ? nextTask : item)
|
|
23689
|
-
});
|
|
23690
|
-
return nextTask;
|
|
23691
|
-
}
|
|
23692
|
-
|
|
23693
|
-
// src/knowledge/init.ts
|
|
23694
|
-
var MARKER_BEGIN = "%% docyrus-knowledge:begin %%";
|
|
23695
|
-
var MARKER_END = "%% docyrus-knowledge:end %%";
|
|
23696
|
-
var HOOK_MARKER_BEGIN = "# docyrus-knowledge:begin";
|
|
23697
|
-
var HOOK_MARKER_END = "# docyrus-knowledge:end";
|
|
23698
|
-
function wrapManagedBlock(content3) {
|
|
23699
|
-
return `${MARKER_BEGIN}
|
|
23700
|
-
${content3}${content3.endsWith("\n") ? "" : "\n"}${MARKER_END}
|
|
23701
|
-
`;
|
|
23702
|
-
}
|
|
23703
|
-
async function ensureDirectory(pathValue) {
|
|
23704
|
-
await (0, import_promises11.mkdir)(pathValue, {
|
|
23705
|
-
recursive: true,
|
|
23706
|
-
mode: 493
|
|
23707
|
-
});
|
|
23708
|
-
}
|
|
23709
|
-
async function writeIfMissing(filePath, content3) {
|
|
23710
|
-
if ((0, import_node_fs9.existsSync)(filePath)) {
|
|
23711
|
-
return "kept";
|
|
23712
|
-
}
|
|
23713
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23714
|
-
await (0, import_promises11.writeFile)(filePath, content3, "utf8");
|
|
23715
|
-
return "created";
|
|
23716
|
-
}
|
|
23717
|
-
async function writeOrUpdate(filePath, content3) {
|
|
23718
|
-
if (!(0, import_node_fs9.existsSync)(filePath)) {
|
|
23719
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23720
|
-
await (0, import_promises11.writeFile)(filePath, content3, "utf8");
|
|
23721
|
-
return "created";
|
|
23722
|
-
}
|
|
23723
|
-
const current = await (0, import_promises11.readFile)(filePath, "utf8");
|
|
23724
|
-
if (current === content3) {
|
|
23725
|
-
return "kept";
|
|
23726
|
-
}
|
|
23727
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23728
|
-
await (0, import_promises11.writeFile)(filePath, content3, "utf8");
|
|
23729
|
-
return "updated";
|
|
23730
|
-
}
|
|
23731
|
-
async function upsertManagedBlock(filePath, content3) {
|
|
23732
|
-
const wrapped = wrapManagedBlock(content3);
|
|
23733
|
-
if (!(0, import_node_fs9.existsSync)(filePath)) {
|
|
23734
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23735
|
-
await (0, import_promises11.writeFile)(filePath, wrapped, "utf8");
|
|
23736
|
-
return "created";
|
|
23737
|
-
}
|
|
23738
|
-
const current = await (0, import_promises11.readFile)(filePath, "utf8");
|
|
23739
|
-
const beginIndex = current.indexOf(MARKER_BEGIN);
|
|
23740
|
-
const endIndex = current.indexOf(MARKER_END);
|
|
23741
|
-
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
23742
|
-
const replaceEnd = current[endIndex + MARKER_END.length] === "\n" ? endIndex + MARKER_END.length + 1 : endIndex + MARKER_END.length;
|
|
23743
|
-
const updated = `${current.slice(0, beginIndex)}${wrapped}${current.slice(replaceEnd)}`;
|
|
23744
|
-
await (0, import_promises11.writeFile)(filePath, updated, "utf8");
|
|
23745
|
-
return "updated";
|
|
23746
|
-
}
|
|
23747
|
-
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
23748
|
-
await (0, import_promises11.writeFile)(filePath, `${current}${separator}${wrapped}`, "utf8");
|
|
23749
|
-
return "appended";
|
|
23750
|
-
}
|
|
23751
|
-
function normalizeObject(value2) {
|
|
23752
|
-
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) ? { ...value2 } : {};
|
|
23753
|
-
}
|
|
23754
|
-
async function updateJsonFile(filePath, updater) {
|
|
23755
|
-
const current = (0, import_node_fs9.existsSync)(filePath) ? normalizeObject(JSON.parse((0, import_node_fs9.readFileSync)(filePath, "utf8"))) : {};
|
|
23756
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23757
|
-
await (0, import_promises11.writeFile)(filePath, `${JSON.stringify(updater(current), null, 2)}
|
|
23758
|
-
`, "utf8");
|
|
23759
|
-
}
|
|
23760
|
-
function wrapHookBlock(content3) {
|
|
23761
|
-
return `${HOOK_MARKER_BEGIN}
|
|
23762
|
-
${content3}${content3.endsWith("\n") ? "" : "\n"}${HOOK_MARKER_END}
|
|
23763
|
-
`;
|
|
23764
|
-
}
|
|
23765
|
-
async function upsertShellHook(filePath, content3, shellHeader) {
|
|
23766
|
-
const wrapped = wrapHookBlock(content3);
|
|
23767
|
-
if (!(0, import_node_fs9.existsSync)(filePath)) {
|
|
23768
|
-
await ensureDirectory((0, import_node_path15.dirname)(filePath));
|
|
23769
|
-
const initial = `${shellHeader || ""}${wrapped}`;
|
|
23770
|
-
await (0, import_promises11.writeFile)(filePath, initial, {
|
|
23771
|
-
encoding: "utf8",
|
|
23772
|
-
mode: 493
|
|
23773
|
-
});
|
|
23774
|
-
await (0, import_promises11.chmod)(filePath, 493);
|
|
23775
|
-
return "created";
|
|
23776
|
-
}
|
|
23777
|
-
const current = await (0, import_promises11.readFile)(filePath, "utf8");
|
|
23778
|
-
const beginIndex = current.indexOf(HOOK_MARKER_BEGIN);
|
|
23779
|
-
const endIndex = current.indexOf(HOOK_MARKER_END);
|
|
23780
|
-
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
23781
|
-
const replaceEnd = current[endIndex + HOOK_MARKER_END.length] === "\n" ? endIndex + HOOK_MARKER_END.length + 1 : endIndex + HOOK_MARKER_END.length;
|
|
23782
|
-
const nextContent = `${current.slice(0, beginIndex)}${wrapped}${current.slice(replaceEnd)}`;
|
|
23783
|
-
if (nextContent === current) {
|
|
23784
|
-
return "kept";
|
|
23785
|
-
}
|
|
23786
|
-
await (0, import_promises11.writeFile)(filePath, nextContent, "utf8");
|
|
23787
|
-
await (0, import_promises11.chmod)(filePath, 493);
|
|
23788
|
-
return "updated";
|
|
23789
|
-
}
|
|
23790
|
-
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
23791
|
-
await (0, import_promises11.writeFile)(filePath, `${current}${separator}${wrapped}`, "utf8");
|
|
23792
|
-
await (0, import_promises11.chmod)(filePath, 493);
|
|
23793
|
-
return "appended";
|
|
23794
|
-
}
|
|
23795
|
-
function buildKnowledgeDocumentTemplate() {
|
|
23796
|
-
return [
|
|
23797
|
-
"# Knowledge",
|
|
23798
|
-
"",
|
|
23799
|
-
"This directory captures the architecture, constraints, workflows, and test expectations that coding agents should read before changing the repo.",
|
|
23800
|
-
"",
|
|
23801
|
-
"## Working Agreement",
|
|
23802
|
-
"",
|
|
23803
|
-
"Search this knowledge graph before coding, keep it updated when behavior changes, and use \\[\\[wiki links\\]\\] plus `@docyrus` backlinks to connect decisions to implementation.",
|
|
23804
|
-
"",
|
|
23805
|
-
"## Backlinks",
|
|
23806
|
-
"",
|
|
23807
|
-
"Use `// @docyrus: \\[\\[knowledge#Section Name\\]\\]` or `# @docyrus: \\[\\[knowledge#Section Name\\]\\]` near the code or tests that implement a documented behavior.",
|
|
23808
|
-
""
|
|
23809
|
-
].join("\n");
|
|
23893
|
+
ok: errors.length === 0,
|
|
23894
|
+
errors
|
|
23895
|
+
};
|
|
23810
23896
|
}
|
|
23811
|
-
function
|
|
23812
|
-
|
|
23813
|
-
|
|
23814
|
-
|
|
23815
|
-
|
|
23816
|
-
|
|
23817
|
-
|
|
23818
|
-
`- Before finishing, run \`${commandPrefix} knowledge check\` and fix any broken links, stale references, or missing \`@docyrus\` backlinks.`,
|
|
23819
|
-
""
|
|
23820
|
-
].join("\n");
|
|
23897
|
+
async function ensureProjectPlanGraph(root) {
|
|
23898
|
+
const graphPath = resolveProjectPlanGraphPath(root);
|
|
23899
|
+
if (!(0, import_node_fs9.existsSync)(graphPath)) {
|
|
23900
|
+
return writeProjectPlanFiles(root, createEmptyProjectPlanGraph());
|
|
23901
|
+
}
|
|
23902
|
+
const graph = await readProjectPlanGraph(root);
|
|
23903
|
+
return writeProjectPlanFiles(root, graph);
|
|
23821
23904
|
}
|
|
23822
|
-
function
|
|
23823
|
-
|
|
23824
|
-
|
|
23825
|
-
|
|
23826
|
-
|
|
23827
|
-
|
|
23828
|
-
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
|
|
23905
|
+
async function upsertProjectPlanSection(params) {
|
|
23906
|
+
const state = await ensureProjectPlanGraph(params.root);
|
|
23907
|
+
const slug = slugify(params.slug?.trim() || params.title);
|
|
23908
|
+
const sectionId = params.id?.trim() || normalizeSectionId(slug);
|
|
23909
|
+
const graph = state.graph;
|
|
23910
|
+
const existing = graph.sections.find((section) => section.id === sectionId) || graph.sections.find((section) => section.slug === slug);
|
|
23911
|
+
const nextSection = {
|
|
23912
|
+
id: existing?.id || sectionId,
|
|
23913
|
+
title: params.title.trim(),
|
|
23914
|
+
slug,
|
|
23915
|
+
summary: params.summary?.trim() || ""
|
|
23916
|
+
};
|
|
23917
|
+
const nextSections = existing ? graph.sections.map((section) => section.id === existing.id ? nextSection : section) : [...graph.sections, nextSection];
|
|
23918
|
+
await writeProjectPlanFiles(params.root, {
|
|
23919
|
+
...graph,
|
|
23920
|
+
sections: nextSections
|
|
23921
|
+
});
|
|
23922
|
+
return nextSection;
|
|
23832
23923
|
}
|
|
23833
|
-
function
|
|
23834
|
-
|
|
23924
|
+
async function upsertProjectPlanFeature(params) {
|
|
23925
|
+
const state = await ensureProjectPlanGraph(params.root);
|
|
23926
|
+
const slug = slugify(params.slug?.trim() || params.title);
|
|
23927
|
+
const featureId = params.featureId?.trim() || normalizeFeatureId(params.sectionId, slug);
|
|
23928
|
+
const graph = state.graph;
|
|
23929
|
+
const existing = graph.features.find((feature) => feature.id === featureId) || graph.features.find((feature) => feature.sectionId === params.sectionId && feature.slug === slug);
|
|
23930
|
+
const nextFeature = {
|
|
23931
|
+
id: existing?.id || featureId,
|
|
23932
|
+
title: params.title.trim(),
|
|
23933
|
+
slug,
|
|
23934
|
+
summary: params.summary?.trim() || "",
|
|
23935
|
+
sectionId: params.sectionId
|
|
23936
|
+
};
|
|
23937
|
+
const nextFeatures = existing ? graph.features.map((feature) => feature.id === existing.id ? nextFeature : feature) : [...graph.features, nextFeature];
|
|
23938
|
+
await writeProjectPlanFiles(params.root, {
|
|
23939
|
+
...graph,
|
|
23940
|
+
features: nextFeatures
|
|
23941
|
+
});
|
|
23942
|
+
return nextFeature;
|
|
23835
23943
|
}
|
|
23836
|
-
function
|
|
23837
|
-
const
|
|
23838
|
-
const
|
|
23839
|
-
|
|
23840
|
-
|
|
23841
|
-
|
|
23842
|
-
}
|
|
23843
|
-
nextHooks[eventName] = entries.filter((entry) => {
|
|
23844
|
-
const hooksList = Array.isArray(entry.hooks) ? entry.hooks || [] : [];
|
|
23845
|
-
return !hooksList.some((hook) => typeof hook.command === "string" && hook.command.includes("knowledge hook claude"));
|
|
23846
|
-
});
|
|
23944
|
+
async function getProjectPlanTask(params) {
|
|
23945
|
+
const graph = await readProjectPlanGraph(params.root);
|
|
23946
|
+
const linkedTodos = await listLinkedTodosByTask(params.root);
|
|
23947
|
+
const task = graph.tasks.find((item) => item.id === params.taskId);
|
|
23948
|
+
if (!task) {
|
|
23949
|
+
return null;
|
|
23847
23950
|
}
|
|
23848
|
-
const
|
|
23849
|
-
const current = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : [];
|
|
23850
|
-
current.push({
|
|
23851
|
-
hooks: [
|
|
23852
|
-
{
|
|
23853
|
-
type: "command",
|
|
23854
|
-
command: buildClaudeHookCommand(commandPrefix, eventName)
|
|
23855
|
-
}
|
|
23856
|
-
]
|
|
23857
|
-
});
|
|
23858
|
-
nextHooks[eventName] = current;
|
|
23859
|
-
};
|
|
23860
|
-
addHook("UserPromptSubmit");
|
|
23861
|
-
addHook("Stop");
|
|
23951
|
+
const taskLinkedTodos = linkedTodos.get(task.id) || [];
|
|
23862
23952
|
return {
|
|
23863
|
-
...
|
|
23864
|
-
|
|
23953
|
+
...task,
|
|
23954
|
+
linkedTodos: taskLinkedTodos,
|
|
23955
|
+
linkedTodoCount: taskLinkedTodos.length
|
|
23865
23956
|
};
|
|
23866
23957
|
}
|
|
23867
|
-
function
|
|
23868
|
-
const
|
|
23869
|
-
const
|
|
23870
|
-
const
|
|
23871
|
-
|
|
23872
|
-
|
|
23873
|
-
|
|
23874
|
-
|
|
23875
|
-
|
|
23876
|
-
|
|
23877
|
-
|
|
23878
|
-
|
|
23879
|
-
|
|
23880
|
-
|
|
23881
|
-
|
|
23882
|
-
|
|
23958
|
+
async function upsertProjectPlanTask(params) {
|
|
23959
|
+
const state = await ensureProjectPlanGraph(params.root);
|
|
23960
|
+
const graph = state.graph;
|
|
23961
|
+
const feature = graph.features.find((item) => item.id === params.featureId);
|
|
23962
|
+
if (!feature) {
|
|
23963
|
+
throw new Error(`Feature "${params.featureId}" not found.`);
|
|
23964
|
+
}
|
|
23965
|
+
const sectionId = params.sectionId?.trim() || feature.sectionId;
|
|
23966
|
+
const taskId = params.taskId?.trim() || normalizeTaskId(feature.id, params.type, params.title);
|
|
23967
|
+
const existing = graph.tasks.find((task) => task.id === taskId) || graph.tasks.find((task) => task.featureId === feature.id && task.type === params.type && task.title === params.title.trim());
|
|
23968
|
+
const nextTask = {
|
|
23969
|
+
id: existing?.id || taskId,
|
|
23970
|
+
title: params.title.trim(),
|
|
23971
|
+
summary: params.summary?.trim() || "",
|
|
23972
|
+
type: params.type,
|
|
23973
|
+
assignee: params.assignee,
|
|
23974
|
+
status: params.status || "planned",
|
|
23975
|
+
acceptanceCriteria: [...new Set((params.acceptanceCriteria || []).map((item) => item.trim()).filter(Boolean))],
|
|
23976
|
+
featureId: feature.id,
|
|
23977
|
+
sectionId
|
|
23883
23978
|
};
|
|
23979
|
+
const nextTasks = existing ? graph.tasks.map((task) => task.id === existing.id ? nextTask : task) : [...graph.tasks, nextTask];
|
|
23980
|
+
await writeProjectPlanFiles(params.root, {
|
|
23981
|
+
...graph,
|
|
23982
|
+
tasks: nextTasks
|
|
23983
|
+
});
|
|
23984
|
+
return nextTask;
|
|
23884
23985
|
}
|
|
23885
|
-
async function
|
|
23886
|
-
const
|
|
23887
|
-
const
|
|
23888
|
-
|
|
23889
|
-
|
|
23890
|
-
const knowledgeFileStatus = await writeIfMissing(knowledgeFile, `${buildKnowledgeDocumentTemplate()}
|
|
23891
|
-
`);
|
|
23892
|
-
const agentsStatus = await upsertManagedBlock((0, import_node_path15.join)(root, "AGENTS.md"), buildAgentsTemplate(params.commandPrefix));
|
|
23893
|
-
const claudeStatus = await upsertManagedBlock((0, import_node_path15.join)(root, "CLAUDE.md"), buildAgentsTemplate(params.commandPrefix));
|
|
23894
|
-
const cursorRulesStatus = await writeOrUpdate((0, import_node_path15.join)(root, ".cursor", "rules", "docyrus-knowledge.md"), `${buildCursorRulesTemplate(params.commandPrefix)}
|
|
23895
|
-
`);
|
|
23896
|
-
const bootstrap = (await generateInitialKnowledge({
|
|
23897
|
-
root,
|
|
23898
|
-
brief: params.brief
|
|
23899
|
-
})).files;
|
|
23900
|
-
const projectPlan = await syncProjectPlanSectionsFromKnowledge(root);
|
|
23901
|
-
await updateJsonFile((0, import_node_path15.join)(root, ".claude", "settings.json"), (value2) => syncClaudeHooks(value2, params.commandPrefix));
|
|
23902
|
-
await updateJsonFile((0, import_node_path15.join)(root, ".cursor", "hooks.json"), (value2) => syncCursorHooks(value2, params.commandPrefix));
|
|
23903
|
-
let huskyHookStatus;
|
|
23904
|
-
const huskyDir = (0, import_node_path15.join)(root, ".husky");
|
|
23905
|
-
if ((0, import_node_fs9.existsSync)(huskyDir)) {
|
|
23906
|
-
const huskyHookPath = (0, import_node_path15.join)(huskyDir, "pre-commit");
|
|
23907
|
-
const huskyHeader = (0, import_node_fs9.existsSync)((0, import_node_path15.join)(huskyDir, "_", "h")) ? '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/h"\n\n' : void 0;
|
|
23908
|
-
huskyHookStatus = await upsertShellHook(huskyHookPath, `${params.commandPrefix} knowledge pre-commit
|
|
23909
|
-
`, huskyHeader);
|
|
23910
|
-
}
|
|
23911
|
-
let gitHookStatus;
|
|
23912
|
-
if (!huskyHookStatus && params.installGitHook) {
|
|
23913
|
-
const gitHookPath = (0, import_node_path15.join)(root, ".git", "hooks", "pre-commit");
|
|
23914
|
-
gitHookStatus = await upsertShellHook(gitHookPath, `${params.commandPrefix} knowledge pre-commit
|
|
23915
|
-
`, "#!/usr/bin/env sh\n\n");
|
|
23986
|
+
async function updateProjectPlanTaskStatus(params) {
|
|
23987
|
+
const graph = await readProjectPlanGraph(params.root);
|
|
23988
|
+
const task = graph.tasks.find((item) => item.id === params.taskId);
|
|
23989
|
+
if (!task) {
|
|
23990
|
+
throw new Error(`Task "${params.taskId}" not found.`);
|
|
23916
23991
|
}
|
|
23917
|
-
|
|
23918
|
-
|
|
23919
|
-
|
|
23920
|
-
knowledgeFile,
|
|
23921
|
-
projectPlanGraphPath: projectPlan.graphPath,
|
|
23922
|
-
projectPlanMarkdownPath: projectPlan.markdownPath,
|
|
23923
|
-
agentsStatus,
|
|
23924
|
-
claudeStatus,
|
|
23925
|
-
cursorRulesStatus,
|
|
23926
|
-
knowledgeFileStatus,
|
|
23927
|
-
bootstrap,
|
|
23928
|
-
huskyHookStatus,
|
|
23929
|
-
gitHookStatus
|
|
23992
|
+
const nextTask = {
|
|
23993
|
+
...task,
|
|
23994
|
+
status: params.status
|
|
23930
23995
|
};
|
|
23996
|
+
await writeProjectPlanFiles(params.root, {
|
|
23997
|
+
...graph,
|
|
23998
|
+
tasks: graph.tasks.map((item) => item.id === task.id ? nextTask : item)
|
|
23999
|
+
});
|
|
24000
|
+
return nextTask;
|
|
23931
24001
|
}
|
|
23932
24002
|
|
|
23933
24003
|
// src/agent/authFlows.ts
|
|
@@ -25366,6 +25436,49 @@ async function waitForIdle(session, timeoutMs = 3e4) {
|
|
|
25366
25436
|
function resolveServerSettingsRootPath(agentDir) {
|
|
25367
25437
|
return (0, import_node_path18.resolve)(agentDir, "..", "..");
|
|
25368
25438
|
}
|
|
25439
|
+
var SESSION_MODE_COMMANDS = {
|
|
25440
|
+
"read-only": "read-only",
|
|
25441
|
+
"end-read-only": "normal"
|
|
25442
|
+
};
|
|
25443
|
+
var SESSION_MODE_COMMAND_LABELS = {
|
|
25444
|
+
"read-only": "Read-only mode activated. Write and edit tools are disabled. Use /end-read-only to resume normal operation.",
|
|
25445
|
+
"end-read-only": "Read-only mode deactivated. Normal operation resumed."
|
|
25446
|
+
};
|
|
25447
|
+
function parseModeSlashCommand(text3) {
|
|
25448
|
+
const match2 = /^\/([a-z][a-z0-9-]*)(?:\s+([\s\S]+))?$/u.exec(text3.trim());
|
|
25449
|
+
if (!match2) {
|
|
25450
|
+
return null;
|
|
25451
|
+
}
|
|
25452
|
+
const command = match2[1];
|
|
25453
|
+
if (!(command in SESSION_MODE_COMMANDS)) {
|
|
25454
|
+
return null;
|
|
25455
|
+
}
|
|
25456
|
+
return {
|
|
25457
|
+
command,
|
|
25458
|
+
remainder: match2[2]?.trim() ?? ""
|
|
25459
|
+
};
|
|
25460
|
+
}
|
|
25461
|
+
function makeModeNotificationStream(params) {
|
|
25462
|
+
const { command, messageId, encoder } = params;
|
|
25463
|
+
const label = SESSION_MODE_COMMAND_LABELS[command] ?? `/${command} executed.`;
|
|
25464
|
+
return new ReadableStream({
|
|
25465
|
+
start(controller) {
|
|
25466
|
+
function write(chunk) {
|
|
25467
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}
|
|
25468
|
+
|
|
25469
|
+
`));
|
|
25470
|
+
}
|
|
25471
|
+
write({ type: "start" });
|
|
25472
|
+
write({ type: "start-step" });
|
|
25473
|
+
write({ type: "text-start", id: messageId });
|
|
25474
|
+
write({ type: "text-delta", id: messageId, delta: label });
|
|
25475
|
+
write({ type: "text-end", id: messageId });
|
|
25476
|
+
write({ type: "finish-step" });
|
|
25477
|
+
write({ type: "finish" });
|
|
25478
|
+
controller.close();
|
|
25479
|
+
}
|
|
25480
|
+
});
|
|
25481
|
+
}
|
|
25369
25482
|
function serializeKnowledgeMatch(match2) {
|
|
25370
25483
|
return {
|
|
25371
25484
|
id: match2.section.id,
|
|
@@ -25660,6 +25773,7 @@ async function walkFiles(params) {
|
|
|
25660
25773
|
async function createAgentServer(params) {
|
|
25661
25774
|
const { port, sessionManager, modelRegistry, authRuntime, context, onCreateSession, onResumeSession, authToken } = params;
|
|
25662
25775
|
let activeSession = params.session;
|
|
25776
|
+
let sessionMode = "normal";
|
|
25663
25777
|
const pendingAskUserRequests = /* @__PURE__ */ new Map();
|
|
25664
25778
|
const pendingDocyrusWebBrowserRequests = /* @__PURE__ */ new Map();
|
|
25665
25779
|
const oauthFlowManager = new OAuthFlowManager();
|
|
@@ -25700,15 +25814,17 @@ async function createAgentServer(params) {
|
|
|
25700
25814
|
app.get("/api/status", (c) => {
|
|
25701
25815
|
return c.json({
|
|
25702
25816
|
isStreaming: activeSession.isStreaming,
|
|
25703
|
-
model: activeSession.model ? { provider: activeSession.model.provider, id: activeSession.model.id } : null
|
|
25817
|
+
model: activeSession.model ? { provider: activeSession.model.provider, id: activeSession.model.id } : null,
|
|
25818
|
+
mode: sessionMode
|
|
25704
25819
|
});
|
|
25705
25820
|
});
|
|
25706
25821
|
app.post("/api/chat", async (c) => {
|
|
25707
25822
|
const body2 = await c.req.json();
|
|
25708
25823
|
const messages = body2.messages ?? [];
|
|
25709
|
-
if (body2.sessionId) {
|
|
25824
|
+
if (body2.sessionId && body2.sessionId.trim() !== activeSession.id?.trim()) {
|
|
25710
25825
|
try {
|
|
25711
25826
|
activeSession = await onResumeSession(body2.sessionId);
|
|
25827
|
+
sessionMode = "normal";
|
|
25712
25828
|
} catch (error) {
|
|
25713
25829
|
const msg = error instanceof Error ? error.message : String(error);
|
|
25714
25830
|
return c.json({ error: `Failed to resume session: ${msg}` }, 400);
|
|
@@ -25745,6 +25861,34 @@ async function createAgentServer(params) {
|
|
|
25745
25861
|
pendingDocyrusWebBrowserRequests.delete(sessionId);
|
|
25746
25862
|
}
|
|
25747
25863
|
}
|
|
25864
|
+
let promptText = userMessage;
|
|
25865
|
+
if (!askUserResponse && !docyrusWebBrowserResponse) {
|
|
25866
|
+
const modeCmd = parseModeSlashCommand(userMessage);
|
|
25867
|
+
if (modeCmd) {
|
|
25868
|
+
if (activeSession.isStreaming) {
|
|
25869
|
+
await activeSession.abort();
|
|
25870
|
+
await waitForIdle(activeSession);
|
|
25871
|
+
}
|
|
25872
|
+
await activeSession.prompt(`/${modeCmd.command}`);
|
|
25873
|
+
sessionMode = SESSION_MODE_COMMANDS[modeCmd.command] ?? sessionMode;
|
|
25874
|
+
if (!modeCmd.remainder) {
|
|
25875
|
+
const notifEncoder = new TextEncoder();
|
|
25876
|
+
const notifMessageId = generateMessageId();
|
|
25877
|
+
return c.body(
|
|
25878
|
+
makeModeNotificationStream({ command: modeCmd.command, messageId: notifMessageId, encoder: notifEncoder }),
|
|
25879
|
+
{
|
|
25880
|
+
headers: {
|
|
25881
|
+
"Content-Type": "text/event-stream",
|
|
25882
|
+
"Cache-Control": "no-cache",
|
|
25883
|
+
"Connection": "keep-alive",
|
|
25884
|
+
"x-vercel-ai-ui-message-stream": "v1"
|
|
25885
|
+
}
|
|
25886
|
+
}
|
|
25887
|
+
);
|
|
25888
|
+
}
|
|
25889
|
+
promptText = modeCmd.remainder;
|
|
25890
|
+
}
|
|
25891
|
+
}
|
|
25748
25892
|
if (activeSession.isStreaming) {
|
|
25749
25893
|
await activeSession.abort();
|
|
25750
25894
|
await waitForIdle(activeSession);
|
|
@@ -25789,7 +25933,7 @@ async function createAgentServer(params) {
|
|
|
25789
25933
|
unsubscribe();
|
|
25790
25934
|
}
|
|
25791
25935
|
});
|
|
25792
|
-
activeSession.prompt(
|
|
25936
|
+
activeSession.prompt(promptText).then(() => {
|
|
25793
25937
|
if (!activeSession.isStreaming) {
|
|
25794
25938
|
unsubscribe();
|
|
25795
25939
|
writeChunk({ type: "finish-step" });
|
|
@@ -25815,6 +25959,19 @@ async function createAgentServer(params) {
|
|
|
25815
25959
|
}
|
|
25816
25960
|
});
|
|
25817
25961
|
});
|
|
25962
|
+
app.post("/api/chat/abort", async (c) => {
|
|
25963
|
+
if (!activeSession.isStreaming) {
|
|
25964
|
+
return c.json({ ok: true, wasStreaming: false });
|
|
25965
|
+
}
|
|
25966
|
+
try {
|
|
25967
|
+
await activeSession.abort();
|
|
25968
|
+
await waitForIdle(activeSession);
|
|
25969
|
+
return c.json({ ok: true, wasStreaming: true });
|
|
25970
|
+
} catch (error) {
|
|
25971
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25972
|
+
return c.json({ error: message }, 500);
|
|
25973
|
+
}
|
|
25974
|
+
});
|
|
25818
25975
|
app.get("/api/sessions", async (c) => {
|
|
25819
25976
|
try {
|
|
25820
25977
|
const sessions = await sessionManager.list();
|
|
@@ -25829,6 +25986,21 @@ async function createAgentServer(params) {
|
|
|
25829
25986
|
messageCount: s.messageCount,
|
|
25830
25987
|
firstMessage: s.firstMessage
|
|
25831
25988
|
})).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
25989
|
+
const activeId = activeSession.id?.trim();
|
|
25990
|
+
if (activeId && !mapped.some((s) => s.id === activeId)) {
|
|
25991
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
25992
|
+
mapped.unshift({
|
|
25993
|
+
id: activeId,
|
|
25994
|
+
path: "",
|
|
25995
|
+
cwd: context.cwd,
|
|
25996
|
+
name: null,
|
|
25997
|
+
parentSessionPath: null,
|
|
25998
|
+
created: now,
|
|
25999
|
+
modified: now,
|
|
26000
|
+
messageCount: 0,
|
|
26001
|
+
firstMessage: null
|
|
26002
|
+
});
|
|
26003
|
+
}
|
|
25832
26004
|
return c.json({ sessions: mapped });
|
|
25833
26005
|
} catch (error) {
|
|
25834
26006
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -25842,6 +26014,7 @@ async function createAgentServer(params) {
|
|
|
25842
26014
|
await waitForIdle(activeSession);
|
|
25843
26015
|
}
|
|
25844
26016
|
activeSession = await onCreateSession();
|
|
26017
|
+
sessionMode = "normal";
|
|
25845
26018
|
const sessionId = activeSession.id?.trim();
|
|
25846
26019
|
if (!sessionId) {
|
|
25847
26020
|
return c.json({ error: "Created session is missing an id" }, 500);
|
|
@@ -26049,8 +26222,7 @@ async function createAgentServer(params) {
|
|
|
26049
26222
|
const codeRefs = await checkKnowledgeCodeRefs(knowledgeDir);
|
|
26050
26223
|
const index2 = await checkKnowledgeIndex(knowledgeDir);
|
|
26051
26224
|
const sections = await checkKnowledgeSections(knowledgeDir);
|
|
26052
|
-
const
|
|
26053
|
-
const totalErrors = markdown.errors.length + codeRefs.errors.length + index2.length + sections.length + projectPlan.errors.length;
|
|
26225
|
+
const totalErrors = markdown.errors.length + codeRefs.errors.length + index2.length + sections.length;
|
|
26054
26226
|
return c.json({
|
|
26055
26227
|
ok: totalErrors === 0,
|
|
26056
26228
|
knowledgeDir,
|
|
@@ -26058,8 +26230,7 @@ async function createAgentServer(params) {
|
|
|
26058
26230
|
markdown: markdown.errors,
|
|
26059
26231
|
codeRefs: codeRefs.errors,
|
|
26060
26232
|
index: index2,
|
|
26061
|
-
sections
|
|
26062
|
-
projectPlan: projectPlan.errors
|
|
26233
|
+
sections
|
|
26063
26234
|
},
|
|
26064
26235
|
files: {
|
|
26065
26236
|
markdown: markdown.files,
|
|
@@ -26095,12 +26266,7 @@ async function createAgentServer(params) {
|
|
|
26095
26266
|
brief: body2.brief?.trim() || void 0,
|
|
26096
26267
|
targetSectionIds: Array.isArray(body2.sectionIds) ? body2.sectionIds : void 0
|
|
26097
26268
|
});
|
|
26098
|
-
|
|
26099
|
-
return c.json({
|
|
26100
|
-
...result,
|
|
26101
|
-
projectPlanGraphPath: projectPlan.graphPath,
|
|
26102
|
-
projectPlanMarkdownPath: projectPlan.markdownPath
|
|
26103
|
-
});
|
|
26269
|
+
return c.json(result);
|
|
26104
26270
|
} catch (error) {
|
|
26105
26271
|
const message = error instanceof Error ? error.message : String(error);
|
|
26106
26272
|
return c.json({ error: message }, 500);
|
|
@@ -26144,17 +26310,23 @@ async function createAgentServer(params) {
|
|
|
26144
26310
|
return c.json({ error: message }, 500);
|
|
26145
26311
|
}
|
|
26146
26312
|
});
|
|
26147
|
-
app.post("/api/project-plan/
|
|
26313
|
+
app.post("/api/project-plan/sections", async (c) => {
|
|
26314
|
+
const body2 = await c.req.json().catch(() => ({}));
|
|
26315
|
+
if (!body2.title?.trim()) {
|
|
26316
|
+
return c.json({ error: "Missing required field: title" }, 400);
|
|
26317
|
+
}
|
|
26148
26318
|
try {
|
|
26149
|
-
const
|
|
26150
|
-
|
|
26151
|
-
|
|
26152
|
-
|
|
26153
|
-
|
|
26319
|
+
const section = await upsertProjectPlanSection({
|
|
26320
|
+
root: context.cwd,
|
|
26321
|
+
id: body2.id?.trim() || void 0,
|
|
26322
|
+
title: body2.title.trim(),
|
|
26323
|
+
slug: body2.slug?.trim() || void 0,
|
|
26324
|
+
summary: body2.summary?.trim() || void 0
|
|
26154
26325
|
});
|
|
26326
|
+
return c.json(section);
|
|
26155
26327
|
} catch (error) {
|
|
26156
26328
|
const message = error instanceof Error ? error.message : String(error);
|
|
26157
|
-
return c.json({ error: message },
|
|
26329
|
+
return c.json({ error: message }, 400);
|
|
26158
26330
|
}
|
|
26159
26331
|
});
|
|
26160
26332
|
app.get("/api/project-plan/tasks/:taskId", async (c) => {
|
|
@@ -26434,11 +26606,13 @@ async function createAgentServer(params) {
|
|
|
26434
26606
|
app.post("/api/sessions/:sessionId/resume", async (c) => {
|
|
26435
26607
|
const sessionId = c.req.param("sessionId");
|
|
26436
26608
|
try {
|
|
26437
|
-
if (activeSession.
|
|
26438
|
-
|
|
26439
|
-
|
|
26609
|
+
if (sessionId.trim() !== activeSession.id?.trim()) {
|
|
26610
|
+
if (activeSession.isStreaming) {
|
|
26611
|
+
await activeSession.abort();
|
|
26612
|
+
await waitForIdle(activeSession);
|
|
26613
|
+
}
|
|
26614
|
+
activeSession = await onResumeSession(sessionId);
|
|
26440
26615
|
}
|
|
26441
|
-
activeSession = await onResumeSession(sessionId);
|
|
26442
26616
|
return c.json({
|
|
26443
26617
|
ok: true,
|
|
26444
26618
|
sessionId,
|
|
@@ -26449,6 +26623,20 @@ async function createAgentServer(params) {
|
|
|
26449
26623
|
return c.json({ error: message }, 500);
|
|
26450
26624
|
}
|
|
26451
26625
|
});
|
|
26626
|
+
app.post("/api/sessions/rename", async (c) => {
|
|
26627
|
+
const body2 = await c.req.json();
|
|
26628
|
+
const name2 = typeof body2.name === "string" ? body2.name.trim() : "";
|
|
26629
|
+
if (!name2) {
|
|
26630
|
+
return c.json({ error: "Missing required field: name" }, 400);
|
|
26631
|
+
}
|
|
26632
|
+
try {
|
|
26633
|
+
activeSession.setSessionName(name2);
|
|
26634
|
+
return c.json({ ok: true, name: name2 });
|
|
26635
|
+
} catch (error) {
|
|
26636
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26637
|
+
return c.json({ error: message }, 500);
|
|
26638
|
+
}
|
|
26639
|
+
});
|
|
26452
26640
|
app.get("/api/fs/tree", async (c) => {
|
|
26453
26641
|
const requestPath = c.req.query("path") ?? ".";
|
|
26454
26642
|
const maxDepth = Math.min(Number(c.req.query("depth") ?? 4), 10);
|
|
@@ -26953,6 +27141,29 @@ async function createAgentServer(params) {
|
|
|
26953
27141
|
return c.json({ error: message }, 500);
|
|
26954
27142
|
}
|
|
26955
27143
|
});
|
|
27144
|
+
app.get("/api/git/commits", async (c) => {
|
|
27145
|
+
const cwd = context.cwd;
|
|
27146
|
+
const limit = Math.min(Math.max(Number(c.req.query("limit")) || 50, 1), 500);
|
|
27147
|
+
try {
|
|
27148
|
+
let raw2 = "";
|
|
27149
|
+
try {
|
|
27150
|
+
raw2 = await gitExec(
|
|
27151
|
+
["log", `--max-count=${limit}`, "--format=%H%x00%an%x00%ae%x00%aI%x00%s"],
|
|
27152
|
+
cwd
|
|
27153
|
+
);
|
|
27154
|
+
} catch {
|
|
27155
|
+
return c.json({ commits: [] });
|
|
27156
|
+
}
|
|
27157
|
+
const commits = raw2.trim().split("\n").filter(Boolean).map((line) => {
|
|
27158
|
+
const [hash, authorName, authorEmail, date, message] = line.split("\0");
|
|
27159
|
+
return { hash, authorName, authorEmail, date, message };
|
|
27160
|
+
});
|
|
27161
|
+
return c.json({ commits });
|
|
27162
|
+
} catch (error) {
|
|
27163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27164
|
+
return c.json({ error: message }, 500);
|
|
27165
|
+
}
|
|
27166
|
+
});
|
|
26956
27167
|
app.get("/api/mcp/servers", async (c) => {
|
|
26957
27168
|
try {
|
|
26958
27169
|
const [config, cache, provenance] = await Promise.all([
|
|
@@ -27236,6 +27447,8 @@ async function createAgentServer(params) {
|
|
|
27236
27447
|
|
|
27237
27448
|
`);
|
|
27238
27449
|
process.stderr.write(` POST /api/chat \u2014 send chat messages (SSE UIMessage stream)
|
|
27450
|
+
`);
|
|
27451
|
+
process.stderr.write(` POST /api/chat/abort \u2014 abort active chat stream
|
|
27239
27452
|
`);
|
|
27240
27453
|
process.stderr.write(` GET /api/health \u2014 health check
|
|
27241
27454
|
`);
|
|
@@ -27248,6 +27461,8 @@ async function createAgentServer(params) {
|
|
|
27248
27461
|
process.stderr.write(` GET /api/sessions/:sessionId/messages \u2014 session messages
|
|
27249
27462
|
`);
|
|
27250
27463
|
process.stderr.write(` POST /api/sessions/:sessionId/resume \u2014 resume a session
|
|
27464
|
+
`);
|
|
27465
|
+
process.stderr.write(` POST /api/sessions/rename \u2014 rename active session
|
|
27251
27466
|
`);
|
|
27252
27467
|
process.stderr.write(` GET /api/context \u2014 server context
|
|
27253
27468
|
`);
|
|
@@ -27263,11 +27478,11 @@ async function createAgentServer(params) {
|
|
|
27263
27478
|
`);
|
|
27264
27479
|
process.stderr.write(` GET /api/knowledge/search \u2014 semantic knowledge search
|
|
27265
27480
|
`);
|
|
27266
|
-
process.stderr.write(` GET /api/knowledge/check \u2014 knowledge
|
|
27481
|
+
process.stderr.write(` GET /api/knowledge/check \u2014 knowledge validation
|
|
27267
27482
|
`);
|
|
27268
|
-
process.stderr.write(` POST /api/knowledge/init \u2014 initialize knowledge graph
|
|
27483
|
+
process.stderr.write(` POST /api/knowledge/init \u2014 initialize knowledge graph
|
|
27269
27484
|
`);
|
|
27270
|
-
process.stderr.write(` POST /api/knowledge/refresh \u2014 refresh managed knowledge files
|
|
27485
|
+
process.stderr.write(` POST /api/knowledge/refresh \u2014 refresh managed knowledge files
|
|
27271
27486
|
`);
|
|
27272
27487
|
process.stderr.write(` GET /api/project-plan \u2014 canonical project-plan graph and hierarchy
|
|
27273
27488
|
`);
|
|
@@ -27275,7 +27490,7 @@ async function createAgentServer(params) {
|
|
|
27275
27490
|
`);
|
|
27276
27491
|
process.stderr.write(` POST /api/project-plan/ensure \u2014 ensure project-plan files exist
|
|
27277
27492
|
`);
|
|
27278
|
-
process.stderr.write(` POST /api/project-plan/
|
|
27493
|
+
process.stderr.write(` POST /api/project-plan/sections \u2014 upsert project-plan section
|
|
27279
27494
|
`);
|
|
27280
27495
|
process.stderr.write(` GET /api/project-plan/tasks/:taskId \u2014 project task detail
|
|
27281
27496
|
`);
|
|
@@ -27512,6 +27727,12 @@ function createServerSessionAdapter(params) {
|
|
|
27512
27727
|
supportsThinking() {
|
|
27513
27728
|
return params.session.supportsThinking();
|
|
27514
27729
|
},
|
|
27730
|
+
get sessionName() {
|
|
27731
|
+
return params.session.sessionName;
|
|
27732
|
+
},
|
|
27733
|
+
setSessionName(name2) {
|
|
27734
|
+
params.session.setSessionName?.(name2);
|
|
27735
|
+
},
|
|
27515
27736
|
listCommands() {
|
|
27516
27737
|
const getCommands = params.extensionsResult.runtime.getCommands;
|
|
27517
27738
|
return typeof getCommands === "function" ? getCommands() : [];
|