@phren/cli 0.0.7 → 0.0.9
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/icon.svg +416 -0
- package/mcp/dist/capabilities/mcp.js +5 -5
- package/mcp/dist/capabilities/vscode.js +3 -3
- package/mcp/dist/cli-actions.js +113 -3
- package/mcp/dist/cli-namespaces.js +497 -4
- package/mcp/dist/cli.js +7 -1
- package/mcp/dist/data-tasks.js +16 -1
- package/mcp/dist/entrypoint.js +6 -0
- package/mcp/dist/init-config.js +6 -6
- package/mcp/dist/init.js +29 -7
- package/mcp/dist/mcp-finding.js +26 -1
- package/mcp/dist/mcp-tasks.js +2 -1
- package/mcp/dist/memory-ui-assets.js +37 -1
- package/mcp/dist/memory-ui-graph.js +19 -5
- package/mcp/dist/memory-ui-page.js +7 -30
- package/mcp/dist/memory-ui-scripts.js +1 -103
- package/mcp/dist/memory-ui-server.js +1 -82
- package/mcp/dist/phren-paths.js +3 -3
- package/mcp/dist/shell-input.js +4 -22
- package/mcp/dist/shell-render.js +2 -2
- package/mcp/dist/shell-view.js +1 -2
- package/mcp/dist/shell.js +0 -5
- package/mcp/dist/tool-registry.js +1 -0
- package/package.json +2 -1
- package/skills/docs.md +170 -0
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { execFileSync } from "child_process";
|
|
4
|
-
import { expandHomePath, getPhrenPath, getProjectDirs, homePath, hookConfigPath, readRootManifest, } from "./shared.js";
|
|
4
|
+
import { expandHomePath, findProjectNameCaseInsensitive, getPhrenPath, getProjectDirs, homePath, hookConfigPath, normalizeProjectNameForCreate, readRootManifest, } from "./shared.js";
|
|
5
5
|
import { isValidProjectName, errorMessage } from "./utils.js";
|
|
6
6
|
import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
|
|
7
7
|
import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "./skill-registry.js";
|
|
8
8
|
import { setSkillEnabledAndSync, syncSkillLinksForScope } from "./skill-files.js";
|
|
9
9
|
import { findProjectDir } from "./project-locator.js";
|
|
10
|
-
import { TASK_FILE_ALIASES, addTask, completeTask, updateTask, reorderTask, pinTask } from "./data-tasks.js";
|
|
10
|
+
import { TASK_FILE_ALIASES, addTask, completeTask, updateTask, reorderTask, pinTask, removeTask, workNextTask, tidyDoneTasks, linkTaskIssue, promoteTask, resolveTaskItem } from "./data-tasks.js";
|
|
11
|
+
import { buildTaskIssueBody, createGithubIssueForTask, parseGithubIssueUrl, resolveProjectGithubRepo } from "./tasks-github.js";
|
|
11
12
|
import { PROJECT_HOOK_EVENTS, PROJECT_OWNERSHIP_MODES, isProjectHookEnabled, parseProjectOwnershipMode, readProjectConfig, writeProjectConfig, writeProjectHookConfig, } from "./project-config.js";
|
|
12
13
|
import { addFinding, removeFinding } from "./core-finding.js";
|
|
13
|
-
import { supersedeFinding, retractFinding } from "./finding-lifecycle.js";
|
|
14
|
+
import { supersedeFinding, retractFinding, resolveFindingContradiction } from "./finding-lifecycle.js";
|
|
14
15
|
import { readCustomHooks, getHookTarget, HOOK_EVENT_VALUES } from "./hooks.js";
|
|
15
16
|
import { runtimeFile } from "./shared.js";
|
|
16
17
|
const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
|
|
@@ -566,6 +567,10 @@ export async function handleProjectsNamespace(args, profile) {
|
|
|
566
567
|
console.log(" phren projects configure <name> Update per-project enrollment settings");
|
|
567
568
|
console.log(" flags: --ownership=<mode> --hooks=on|off");
|
|
568
569
|
console.log(" phren projects remove <name> Remove a project (asks for confirmation)");
|
|
570
|
+
console.log(" phren projects export <name> Export project data as JSON to stdout");
|
|
571
|
+
console.log(" phren projects import <file> Import project from a JSON file");
|
|
572
|
+
console.log(" phren projects archive <name> Archive a project (removes from active index)");
|
|
573
|
+
console.log(" phren projects unarchive <name> Restore an archived project");
|
|
569
574
|
return;
|
|
570
575
|
}
|
|
571
576
|
return handleProjectsList(profile);
|
|
@@ -630,8 +635,203 @@ export async function handleProjectsNamespace(args, profile) {
|
|
|
630
635
|
console.log(`Updated ${name}: ${updates.join(", ")}`);
|
|
631
636
|
return;
|
|
632
637
|
}
|
|
638
|
+
if (subcommand === "export") {
|
|
639
|
+
const name = args[1];
|
|
640
|
+
if (!name) {
|
|
641
|
+
console.error("Usage: phren projects export <name>");
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
if (!isValidProjectName(name)) {
|
|
645
|
+
console.error(`Invalid project name: "${name}".`);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
const phrenPath = getPhrenPath();
|
|
649
|
+
const projectDir = path.join(phrenPath, name);
|
|
650
|
+
if (!fs.existsSync(projectDir)) {
|
|
651
|
+
console.error(`Project "${name}" not found.`);
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
const { readFindings, readTasks, resolveTaskFilePath, TASKS_FILENAME } = await import("./data-access.js");
|
|
655
|
+
const exported = { project: name, exportedAt: new Date().toISOString(), version: 1 };
|
|
656
|
+
const summaryPath = path.join(projectDir, "summary.md");
|
|
657
|
+
if (fs.existsSync(summaryPath))
|
|
658
|
+
exported.summary = fs.readFileSync(summaryPath, "utf8");
|
|
659
|
+
const learningsResult = readFindings(phrenPath, name);
|
|
660
|
+
if (learningsResult.ok)
|
|
661
|
+
exported.learnings = learningsResult.data;
|
|
662
|
+
const findingsPath = path.join(projectDir, "FINDINGS.md");
|
|
663
|
+
if (fs.existsSync(findingsPath))
|
|
664
|
+
exported.findingsRaw = fs.readFileSync(findingsPath, "utf8");
|
|
665
|
+
const taskResult = readTasks(phrenPath, name);
|
|
666
|
+
if (taskResult.ok) {
|
|
667
|
+
exported.task = taskResult.data.items;
|
|
668
|
+
const taskRawPath = resolveTaskFilePath(phrenPath, name);
|
|
669
|
+
if (taskRawPath && fs.existsSync(taskRawPath))
|
|
670
|
+
exported.taskRaw = fs.readFileSync(taskRawPath, "utf8");
|
|
671
|
+
}
|
|
672
|
+
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
673
|
+
if (fs.existsSync(claudePath))
|
|
674
|
+
exported.claudeMd = fs.readFileSync(claudePath, "utf8");
|
|
675
|
+
process.stdout.write(JSON.stringify(exported, null, 2) + "\n");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (subcommand === "import") {
|
|
679
|
+
const filePath = args[1];
|
|
680
|
+
if (!filePath) {
|
|
681
|
+
console.error("Usage: phren projects import <file>");
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
const resolvedPath = path.resolve(expandHomePath(filePath));
|
|
685
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
686
|
+
console.error(`File not found: ${resolvedPath}`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
let rawData;
|
|
690
|
+
try {
|
|
691
|
+
rawData = fs.readFileSync(resolvedPath, "utf8");
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
console.error(`Failed to read file: ${errorMessage(err)}`);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
let decoded;
|
|
698
|
+
try {
|
|
699
|
+
decoded = JSON.parse(rawData);
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
console.error("Invalid JSON in file.");
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
if (!decoded || typeof decoded !== "object" || !decoded.project) {
|
|
706
|
+
console.error("Invalid import payload: missing project field.");
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
const { TASKS_FILENAME } = await import("./data-access.js");
|
|
710
|
+
const phrenPath = getPhrenPath();
|
|
711
|
+
const projectName = normalizeProjectNameForCreate(String(decoded.project));
|
|
712
|
+
if (!isValidProjectName(projectName)) {
|
|
713
|
+
console.error(`Invalid project name: "${decoded.project}".`);
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
716
|
+
const existingProject = findProjectNameCaseInsensitive(phrenPath, projectName);
|
|
717
|
+
if (existingProject && existingProject !== projectName) {
|
|
718
|
+
console.error(`Project "${existingProject}" already exists with different casing. Refusing to import "${projectName}".`);
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
const projectDir = path.join(phrenPath, projectName);
|
|
722
|
+
if (fs.existsSync(projectDir)) {
|
|
723
|
+
console.error(`Project "${projectName}" already exists. Remove it first or use the MCP tool with overwrite:true.`);
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
const imported = [];
|
|
727
|
+
const stagingRoot = fs.mkdtempSync(path.join(phrenPath, `.phren-import-${projectName}-`));
|
|
728
|
+
const stagedProjectDir = path.join(stagingRoot, projectName);
|
|
729
|
+
try {
|
|
730
|
+
fs.mkdirSync(stagedProjectDir, { recursive: true });
|
|
731
|
+
if (typeof decoded.summary === "string") {
|
|
732
|
+
fs.writeFileSync(path.join(stagedProjectDir, "summary.md"), decoded.summary);
|
|
733
|
+
imported.push("summary.md");
|
|
734
|
+
}
|
|
735
|
+
if (typeof decoded.claudeMd === "string") {
|
|
736
|
+
fs.writeFileSync(path.join(stagedProjectDir, "CLAUDE.md"), decoded.claudeMd);
|
|
737
|
+
imported.push("CLAUDE.md");
|
|
738
|
+
}
|
|
739
|
+
if (typeof decoded.findingsRaw === "string") {
|
|
740
|
+
fs.writeFileSync(path.join(stagedProjectDir, "FINDINGS.md"), decoded.findingsRaw);
|
|
741
|
+
imported.push("FINDINGS.md");
|
|
742
|
+
}
|
|
743
|
+
else if (Array.isArray(decoded.learnings) && decoded.learnings.length > 0) {
|
|
744
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
745
|
+
const lines = [`# ${projectName} Findings`, "", `## ${date}`, ""];
|
|
746
|
+
for (const item of decoded.learnings) {
|
|
747
|
+
if (item.text)
|
|
748
|
+
lines.push(`- ${item.text}`);
|
|
749
|
+
}
|
|
750
|
+
lines.push("");
|
|
751
|
+
fs.writeFileSync(path.join(stagedProjectDir, "FINDINGS.md"), lines.join("\n"));
|
|
752
|
+
imported.push("FINDINGS.md");
|
|
753
|
+
}
|
|
754
|
+
if (typeof decoded.taskRaw === "string") {
|
|
755
|
+
fs.writeFileSync(path.join(stagedProjectDir, TASKS_FILENAME), decoded.taskRaw);
|
|
756
|
+
imported.push(TASKS_FILENAME);
|
|
757
|
+
}
|
|
758
|
+
fs.renameSync(stagedProjectDir, projectDir);
|
|
759
|
+
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
760
|
+
console.log(`Imported project "${projectName}": ${imported.join(", ") || "(no files)"}`);
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
try {
|
|
764
|
+
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
765
|
+
}
|
|
766
|
+
catch { /* best-effort */ }
|
|
767
|
+
console.error(`Import failed: ${errorMessage(err)}`);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (subcommand === "archive" || subcommand === "unarchive") {
|
|
773
|
+
const name = args[1];
|
|
774
|
+
if (!name) {
|
|
775
|
+
console.error(`Usage: phren projects ${subcommand} <name>`);
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
if (!isValidProjectName(name)) {
|
|
779
|
+
console.error(`Invalid project name: "${name}".`);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
const phrenPath = getPhrenPath();
|
|
783
|
+
const projectDir = path.join(phrenPath, name);
|
|
784
|
+
const archiveDir = path.join(phrenPath, `${name}.archived`);
|
|
785
|
+
if (subcommand === "archive") {
|
|
786
|
+
if (!fs.existsSync(projectDir)) {
|
|
787
|
+
console.error(`Project "${name}" not found.`);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
if (fs.existsSync(archiveDir)) {
|
|
791
|
+
console.error(`Archive "${name}.archived" already exists. Unarchive or remove it first.`);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
fs.renameSync(projectDir, archiveDir);
|
|
796
|
+
console.log(`Archived project "${name}". Data preserved at ${archiveDir}.`);
|
|
797
|
+
console.log("Note: the search index will be updated on next search.");
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
console.error(`Archive failed: ${errorMessage(err)}`);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
if (fs.existsSync(projectDir)) {
|
|
806
|
+
console.error(`Project "${name}" already exists as an active project.`);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
if (!fs.existsSync(archiveDir)) {
|
|
810
|
+
const available = fs.readdirSync(phrenPath)
|
|
811
|
+
.filter((e) => e.endsWith(".archived"))
|
|
812
|
+
.map((e) => e.replace(/\.archived$/, ""));
|
|
813
|
+
if (available.length > 0) {
|
|
814
|
+
console.error(`No archive found for "${name}". Available archives: ${available.join(", ")}`);
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
console.error(`No archive found for "${name}".`);
|
|
818
|
+
}
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
fs.renameSync(archiveDir, projectDir);
|
|
823
|
+
console.log(`Unarchived project "${name}". It is now active again.`);
|
|
824
|
+
console.log("Note: the search index will be updated on next search.");
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
console.error(`Unarchive failed: ${errorMessage(err)}`);
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
633
833
|
console.error(`Unknown subcommand: ${subcommand}`);
|
|
634
|
-
console.error("Usage: phren projects [list|configure|remove]");
|
|
834
|
+
console.error("Usage: phren projects [list|configure|remove|export|import|archive|unarchive]");
|
|
635
835
|
process.exit(1);
|
|
636
836
|
}
|
|
637
837
|
function handleProjectsList(profile) {
|
|
@@ -717,6 +917,13 @@ function printTaskUsage() {
|
|
|
717
917
|
console.log("Usage:");
|
|
718
918
|
console.log(' phren task add <project> "<text>"');
|
|
719
919
|
console.log(' phren task complete <project> "<text>"');
|
|
920
|
+
console.log(' phren task remove <project> "<text>"');
|
|
921
|
+
console.log(' phren task next [project]');
|
|
922
|
+
console.log(' phren task promote <project> "<text>" [--active]');
|
|
923
|
+
console.log(' phren task tidy [project] [--keep=<n>] [--dry-run]');
|
|
924
|
+
console.log(' phren task link <project> "<text>" --issue <number> [--url <url>]');
|
|
925
|
+
console.log(' phren task link <project> "<text>" --unlink');
|
|
926
|
+
console.log(' phren task create-issue <project> "<text>" [--repo <owner/name>] [--title "<title>"] [--done]');
|
|
720
927
|
console.log(' phren task update <project> "<text>" [--priority=high|medium|low] [--section=Active|Queue|Done] [--context="..."]');
|
|
721
928
|
console.log(' phren task pin <project> "<text>"');
|
|
722
929
|
console.log(' phren task reorder <project> "<text>" --rank=<n>');
|
|
@@ -843,6 +1050,231 @@ export async function handleTaskNamespace(args) {
|
|
|
843
1050
|
console.log(result.data);
|
|
844
1051
|
return;
|
|
845
1052
|
}
|
|
1053
|
+
if (subcommand === "remove") {
|
|
1054
|
+
const project = args[1];
|
|
1055
|
+
const match = args.slice(2).join(" ");
|
|
1056
|
+
if (!project || !match) {
|
|
1057
|
+
console.error('Usage: phren task remove <project> "<text>"');
|
|
1058
|
+
process.exit(1);
|
|
1059
|
+
}
|
|
1060
|
+
const result = removeTask(getPhrenPath(), project, match);
|
|
1061
|
+
if (!result.ok) {
|
|
1062
|
+
console.error(result.error);
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
console.log(result.data);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (subcommand === "next") {
|
|
1069
|
+
const project = args[1];
|
|
1070
|
+
if (!project) {
|
|
1071
|
+
console.error("Usage: phren task next <project>");
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
const result = workNextTask(getPhrenPath(), project);
|
|
1075
|
+
if (!result.ok) {
|
|
1076
|
+
console.error(result.error);
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
console.log(result.data);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (subcommand === "promote") {
|
|
1083
|
+
const project = args[1];
|
|
1084
|
+
if (!project) {
|
|
1085
|
+
printTaskUsage();
|
|
1086
|
+
process.exit(1);
|
|
1087
|
+
}
|
|
1088
|
+
const positional = [];
|
|
1089
|
+
let moveToActive = false;
|
|
1090
|
+
for (const arg of args.slice(2)) {
|
|
1091
|
+
if (arg === "--active") {
|
|
1092
|
+
moveToActive = true;
|
|
1093
|
+
}
|
|
1094
|
+
else if (!arg.startsWith("--")) {
|
|
1095
|
+
positional.push(arg);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const match = positional.join(" ");
|
|
1099
|
+
if (!match) {
|
|
1100
|
+
console.error('Usage: phren task promote <project> "<text>" [--active]');
|
|
1101
|
+
process.exit(1);
|
|
1102
|
+
}
|
|
1103
|
+
const result = promoteTask(getPhrenPath(), project, match, moveToActive);
|
|
1104
|
+
if (!result.ok) {
|
|
1105
|
+
console.error(result.error);
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
}
|
|
1108
|
+
console.log(`Promoted task "${result.data.line}" in ${project}${moveToActive ? " (moved to Active)" : ""}.`);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (subcommand === "tidy") {
|
|
1112
|
+
const project = args[1];
|
|
1113
|
+
if (!project) {
|
|
1114
|
+
console.error("Usage: phren task tidy <project> [--keep=<n>] [--dry-run]");
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
let keep = 30;
|
|
1118
|
+
let dryRun = false;
|
|
1119
|
+
for (const arg of args.slice(2)) {
|
|
1120
|
+
if (arg.startsWith("--keep=")) {
|
|
1121
|
+
const n = Number.parseInt(arg.slice("--keep=".length), 10);
|
|
1122
|
+
if (Number.isFinite(n) && n > 0)
|
|
1123
|
+
keep = n;
|
|
1124
|
+
}
|
|
1125
|
+
else if (arg === "--dry-run") {
|
|
1126
|
+
dryRun = true;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const result = tidyDoneTasks(getPhrenPath(), project, keep, dryRun);
|
|
1130
|
+
if (!result.ok) {
|
|
1131
|
+
console.error(result.error);
|
|
1132
|
+
process.exit(1);
|
|
1133
|
+
}
|
|
1134
|
+
console.log(result.data);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (subcommand === "link") {
|
|
1138
|
+
const project = args[1];
|
|
1139
|
+
if (!project) {
|
|
1140
|
+
printTaskUsage();
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
const positional = [];
|
|
1144
|
+
let issueArg;
|
|
1145
|
+
let urlArg;
|
|
1146
|
+
let unlink = false;
|
|
1147
|
+
const rest = args.slice(2);
|
|
1148
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1149
|
+
const arg = rest[i];
|
|
1150
|
+
if (arg === "--issue" || arg === "-i") {
|
|
1151
|
+
issueArg = rest[++i];
|
|
1152
|
+
}
|
|
1153
|
+
else if (arg.startsWith("--issue=")) {
|
|
1154
|
+
issueArg = arg.slice("--issue=".length);
|
|
1155
|
+
}
|
|
1156
|
+
else if (arg === "--url") {
|
|
1157
|
+
urlArg = rest[++i];
|
|
1158
|
+
}
|
|
1159
|
+
else if (arg.startsWith("--url=")) {
|
|
1160
|
+
urlArg = arg.slice("--url=".length);
|
|
1161
|
+
}
|
|
1162
|
+
else if (arg === "--unlink") {
|
|
1163
|
+
unlink = true;
|
|
1164
|
+
}
|
|
1165
|
+
else if (!arg.startsWith("--")) {
|
|
1166
|
+
positional.push(arg);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const match = positional.join(" ");
|
|
1170
|
+
if (!match) {
|
|
1171
|
+
console.error('Usage: phren task link <project> "<text>" --issue <number>');
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|
|
1174
|
+
if (!unlink && !issueArg && !urlArg) {
|
|
1175
|
+
console.error("Provide --issue <number> or --url <url> to link, or --unlink to remove the link.");
|
|
1176
|
+
process.exit(1);
|
|
1177
|
+
}
|
|
1178
|
+
if (urlArg) {
|
|
1179
|
+
const parsed = parseGithubIssueUrl(urlArg);
|
|
1180
|
+
if (!parsed) {
|
|
1181
|
+
console.error("--url must be a valid GitHub issue URL.");
|
|
1182
|
+
process.exit(1);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const result = linkTaskIssue(getPhrenPath(), project, match, {
|
|
1186
|
+
github_issue: issueArg,
|
|
1187
|
+
github_url: urlArg,
|
|
1188
|
+
unlink: unlink,
|
|
1189
|
+
});
|
|
1190
|
+
if (!result.ok) {
|
|
1191
|
+
console.error(result.error);
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
if (unlink) {
|
|
1195
|
+
console.log(`Removed GitHub link from ${project} task.`);
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
console.log(`Linked ${project} task to ${result.data.githubIssue ? `#${result.data.githubIssue}` : result.data.githubUrl}.`);
|
|
1199
|
+
}
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (subcommand === "create-issue") {
|
|
1203
|
+
const project = args[1];
|
|
1204
|
+
if (!project) {
|
|
1205
|
+
printTaskUsage();
|
|
1206
|
+
process.exit(1);
|
|
1207
|
+
}
|
|
1208
|
+
const positional = [];
|
|
1209
|
+
let repoArg;
|
|
1210
|
+
let titleArg;
|
|
1211
|
+
let markDone = false;
|
|
1212
|
+
const rest = args.slice(2);
|
|
1213
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1214
|
+
const arg = rest[i];
|
|
1215
|
+
if (arg === "--repo") {
|
|
1216
|
+
repoArg = rest[++i];
|
|
1217
|
+
}
|
|
1218
|
+
else if (arg.startsWith("--repo=")) {
|
|
1219
|
+
repoArg = arg.slice("--repo=".length);
|
|
1220
|
+
}
|
|
1221
|
+
else if (arg === "--title") {
|
|
1222
|
+
titleArg = rest[++i];
|
|
1223
|
+
}
|
|
1224
|
+
else if (arg.startsWith("--title=")) {
|
|
1225
|
+
titleArg = arg.slice("--title=".length);
|
|
1226
|
+
}
|
|
1227
|
+
else if (arg === "--done") {
|
|
1228
|
+
markDone = true;
|
|
1229
|
+
}
|
|
1230
|
+
else if (!arg.startsWith("--")) {
|
|
1231
|
+
positional.push(arg);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const match = positional.join(" ");
|
|
1235
|
+
if (!match) {
|
|
1236
|
+
console.error('Usage: phren task create-issue <project> "<text>" [--repo <owner/name>] [--title "<title>"] [--done]');
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
const phrenPath = getPhrenPath();
|
|
1240
|
+
const resolved = resolveTaskItem(phrenPath, project, match);
|
|
1241
|
+
if (!resolved.ok) {
|
|
1242
|
+
console.error(resolved.error);
|
|
1243
|
+
process.exit(1);
|
|
1244
|
+
}
|
|
1245
|
+
const targetRepo = repoArg || resolveProjectGithubRepo(phrenPath, project);
|
|
1246
|
+
if (!targetRepo) {
|
|
1247
|
+
console.error("Could not infer a GitHub repo. Provide --repo <owner/name> or add a GitHub URL to CLAUDE.md/summary.md.");
|
|
1248
|
+
process.exit(1);
|
|
1249
|
+
}
|
|
1250
|
+
const created = createGithubIssueForTask({
|
|
1251
|
+
repo: targetRepo,
|
|
1252
|
+
title: titleArg?.trim() || resolved.data.line.replace(/\s*\[(high|medium|low)\]\s*$/i, "").trim(),
|
|
1253
|
+
body: buildTaskIssueBody(project, resolved.data),
|
|
1254
|
+
});
|
|
1255
|
+
if (!created.ok) {
|
|
1256
|
+
console.error(created.error);
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
}
|
|
1259
|
+
const linked = linkTaskIssue(phrenPath, project, resolved.data.stableId ? `bid:${resolved.data.stableId}` : resolved.data.id, {
|
|
1260
|
+
github_issue: created.data.issueNumber,
|
|
1261
|
+
github_url: created.data.url,
|
|
1262
|
+
});
|
|
1263
|
+
if (!linked.ok) {
|
|
1264
|
+
console.error(linked.error);
|
|
1265
|
+
process.exit(1);
|
|
1266
|
+
}
|
|
1267
|
+
if (markDone) {
|
|
1268
|
+
const completionMatch = linked.data.stableId ? `bid:${linked.data.stableId}` : linked.data.id;
|
|
1269
|
+
const completed = completeTask(phrenPath, project, completionMatch);
|
|
1270
|
+
if (!completed.ok) {
|
|
1271
|
+
console.error(completed.error);
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
console.log(`Created GitHub issue ${created.data.issueNumber ? `#${created.data.issueNumber}` : created.data.url} for ${project} task.`);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
846
1278
|
console.error(`Unknown task subcommand: ${subcommand}`);
|
|
847
1279
|
printTaskUsage();
|
|
848
1280
|
process.exit(1);
|
|
@@ -854,6 +1286,8 @@ function printFindingUsage() {
|
|
|
854
1286
|
console.log(' phren finding remove <project> "<text>"');
|
|
855
1287
|
console.log(' phren finding supersede <project> "<text>" --by "<newer guidance>"');
|
|
856
1288
|
console.log(' phren finding retract <project> "<text>" --reason "<reason>"');
|
|
1289
|
+
console.log(' phren finding contradictions [project]');
|
|
1290
|
+
console.log(' phren finding resolve <project> "<finding_text>" "<other_text>" <keep_a|keep_b|keep_both|retract_both>');
|
|
857
1291
|
}
|
|
858
1292
|
export async function handleFindingNamespace(args) {
|
|
859
1293
|
const subcommand = args[0];
|
|
@@ -983,6 +1417,65 @@ export async function handleFindingNamespace(args) {
|
|
|
983
1417
|
console.log(`Finding retracted: "${result.data.finding}" (reason: ${result.data.reason})`);
|
|
984
1418
|
return;
|
|
985
1419
|
}
|
|
1420
|
+
if (subcommand === "contradictions") {
|
|
1421
|
+
const project = args[1];
|
|
1422
|
+
const phrenPath = getPhrenPath();
|
|
1423
|
+
const RESERVED_DIRS = new Set(["global", ".runtime", ".sessions", ".governance"]);
|
|
1424
|
+
const { readFindings } = await import("./data-access.js");
|
|
1425
|
+
const projects = project
|
|
1426
|
+
? [project]
|
|
1427
|
+
: fs.readdirSync(phrenPath, { withFileTypes: true })
|
|
1428
|
+
.filter((entry) => entry.isDirectory() && !RESERVED_DIRS.has(entry.name) && isValidProjectName(entry.name))
|
|
1429
|
+
.map((entry) => entry.name);
|
|
1430
|
+
const contradictions = [];
|
|
1431
|
+
for (const p of projects) {
|
|
1432
|
+
const result = readFindings(phrenPath, p);
|
|
1433
|
+
if (!result.ok)
|
|
1434
|
+
continue;
|
|
1435
|
+
for (const finding of result.data) {
|
|
1436
|
+
if (finding.status !== "contradicted")
|
|
1437
|
+
continue;
|
|
1438
|
+
contradictions.push({ project: p, id: finding.id, text: finding.text, date: finding.date, status_ref: finding.status_ref });
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (!contradictions.length) {
|
|
1442
|
+
console.log("No unresolved contradictions found.");
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
console.log(`${contradictions.length} unresolved contradiction(s):\n`);
|
|
1446
|
+
for (const c of contradictions) {
|
|
1447
|
+
console.log(`[${c.project}] ${c.date} ${c.id}`);
|
|
1448
|
+
console.log(` ${c.text}`);
|
|
1449
|
+
if (c.status_ref)
|
|
1450
|
+
console.log(` contradicts: ${c.status_ref}`);
|
|
1451
|
+
console.log("");
|
|
1452
|
+
}
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (subcommand === "resolve") {
|
|
1456
|
+
const project = args[1];
|
|
1457
|
+
const findingText = args[2];
|
|
1458
|
+
const otherText = args[3];
|
|
1459
|
+
const resolution = args[4];
|
|
1460
|
+
const validResolutions = ["keep_a", "keep_b", "keep_both", "retract_both"];
|
|
1461
|
+
if (!project || !findingText || !otherText || !resolution) {
|
|
1462
|
+
console.error('Usage: phren finding resolve <project> "<finding_text>" "<other_text>" <keep_a|keep_b|keep_both|retract_both>');
|
|
1463
|
+
process.exit(1);
|
|
1464
|
+
}
|
|
1465
|
+
if (!validResolutions.includes(resolution)) {
|
|
1466
|
+
console.error(`Invalid resolution "${resolution}". Valid values: ${validResolutions.join(", ")}`);
|
|
1467
|
+
process.exit(1);
|
|
1468
|
+
}
|
|
1469
|
+
const result = resolveFindingContradiction(getPhrenPath(), project, findingText, otherText, resolution);
|
|
1470
|
+
if (!result.ok) {
|
|
1471
|
+
console.error(result.error);
|
|
1472
|
+
process.exit(1);
|
|
1473
|
+
}
|
|
1474
|
+
console.log(`Resolved contradiction in "${project}" with "${resolution}".`);
|
|
1475
|
+
console.log(` finding_a: ${result.data.finding_a.text} → ${result.data.finding_a.status}`);
|
|
1476
|
+
console.log(` finding_b: ${result.data.finding_b.text} → ${result.data.finding_b.status}`);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
986
1479
|
console.error(`Unknown finding subcommand: ${subcommand}`);
|
|
987
1480
|
printFindingUsage();
|
|
988
1481
|
process.exit(1);
|
package/mcp/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { handleConfig, handleIndexPolicy, handleRetentionPolicy, handleWorkflowP
|
|
|
9
9
|
import { parseSearchArgs } from "./cli-search.js";
|
|
10
10
|
import { handleDetectSkills, handleFindingNamespace, handleHooksNamespace, handleProjectsNamespace, handleSkillsNamespace, handleSkillList, handleTaskNamespace, } from "./cli-namespaces.js";
|
|
11
11
|
import { handleTaskView, handleSessionsView, handleQuickstart, handleDebugInjection, handleInspectIndex, } from "./cli-ops.js";
|
|
12
|
-
import { handleAddFinding, handleDoctor, handleFragmentSearch, handleMemoryUi, handlePinCanonical, handleQualityFeedback, handleRelatedDocs, handleSearch, handleShell, handleStatus, handleUpdate, } from "./cli-actions.js";
|
|
12
|
+
import { handleAddFinding, handleDoctor, handleFragmentSearch, handleMemoryUi, handlePinCanonical, handleQualityFeedback, handleRelatedDocs, handleReview, handleConsolidationStatus, handleSessionContext, handleSearch, handleShell, handleStatus, handleUpdate, } from "./cli-actions.js";
|
|
13
13
|
import { handleGraphNamespace } from "./cli-graph.js";
|
|
14
14
|
import { resolveRuntimeProfile } from "./runtime-profile.js";
|
|
15
15
|
// ── CLI router ───────────────────────────────────────────────────────────────
|
|
@@ -101,6 +101,12 @@ export async function runCliCommand(command, args) {
|
|
|
101
101
|
return handleDetectSkills(args, getProfile());
|
|
102
102
|
case "graph":
|
|
103
103
|
return handleGraphNamespace(args);
|
|
104
|
+
case "review":
|
|
105
|
+
return handleReview(args);
|
|
106
|
+
case "consolidation-status":
|
|
107
|
+
return handleConsolidationStatus(args);
|
|
108
|
+
case "session-context":
|
|
109
|
+
return handleSessionContext();
|
|
104
110
|
default:
|
|
105
111
|
console.error(`Unknown command: ${command}`);
|
|
106
112
|
process.exit(1);
|
package/mcp/dist/data-tasks.js
CHANGED
|
@@ -29,6 +29,12 @@ function normalizePriority(text) {
|
|
|
29
29
|
return undefined;
|
|
30
30
|
return m[1].toLowerCase();
|
|
31
31
|
}
|
|
32
|
+
function stripPriorityTag(text) {
|
|
33
|
+
return text
|
|
34
|
+
.replace(/\s*\[(high|medium|low)\](?=\s*(?:\[pinned\])?\s*$)/gi, "")
|
|
35
|
+
.replace(/\s{2,}/g, " ")
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
32
38
|
function detectPinned(text) {
|
|
33
39
|
return /\[pinned\]/i.test(text);
|
|
34
40
|
}
|
|
@@ -537,11 +543,20 @@ export function updateTask(phrenPath, project, match, updates) {
|
|
|
537
543
|
return taskItemNotFound(project, match);
|
|
538
544
|
const item = parsed.data.items[found.match.section][found.match.index];
|
|
539
545
|
const changes = [];
|
|
546
|
+
if (updates.text !== undefined) {
|
|
547
|
+
const nextText = updates.text.trim();
|
|
548
|
+
if (!nextText)
|
|
549
|
+
return phrenErr("Task text cannot be empty.", PhrenError.EMPTY_INPUT);
|
|
550
|
+
item.line = nextText;
|
|
551
|
+
item.priority = normalizePriority(nextText);
|
|
552
|
+
item.pinned = detectPinned(nextText) || undefined;
|
|
553
|
+
changes.push("text updated");
|
|
554
|
+
}
|
|
540
555
|
if (updates.priority) {
|
|
541
556
|
const priority = updates.priority.toLowerCase();
|
|
542
557
|
if (["high", "medium", "low"].includes(priority)) {
|
|
543
558
|
item.priority = priority;
|
|
544
|
-
item.line = item.line
|
|
559
|
+
item.line = stripPriorityTag(item.line);
|
|
545
560
|
item.line = `${item.line} [${item.priority}]`;
|
|
546
561
|
changes.push(`priority -> ${priority}`);
|
|
547
562
|
}
|
package/mcp/dist/entrypoint.js
CHANGED
|
@@ -28,6 +28,9 @@ Usage:
|
|
|
28
28
|
phren hooks enable <tool> Enable hooks for one tool
|
|
29
29
|
phren hooks disable <tool> Disable hooks for one tool
|
|
30
30
|
phren status Health, active project, stats
|
|
31
|
+
phren review [project] Show review queue items with date, confidence, text
|
|
32
|
+
phren consolidation-status [project] Check if findings need consolidation
|
|
33
|
+
phren session-context Show current session state (project, duration, findings)
|
|
31
34
|
phren search <query> [--project <n>] [--type <t>] [--limit <n>]
|
|
32
35
|
Search what phren remembers
|
|
33
36
|
phren add-finding <project> "..." Tell phren what you learned
|
|
@@ -143,6 +146,9 @@ const CLI_COMMANDS = [
|
|
|
143
146
|
"policy",
|
|
144
147
|
"workflow",
|
|
145
148
|
"access",
|
|
149
|
+
"review",
|
|
150
|
+
"consolidation-status",
|
|
151
|
+
"session-context",
|
|
146
152
|
];
|
|
147
153
|
async function flushTopLevelOutput() {
|
|
148
154
|
await Promise.all([
|
package/mcp/dist/init-config.js
CHANGED
|
@@ -162,15 +162,15 @@ export function removeMcpServerAtPath(filePath) {
|
|
|
162
162
|
return removed;
|
|
163
163
|
}
|
|
164
164
|
export function isPhrenCommand(command) {
|
|
165
|
-
// Detect PHREN_PATH=
|
|
166
|
-
if (/\
|
|
165
|
+
// Detect PHREN_PATH= env var prefix (present in all lifecycle hook commands)
|
|
166
|
+
if (/\bPHREN_PATH=/.test(command))
|
|
167
167
|
return true;
|
|
168
|
-
// Detect npx phren
|
|
169
|
-
if (command.includes("phren")
|
|
168
|
+
// Detect npx phren package references
|
|
169
|
+
if (command.includes("phren"))
|
|
170
170
|
return true;
|
|
171
|
-
// Detect bare "phren"
|
|
171
|
+
// Detect bare "phren" executable segment
|
|
172
172
|
const segments = command.split(/[/\\\s]+/);
|
|
173
|
-
if (segments.some(seg => seg === "phren" || seg.startsWith("phren@")
|
|
173
|
+
if (segments.some(seg => seg === "phren" || seg.startsWith("phren@")))
|
|
174
174
|
return true;
|
|
175
175
|
// Also match commands that include hook subcommands (used when installed via absolute path)
|
|
176
176
|
const HOOK_MARKERS = ["hook-prompt", "hook-stop", "hook-session-start", "hook-tool"];
|