@phren/cli 0.0.10 → 0.0.12

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.
Files changed (100) hide show
  1. package/README.md +11 -17
  2. package/mcp/dist/capabilities/cli.js +1 -1
  3. package/mcp/dist/capabilities/mcp.js +1 -1
  4. package/mcp/dist/capabilities/vscode.js +1 -1
  5. package/mcp/dist/capabilities/web-ui.js +1 -1
  6. package/mcp/dist/cli-actions.js +58 -71
  7. package/mcp/dist/cli-config.js +337 -131
  8. package/mcp/dist/cli-extract.js +3 -2
  9. package/mcp/dist/cli-govern.js +35 -63
  10. package/mcp/dist/cli-graph.js +19 -4
  11. package/mcp/dist/cli-hooks-globs.js +2 -1
  12. package/mcp/dist/cli-hooks-output.js +4 -4
  13. package/mcp/dist/cli-hooks-session.js +1 -1
  14. package/mcp/dist/cli-hooks.js +44 -35
  15. package/mcp/dist/cli-namespaces.js +15 -5
  16. package/mcp/dist/cli-search.js +2 -2
  17. package/mcp/dist/cli.js +1 -1
  18. package/mcp/dist/content-archive.js +23 -14
  19. package/mcp/dist/content-citation.js +13 -2
  20. package/mcp/dist/content-dedup.js +9 -9
  21. package/mcp/dist/content-learning.js +6 -4
  22. package/mcp/dist/content-metadata.js +10 -0
  23. package/mcp/dist/core-finding.js +1 -1
  24. package/mcp/dist/data-access.js +10 -31
  25. package/mcp/dist/data-tasks.js +5 -26
  26. package/mcp/dist/embedding.js +7 -8
  27. package/mcp/dist/entrypoint.js +133 -102
  28. package/mcp/dist/finding-impact.js +1 -32
  29. package/mcp/dist/finding-journal.js +1 -1
  30. package/mcp/dist/finding-lifecycle.js +2 -7
  31. package/mcp/dist/governance-locks.js +12 -5
  32. package/mcp/dist/governance-policy.js +156 -9
  33. package/mcp/dist/governance-scores.js +4 -10
  34. package/mcp/dist/hooks.js +62 -18
  35. package/mcp/dist/index.js +4 -4
  36. package/mcp/dist/init-config.js +4 -25
  37. package/mcp/dist/init-preferences.js +1 -1
  38. package/mcp/dist/init-setup.js +6 -55
  39. package/mcp/dist/init-shared.js +53 -1
  40. package/mcp/dist/init.js +191 -29
  41. package/mcp/dist/link-checksums.js +3 -2
  42. package/mcp/dist/link-context.js +2 -2
  43. package/mcp/dist/link-doctor.js +14 -57
  44. package/mcp/dist/link-skills.js +98 -12
  45. package/mcp/dist/link.js +16 -75
  46. package/mcp/dist/machine-identity.js +1 -9
  47. package/mcp/dist/mcp-config.js +247 -42
  48. package/mcp/dist/mcp-data.js +9 -9
  49. package/mcp/dist/mcp-extract-facts.js +12 -7
  50. package/mcp/dist/mcp-extract.js +2 -2
  51. package/mcp/dist/mcp-finding.js +16 -20
  52. package/mcp/dist/mcp-graph.js +12 -12
  53. package/mcp/dist/mcp-hooks.js +1 -1
  54. package/mcp/dist/mcp-ops.js +18 -18
  55. package/mcp/dist/mcp-search.js +11 -16
  56. package/mcp/dist/mcp-session.js +12 -2
  57. package/mcp/dist/memory-ui-assets.js +1 -36
  58. package/mcp/dist/memory-ui-graph.js +152 -50
  59. package/mcp/dist/memory-ui-page.js +30 -5
  60. package/mcp/dist/memory-ui-scripts.js +252 -63
  61. package/mcp/dist/memory-ui-server.js +115 -3
  62. package/mcp/dist/phren-core.js +2 -0
  63. package/mcp/dist/phren-paths.js +8 -9
  64. package/mcp/dist/proactivity.js +5 -5
  65. package/mcp/dist/profile-store.js +2 -2
  66. package/mcp/dist/project-config.js +64 -17
  67. package/mcp/dist/provider-adapters.js +1 -1
  68. package/mcp/dist/query-correlation.js +22 -19
  69. package/mcp/dist/session-checkpoints.js +14 -14
  70. package/mcp/dist/session-utils.js +3 -2
  71. package/mcp/dist/shared-data-utils.js +28 -0
  72. package/mcp/dist/shared-fragment-graph.js +22 -21
  73. package/mcp/dist/shared-governance.js +1 -1
  74. package/mcp/dist/shared-index.js +144 -105
  75. package/mcp/dist/shared-retrieval.js +21 -23
  76. package/mcp/dist/shared-search-fallback.js +15 -25
  77. package/mcp/dist/shared-sqljs.js +3 -2
  78. package/mcp/dist/shared.js +5 -6
  79. package/mcp/dist/shell-entry.js +1 -1
  80. package/mcp/dist/shell-input.js +63 -53
  81. package/mcp/dist/shell-palette.js +6 -1
  82. package/mcp/dist/shell-render.js +9 -5
  83. package/mcp/dist/shell-state-store.js +2 -5
  84. package/mcp/dist/shell-view.js +7 -6
  85. package/mcp/dist/shell.js +5 -55
  86. package/mcp/dist/skill-files.js +4 -10
  87. package/mcp/dist/skill-registry.js +3 -0
  88. package/mcp/dist/status.js +43 -21
  89. package/mcp/dist/task-hygiene.js +1 -1
  90. package/mcp/dist/telemetry.js +5 -4
  91. package/mcp/dist/update.js +1 -1
  92. package/mcp/dist/utils.js +4 -4
  93. package/package.json +2 -3
  94. package/skills/docs.md +11 -11
  95. package/starter/README.md +1 -1
  96. package/starter/global/CLAUDE.md +2 -2
  97. package/starter/global/skills/audit.md +106 -0
  98. package/mcp/dist/cli-hooks-retrieval.js +0 -2
  99. package/mcp/dist/impact-scoring.js +0 -22
  100. package/mcp/dist/shared-paths.js +0 -1
@@ -1,22 +1,41 @@
1
- export function renderSkillUiEnhancementScript(authToken) {
1
+ /**
2
+ * Returns a <script> block with shared browser helpers used across all UI IIFEs:
3
+ * window._phrenEsc(s) — HTML-escape a value
4
+ * window._phrenAuthToken — the current auth token
5
+ * window._phrenAuthUrl(base) — append _auth param to a URL
6
+ * window._phrenAuthBody(body) — append _auth param to a form body
7
+ * window._phrenFetchCsrfToken(cb) — fetch the CSRF token and call cb(token)
8
+ */
9
+ export function renderSharedWebUiHelpers(authToken) {
10
+ return `(function() {
11
+ window._phrenAuthToken = '${authToken}';
12
+ window._phrenEsc = function(s) {
13
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
14
+ };
15
+ window._phrenAuthUrl = function(base) {
16
+ var tok = window._phrenAuthToken;
17
+ return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(tok);
18
+ };
19
+ window._phrenAuthBody = function(body) {
20
+ var tok = window._phrenAuthToken;
21
+ return body + (tok ? '&_auth=' + encodeURIComponent(tok) : '');
22
+ };
23
+ window._phrenFetchCsrfToken = function(cb) {
24
+ var tok = window._phrenAuthToken;
25
+ var url = '/api/csrf-token' + (tok ? '?_auth=' + encodeURIComponent(tok) : '');
26
+ fetch(url).then(function(r) { return r.json(); }).then(function(d) { cb(d.token || null); }).catch(function() { cb(null); });
27
+ };
28
+ })();`;
29
+ }
30
+ export function renderSkillUiEnhancementScript(_authToken) {
2
31
  return `(function() {
3
- var _skillAuthToken = '${authToken}';
4
32
  var _skillCurrent = null;
5
33
  var _skillEditing = false;
34
+ var esc = window._phrenEsc;
35
+ var authUrl = window._phrenAuthUrl;
36
+ var authBody = window._phrenAuthBody;
37
+ var fetchCsrfToken = window._phrenFetchCsrfToken;
6
38
 
7
- function esc(s) {
8
- return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
9
- }
10
- function authUrl(base) {
11
- return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(_skillAuthToken);
12
- }
13
- function authBody(body) {
14
- return body + (_skillAuthToken ? '&_auth=' + encodeURIComponent(_skillAuthToken) : '');
15
- }
16
- function fetchCsrfToken(cb) {
17
- var url = '/api/csrf-token' + (_skillAuthToken ? '?_auth=' + encodeURIComponent(_skillAuthToken) : '');
18
- fetch(url).then(function(r) { return r.json(); }).then(function(d) { cb(d.token || null); }).catch(function() { cb(null); });
19
- }
20
39
  function renderSkillReader(content) {
21
40
  var reader = document.getElementById('skills-reader');
22
41
  if (!_skillCurrent || !reader) return;
@@ -166,9 +185,8 @@ export function renderSkillUiEnhancementScript(authToken) {
166
185
  });
167
186
  })();`;
168
187
  }
169
- export function renderProjectReferenceEnhancementScript(authToken) {
188
+ export function renderProjectReferenceEnhancementScript(_authToken) {
170
189
  return `(function() {
171
- var _referenceAuthToken = '${authToken}';
172
190
  var _referenceState = {
173
191
  project: '',
174
192
  topicsData: null,
@@ -177,20 +195,11 @@ export function renderProjectReferenceEnhancementScript(authToken) {
177
195
  selectedKey: '',
178
196
  editor: null
179
197
  };
198
+ var esc = window._phrenEsc;
199
+ var authUrl = window._phrenAuthUrl;
200
+ var authBody = window._phrenAuthBody;
201
+ var fetchCsrfToken = window._phrenFetchCsrfToken;
180
202
 
181
- function esc(s) {
182
- return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
183
- }
184
- function authUrl(base) {
185
- return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(_referenceAuthToken);
186
- }
187
- function authBody(body) {
188
- return body + (_referenceAuthToken ? '&_auth=' + encodeURIComponent(_referenceAuthToken) : '');
189
- }
190
- function fetchCsrfToken(cb) {
191
- var url = '/api/csrf-token' + (_referenceAuthToken ? '?_auth=' + encodeURIComponent(_referenceAuthToken) : '');
192
- fetch(url).then(function(r) { return r.json(); }).then(function(d) { cb(d.token || null); }).catch(function() { cb(null); });
193
- }
194
203
  function currentProject() {
195
204
  var selected = document.querySelector('.project-card.selected');
196
205
  return selected ? (selected.getAttribute('data-project') || '') : '';
@@ -585,15 +594,12 @@ export function renderTasksAndSettingsScript(authToken) {
585
594
  return `(function() {
586
595
  var _tsAuthToken = '${authToken}';
587
596
  var _allTasks = [];
597
+ var esc = window._phrenEsc;
588
598
 
589
599
  function tsAuthUrl(base) {
590
600
  return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(_tsAuthToken);
591
601
  }
592
602
 
593
- function esc(s) {
594
- return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
595
- }
596
-
597
603
  function priorityBadge(p) {
598
604
  if (!p) return '';
599
605
  var colors = { high: '#ef4444', medium: '#f59e0b', low: '#6b7280' };
@@ -839,14 +845,95 @@ export function renderTasksAndSettingsScript(authToken) {
839
845
  });
840
846
  }
841
847
 
848
+ function getSettingsProject() {
849
+ var sel = document.getElementById('settings-project-select');
850
+ return sel ? sel.value : '';
851
+ }
852
+
853
+ function postProjectOverride(project, field, value, clearField) {
854
+ var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
855
+ fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
856
+ var payload = { project: project, field: field, value: value || '', clear: clearField ? 'true' : 'false' };
857
+ var body = new URLSearchParams(payload);
858
+ if (csrfData.token) body.set('_csrf', csrfData.token);
859
+ var url = _tsAuthToken ? tsAuthUrl('/api/settings/project-overrides') : '/api/settings/project-overrides';
860
+ return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
861
+ }).then(function(r) { return r.json(); }).then(function(data) {
862
+ if (!data.ok) {
863
+ setSettingsStatus(data.error || 'Failed to update project override', 'err');
864
+ return;
865
+ }
866
+ _settingsLoaded = false;
867
+ loadSettings();
868
+ setSettingsStatus('Project override updated', 'ok');
869
+ }).catch(function(err) {
870
+ setSettingsStatus('Failed: ' + String(err), 'err');
871
+ });
872
+ }
873
+
842
874
  function loadSettings() {
843
- var url = _tsAuthToken ? tsAuthUrl('/api/settings') : '/api/settings';
875
+ var selectedProject = getSettingsProject();
876
+ var baseUrl = '/api/settings';
877
+ if (selectedProject) baseUrl += '?project=' + encodeURIComponent(selectedProject);
878
+ var url = _tsAuthToken ? tsAuthUrl(baseUrl) : baseUrl;
879
+
880
+ // Populate project selector on first load
881
+ var sel = document.getElementById('settings-project-select');
882
+ if (sel && sel.querySelectorAll('option[data-proj]').length === 0) {
883
+ var configUrl = _tsAuthToken ? tsAuthUrl('/api/config') : '/api/config';
884
+ fetch(configUrl).then(function(r) { return r.json(); }).then(function(d) {
885
+ if (d.ok && d.projects && d.projects.length && sel) {
886
+ d.projects.forEach(function(p) {
887
+ var opt = document.createElement('option');
888
+ opt.value = p; opt.textContent = p;
889
+ opt.setAttribute('data-proj', '1');
890
+ sel.appendChild(opt);
891
+ });
892
+ if (selectedProject) sel.value = selectedProject;
893
+ }
894
+ }).catch(function() {});
895
+ }
896
+
897
+ var scopeNote = document.getElementById('settings-scope-note');
898
+ if (scopeNote) {
899
+ scopeNote.textContent = selectedProject
900
+ ? 'Showing effective config for "' + selectedProject + '". Overrides are saved to that project\'s phren.project.yaml.'
901
+ : 'Showing global settings. Select a project to view and edit per-project overrides.';
902
+ }
903
+
904
+ // Wire onChange once
905
+ if (sel && !sel.getAttribute('data-onchange-wired')) {
906
+ sel.setAttribute('data-onchange-wired', '1');
907
+ sel.addEventListener('change', function() {
908
+ _settingsLoaded = false;
909
+ loadSettings();
910
+ });
911
+ }
912
+
844
913
  fetch(url).then(function(r) { return r.json(); }).then(function(data) {
845
914
  if (!data.ok) {
846
915
  setSettingsStatus(data.error || 'Failed to load settings', 'err');
847
916
  return;
848
917
  }
849
918
 
919
+ // Use merged config when a project is selected, else global
920
+ var effective = (selectedProject && data.merged) ? data.merged : null;
921
+ var rawOverrides = (selectedProject && data.overrides) ? data.overrides : null;
922
+ var effectiveSensitivity = effective ? effective.findingSensitivity : (data.findingSensitivity || 'balanced');
923
+ var effectiveTaskMode = effective ? effective.taskMode : (data.taskMode || 'auto');
924
+ var effectiveProactivity = data.proactivity || 'high';
925
+ var effectiveRetention = (effective && effective.retentionPolicy) ? effective.retentionPolicy : (data.retentionPolicy || {});
926
+ var effectiveWorkflow = (effective && effective.workflowPolicy) ? effective.workflowPolicy : (data.workflowPolicy || {});
927
+
928
+ var isProject = Boolean(selectedProject);
929
+
930
+ function sourceBadge(isOverride) {
931
+ if (!isProject) return '';
932
+ return isOverride
933
+ ? '<span style="font-size:10px;font-weight:600;color:var(--warning);margin-left:6px;padding:1px 6px;border:1px solid color-mix(in srgb,var(--warning) 40%,transparent);border-radius:var(--radius-sm)">project override</span>'
934
+ : '<span style="font-size:10px;color:var(--text-muted);margin-left:6px;padding:1px 6px;border:1px solid var(--border);border-radius:var(--radius-sm)">global default</span>';
935
+ }
936
+
850
937
  var findingDescriptions = {
851
938
  high: 'Capture findings proactively, including minor observations.',
852
939
  medium: 'Capture findings that are likely useful.',
@@ -856,56 +943,129 @@ export function renderTasksAndSettingsScript(authToken) {
856
943
 
857
944
  var findingsEl = document.getElementById('settings-findings');
858
945
  if (findingsEl) {
859
- var fsUi = findingStorageToUi(data.findingSensitivity || 'balanced');
946
+ var fsUi = findingStorageToUi(effectiveSensitivity);
860
947
  var findingsHtml = '';
948
+ var fsSensOverride = rawOverrides && rawOverrides.findingSensitivity != null;
861
949
  findingsHtml += '<div class="settings-control">';
862
- findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Finding sensitivity</span></div>';
950
+ findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Finding sensitivity</span>' + sourceBadge(fsSensOverride);
951
+ if (isProject && fsSensOverride) {
952
+ findingsHtml += '<button data-ts-action="clearProjectOverride" data-field="findingSensitivity" class="settings-chip" style="font-size:11px;margin-left:auto">Clear override</button>';
953
+ }
954
+ findingsHtml += '</div>';
863
955
  findingsHtml += '<div class="settings-chip-row">';
864
956
  ['high', 'medium', 'low', 'minimal'].forEach(function(level) {
865
957
  var active = level === fsUi ? ' active' : '';
866
- findingsHtml += '<button data-ts-action="setFindingSensitivity" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
958
+ var action = isProject ? 'setProjectFindingSensitivity' : 'setFindingSensitivity';
959
+ findingsHtml += '<button data-ts-action="' + action + '" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
867
960
  });
868
961
  findingsHtml += '</div>';
869
962
  findingsHtml += '<div class="settings-control-note" id="settings-fs-desc">' + esc(findingDescriptions[fsUi] || '') + '</div>';
870
963
  findingsHtml += '</div>';
871
- findingsHtml += '<div class="settings-control">';
872
- findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Auto-capture</span>';
873
- findingsHtml += '<button data-ts-action="toggleAutoCapture" data-enabled="' + (data.autoCaptureEnabled ? 'true' : 'false') + '" class="settings-chip' + (data.autoCaptureEnabled ? ' active' : '') + '">' + (data.autoCaptureEnabled ? 'On' : 'Off') + '</button></div>';
874
- findingsHtml += '<div class="settings-control-note">Turn automatic finding capture on or off.</div>';
875
- findingsHtml += '</div>';
876
- findingsHtml += '<div class="settings-control">';
877
- findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Consolidation threshold</span><span class="badge">' + esc(String(data.consolidationEntryThreshold || 25)) + ' entries</span></div>';
878
- findingsHtml += '<div class="settings-control-note">Consolidation is also recommended after 60+ days with at least 10 new entries.</div>';
879
- findingsHtml += '</div>';
964
+ if (!isProject) {
965
+ findingsHtml += '<div class="settings-control">';
966
+ findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Auto-capture</span>';
967
+ findingsHtml += '<button data-ts-action="toggleAutoCapture" data-enabled="' + (data.autoCaptureEnabled ? 'true' : 'false') + '" class="settings-chip' + (data.autoCaptureEnabled ? ' active' : '') + '">' + (data.autoCaptureEnabled ? 'On' : 'Off') + '</button></div>';
968
+ findingsHtml += '<div class="settings-control-note">Turn automatic finding capture on or off.</div>';
969
+ findingsHtml += '</div>';
970
+ findingsHtml += '<div class="settings-control">';
971
+ findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Consolidation threshold</span><span class="badge">' + esc(String(data.consolidationEntryThreshold || 25)) + ' entries</span></div>';
972
+ findingsHtml += '<div class="settings-control-note">Consolidation is also recommended after 60+ days with at least 10 new entries.</div>';
973
+ findingsHtml += '</div>';
974
+ }
880
975
  findingsEl.innerHTML = findingsHtml;
881
976
  }
882
977
 
883
978
  var behaviorEl = document.getElementById('settings-behavior');
884
979
  if (behaviorEl) {
885
- var taskMode = data.taskMode === 'suggest' ? 'manual' : (data.taskMode || 'auto');
886
- var proactivity = data.proactivity || 'high';
980
+ var taskMode = effectiveTaskMode || 'auto';
981
+ var proactivity = effectiveProactivity;
887
982
  var behaviorHtml = '';
983
+ var taskModeOverride = rawOverrides && rawOverrides.taskMode != null;
888
984
  behaviorHtml += '<div class="settings-control">';
889
- behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Task mode</span></div>';
985
+ behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Task mode</span>' + sourceBadge(taskModeOverride);
986
+ if (isProject && taskModeOverride) {
987
+ behaviorHtml += '<button data-ts-action="clearProjectOverride" data-field="taskMode" class="settings-chip" style="font-size:11px;margin-left:auto">Clear override</button>';
988
+ }
989
+ behaviorHtml += '</div>';
890
990
  behaviorHtml += '<div class="settings-chip-row">';
891
- ['auto', 'manual', 'off'].forEach(function(mode) {
991
+ ['auto', 'suggest', 'manual', 'off'].forEach(function(mode) {
892
992
  var active = mode === taskMode ? ' active' : '';
893
- behaviorHtml += '<button data-ts-action="setTaskMode" data-mode="' + esc(mode) + '" class="settings-chip' + active + '">' + esc(mode) + '</button>';
894
- });
895
- behaviorHtml += '</div></div>';
896
- behaviorHtml += '<div class="settings-control">';
897
- behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Proactivity level</span></div>';
898
- behaviorHtml += '<div class="settings-chip-row">';
899
- ['high', 'medium', 'low'].forEach(function(level) {
900
- var active = level === proactivity ? ' active' : '';
901
- behaviorHtml += '<button data-ts-action="setProactivity" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
993
+ var action = isProject ? 'setProjectTaskMode' : 'setTaskMode';
994
+ behaviorHtml += '<button data-ts-action="' + action + '" data-mode="' + esc(mode) + '" class="settings-chip' + active + '">' + esc(mode) + '</button>';
902
995
  });
903
996
  behaviorHtml += '</div></div>';
997
+ if (!isProject) {
998
+ behaviorHtml += '<div class="settings-control">';
999
+ behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Proactivity level</span></div>';
1000
+ behaviorHtml += '<div class="settings-chip-row">';
1001
+ ['high', 'medium', 'low'].forEach(function(level) {
1002
+ var active = level === proactivity ? ' active' : '';
1003
+ behaviorHtml += '<button data-ts-action="setProactivity" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
1004
+ });
1005
+ behaviorHtml += '</div></div>';
1006
+ }
904
1007
  behaviorEl.innerHTML = behaviorHtml;
905
1008
  }
906
1009
 
1010
+ var retentionEl = document.getElementById('settings-retention');
1011
+ if (retentionEl) {
1012
+ var ret = effectiveRetention;
1013
+ var retHtml = '';
1014
+ function retRow(label, field, value, note) {
1015
+ var isOverride = isProject && rawOverrides && rawOverrides.retentionPolicy && rawOverrides.retentionPolicy[field] !== undefined;
1016
+ retHtml += '<div class="settings-control">';
1017
+ retHtml += '<div class="settings-control-header"><span class="settings-control-label">' + esc(label) + '</span>' + sourceBadge(isOverride);
1018
+ retHtml += '<span class="settings-control-value" style="margin-left:auto">' + esc(String(value != null ? value : '—')) + '</span>';
1019
+ if (isProject && isOverride) {
1020
+ retHtml += '<button data-ts-action="clearProjectOverride" data-field="' + esc(field) + '" class="settings-chip" style="font-size:11px">Clear</button>';
1021
+ }
1022
+ retHtml += '</div>';
1023
+ if (note) retHtml += '<div class="settings-control-note">' + esc(note) + '</div>';
1024
+ if (isProject) {
1025
+ retHtml += '<div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
1026
+ '<input type="number" id="ret-input-' + esc(field) + '" value="' + esc(String(value != null ? value : '')) + '" style="width:100px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
1027
+ '<button data-ts-action="setProjectRetention" data-field="' + esc(field) + '" class="settings-chip active" style="font-size:11px">Set</button>' +
1028
+ '</div>';
1029
+ }
1030
+ retHtml += '</div>';
1031
+ }
1032
+ retRow('TTL days', 'ttlDays', ret.ttlDays, 'Memories older than this are eligible for pruning.');
1033
+ retRow('Retention days', 'retentionDays', ret.retentionDays, 'Hard cutoff — memories past this age are removed.');
1034
+ retRow('Auto-accept threshold', 'autoAcceptThreshold', ret.autoAcceptThreshold, 'Confidence score (0–1) above which memories are auto-accepted.');
1035
+ retRow('Min inject confidence', 'minInjectConfidence', ret.minInjectConfidence, 'Minimum confidence score to inject a memory into context.');
1036
+ retentionEl.innerHTML = retHtml;
1037
+ }
1038
+
1039
+ var workflowEl = document.getElementById('settings-workflow');
1040
+ if (workflowEl) {
1041
+ var wf = effectiveWorkflow;
1042
+ var wfHtml = '';
1043
+ var lctOverride = isProject && rawOverrides && rawOverrides.workflowPolicy && rawOverrides.workflowPolicy.lowConfidenceThreshold !== undefined;
1044
+ var riskySectionsOverride = isProject && rawOverrides && rawOverrides.workflowPolicy && Array.isArray(rawOverrides.workflowPolicy.riskySections) && rawOverrides.workflowPolicy.riskySections.length > 0;
1045
+ wfHtml += '<div class="settings-control">';
1046
+ wfHtml += '<div class="settings-control-header"><span class="settings-control-label">Low confidence threshold</span>' + sourceBadge(lctOverride);
1047
+ wfHtml += '<span class="settings-control-value" style="margin-left:auto">' + esc(String(wf.lowConfidenceThreshold != null ? wf.lowConfidenceThreshold : '—')) + '</span>';
1048
+ if (isProject && lctOverride) {
1049
+ wfHtml += '<button data-ts-action="clearProjectOverride" data-field="lowConfidenceThreshold" class="settings-chip" style="font-size:11px">Clear</button>';
1050
+ }
1051
+ if (isProject) {
1052
+ wfHtml += '</div><div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
1053
+ '<input type="number" id="wf-input-lowConfidenceThreshold" min="0" max="1" step="0.05" value="' + esc(String(wf.lowConfidenceThreshold != null ? wf.lowConfidenceThreshold : '')) + '" style="width:100px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
1054
+ '<button data-ts-action="setProjectWorkflow" data-field="lowConfidenceThreshold" class="settings-chip active" style="font-size:11px">Set</button>' +
1055
+ '</div>';
1056
+ } else {
1057
+ wfHtml += '</div>';
1058
+ }
1059
+ wfHtml += '<div class="settings-control-note">Memories below this confidence score are flagged for review.</div></div>';
1060
+ wfHtml += '<div class="settings-control">';
1061
+ wfHtml += '<div class="settings-control-header"><span class="settings-control-label">Risky sections</span>' + sourceBadge(riskySectionsOverride);
1062
+ wfHtml += '<span class="settings-control-value" style="margin-left:auto">' + esc(Array.isArray(wf.riskySections) ? wf.riskySections.join(', ') : '—') + '</span></div>';
1063
+ wfHtml += '<div class="settings-control-note">Sections that trigger approval gates when memories are written.</div></div>';
1064
+ workflowEl.innerHTML = wfHtml;
1065
+ }
1066
+
907
1067
  var integrationsEl = document.getElementById('settings-integrations');
908
- if (integrationsEl) {
1068
+ if (integrationsEl && !isProject) {
909
1069
  var tools = Array.isArray(data.hookTools) ? data.hookTools : [];
910
1070
  var html = '';
911
1071
  html += '<div class="settings-control-header" style="margin-bottom:10px"><span class="settings-control-label">Global MCP</span>';
@@ -923,6 +1083,8 @@ export function renderTasksAndSettingsScript(authToken) {
923
1083
  });
924
1084
  html += '</tbody></table>';
925
1085
  integrationsEl.innerHTML = html;
1086
+ } else if (integrationsEl && isProject) {
1087
+ integrationsEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">Integration settings are global — switch to Global scope to edit them.</div>';
926
1088
  }
927
1089
  }).catch(function(err) {
928
1090
  setSettingsStatus('Failed to load settings: ' + String(err), 'err');
@@ -1063,6 +1225,33 @@ export function renderTasksAndSettingsScript(authToken) {
1063
1225
  else if (action === 'toggleIntegrationTool') { toggleIntegrationTool(actionEl.getAttribute('data-tool')); }
1064
1226
  else if (action === 'showSessionDetail') { showSessionDetail(actionEl.getAttribute('data-session-id')); }
1065
1227
  else if (action === 'backToSessionsList') { backToSessionsList(); }
1228
+ else if (action === 'setProjectFindingSensitivity') {
1229
+ var proj = getSettingsProject();
1230
+ var level = actionEl.getAttribute('data-level');
1231
+ postProjectOverride(proj, 'findingSensitivity', findingUiToStorage(level || 'medium'), false);
1232
+ }
1233
+ else if (action === 'setProjectTaskMode') {
1234
+ var proj = getSettingsProject();
1235
+ postProjectOverride(proj, 'taskMode', actionEl.getAttribute('data-mode') || 'auto', false);
1236
+ }
1237
+ else if (action === 'clearProjectOverride') {
1238
+ var proj = getSettingsProject();
1239
+ postProjectOverride(proj, actionEl.getAttribute('data-field') || '', '', true);
1240
+ }
1241
+ else if (action === 'setProjectRetention') {
1242
+ var proj = getSettingsProject();
1243
+ var field = actionEl.getAttribute('data-field') || '';
1244
+ var inputEl = document.getElementById('ret-input-' + field);
1245
+ var val = inputEl ? inputEl.value : '';
1246
+ postProjectOverride(proj, field, val, false);
1247
+ }
1248
+ else if (action === 'setProjectWorkflow') {
1249
+ var proj = getSettingsProject();
1250
+ var field = actionEl.getAttribute('data-field') || '';
1251
+ var inputEl = document.getElementById('wf-input-' + field);
1252
+ var val = inputEl ? inputEl.value : '';
1253
+ postProjectOverride(proj, field, val, false);
1254
+ }
1066
1255
  });
1067
1256
 
1068
1257
  window.setFindingSensitivity = function(level) {
@@ -1104,10 +1293,10 @@ export function renderSearchScript(authToken) {
1104
1293
  var _searchProjectsLoaded = false;
1105
1294
 
1106
1295
  function searchAuthUrl(path) {
1107
- return _searchAuthToken ? path + (path.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(_searchAuthToken) : path;
1296
+ return window._phrenAuthUrl ? window._phrenAuthUrl(path) : (_searchAuthToken ? path + (path.includes('?') ? '&' : '?') + '_auth=' + encodeURIComponent(_searchAuthToken) : path);
1108
1297
  }
1109
1298
 
1110
- function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
1299
+ var esc = window._phrenEsc;
1111
1300
 
1112
1301
  function doSearch() {
1113
1302
  var q = document.getElementById('search-query').value.trim();
@@ -12,7 +12,8 @@ import { readInstallPreferences, writeInstallPreferences, writeGovernanceInstall
12
12
  import { buildGraph, collectProjectsForUI, collectSkillsForUI, getHooksData, isAllowedFilePath, readSyncSnapshot, recentAccepted, recentUsage, } from "./memory-ui-data.js";
13
13
  import { CONSOLIDATION_ENTRY_THRESHOLD } from "./content-validate.js";
14
14
  import { ensureTopicReferenceDoc, getProjectTopicsResponse, listProjectReferenceDocs, pinProjectTopicSuggestion, readReferenceContent, reclassifyLegacyTopicDocs, unpinProjectTopicSuggestion, writeProjectTopics, } from "./project-topics.js";
15
- import { getWorkflowPolicy, updateWorkflowPolicy } from "./governance-policy.js";
15
+ import { getWorkflowPolicy, updateWorkflowPolicy, mergeConfig, getRetentionPolicy, getProjectConfigOverrides } from "./governance-policy.js";
16
+ import { updateProjectConfigOverrides } from "./project-config.js";
16
17
  import { findSkill } from "./skill-registry.js";
17
18
  import { setSkillEnabledAndSync } from "./skill-files.js";
18
19
  import { listAllSessions, getSessionArtifacts } from "./mcp-session.js";
@@ -290,7 +291,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
290
291
  repairPreexistingInstall(phrenPath);
291
292
  }
292
293
  catch (err) {
293
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
294
+ if ((process.env.PHREN_DEBUG))
294
295
  process.stderr.write(`[phren] web-ui repair: ${errorMessage(err)}\n`);
295
296
  }
296
297
  const authToken = opts?.authToken;
@@ -365,7 +366,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
365
366
  }
366
367
  catch (err) {
367
368
  res.writeHead(200, { "content-type": "application/json" });
368
- res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
369
+ res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
369
370
  }
370
371
  });
371
372
  return;
@@ -856,8 +857,13 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
856
857
  try {
857
858
  const prefs = readInstallPreferences(phrenPath);
858
859
  const workflowPolicy = getWorkflowPolicy(phrenPath);
860
+ const retentionPolicy = getRetentionPolicy(phrenPath);
859
861
  const hooksData = getHooksData(phrenPath);
860
862
  const proactivityFindings = prefs.proactivityFindings || prefs.proactivity || "high";
863
+ const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
864
+ const settingsProject = String(qs.project || "");
865
+ const merged = settingsProject && isValidProjectName(settingsProject) ? mergeConfig(phrenPath, settingsProject) : null;
866
+ const overrides = settingsProject && isValidProjectName(settingsProject) ? getProjectConfigOverrides(phrenPath, settingsProject) : null;
861
867
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
862
868
  res.end(JSON.stringify({
863
869
  ok: true,
@@ -871,6 +877,10 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
871
877
  hooksEnabled: hooksData.globalEnabled,
872
878
  mcpEnabled: prefs.mcpEnabled !== false,
873
879
  hookTools: hooksData.tools,
880
+ retentionPolicy,
881
+ workflowPolicy,
882
+ merged,
883
+ overrides,
874
884
  }));
875
885
  }
876
886
  catch (err) {
@@ -975,6 +985,108 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
975
985
  });
976
986
  return;
977
987
  }
988
+ // POST /api/settings/project-overrides — write per-project config overrides
989
+ if (req.method === "POST" && pathname === "/api/settings/project-overrides") {
990
+ void readFormBody(req, res).then((parsed) => {
991
+ if (!parsed)
992
+ return;
993
+ if (!requirePostAuth(req, res, url, parsed, authToken, true))
994
+ return;
995
+ if (!requireCsrf(res, parsed, csrfTokens, true))
996
+ return;
997
+ const project = String(parsed.project || "");
998
+ const field = String(parsed.field || "");
999
+ const value = String(parsed.value || "");
1000
+ const clearField = String(parsed.clear || "") === "true";
1001
+ if (!project || !isValidProjectName(project)) {
1002
+ res.writeHead(400, { "content-type": "application/json" });
1003
+ res.end(JSON.stringify({ ok: false, error: "Invalid project name" }));
1004
+ return;
1005
+ }
1006
+ const registeredProjects = getProjectDirs(phrenPath, profile).map((d) => path.basename(d)).filter((p) => p !== "global");
1007
+ const isRegistered = registeredProjects.includes(project);
1008
+ const registrationWarning = isRegistered ? undefined : `Project '${project}' is not registered in the active profile. Config was saved but it will have no effect until the project is added with 'phren add'.`;
1009
+ const VALID_FIELDS = {
1010
+ findingSensitivity: ["minimal", "conservative", "balanced", "aggressive"],
1011
+ proactivity: ["high", "medium", "low"],
1012
+ proactivityFindings: ["high", "medium", "low"],
1013
+ proactivityTask: ["high", "medium", "low"],
1014
+ taskMode: ["off", "manual", "suggest", "auto"],
1015
+ };
1016
+ const NUMERIC_RETENTION_FIELDS = ["ttlDays", "retentionDays", "autoAcceptThreshold", "minInjectConfidence"];
1017
+ const NUMERIC_WORKFLOW_FIELDS = ["lowConfidenceThreshold"];
1018
+ try {
1019
+ updateProjectConfigOverrides(phrenPath, project, (current) => {
1020
+ const next = { ...current };
1021
+ if (clearField) {
1022
+ if (field in VALID_FIELDS)
1023
+ delete next[field];
1024
+ else if (NUMERIC_RETENTION_FIELDS.includes(field)) {
1025
+ if (next.retentionPolicy)
1026
+ delete next.retentionPolicy[field];
1027
+ }
1028
+ else if (NUMERIC_WORKFLOW_FIELDS.includes(field)) {
1029
+ if (next.workflowPolicy)
1030
+ delete next.workflowPolicy[field];
1031
+ }
1032
+ return next;
1033
+ }
1034
+ if (field in VALID_FIELDS) {
1035
+ const allowed = VALID_FIELDS[field];
1036
+ if (!allowed.includes(value))
1037
+ throw new Error(`Invalid value "${value}" for ${field}`);
1038
+ next[field] = value;
1039
+ }
1040
+ else if (NUMERIC_RETENTION_FIELDS.includes(field)) {
1041
+ const num = parseFloat(value);
1042
+ if (!Number.isFinite(num) || num < 0)
1043
+ throw new Error(`Invalid numeric value for ${field}`);
1044
+ next.retentionPolicy = { ...next.retentionPolicy, [field]: num };
1045
+ }
1046
+ else if (NUMERIC_WORKFLOW_FIELDS.includes(field)) {
1047
+ const num = parseFloat(value);
1048
+ if (!Number.isFinite(num) || num < 0 || num > 1)
1049
+ throw new Error(`Invalid value for ${field} (must be 0–1)`);
1050
+ next.workflowPolicy = { ...next.workflowPolicy, [field]: num };
1051
+ }
1052
+ else {
1053
+ throw new Error(`Unknown config field: ${field}`);
1054
+ }
1055
+ return next;
1056
+ });
1057
+ const merged = mergeConfig(phrenPath, project);
1058
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1059
+ res.end(JSON.stringify({ ok: true, config: merged }));
1060
+ }
1061
+ catch (err) {
1062
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1063
+ res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
1064
+ }
1065
+ });
1066
+ return;
1067
+ }
1068
+ if (req.method === "GET" && pathname === "/api/config") {
1069
+ if (!requireGetAuth(req, res, url, authToken, true))
1070
+ return;
1071
+ const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
1072
+ const project = String(qs.project || "");
1073
+ if (project && !isValidProjectName(project)) {
1074
+ res.writeHead(400, { "content-type": "application/json" });
1075
+ res.end(JSON.stringify({ ok: false, error: "Invalid project name" }));
1076
+ return;
1077
+ }
1078
+ try {
1079
+ const config = mergeConfig(phrenPath, project || undefined);
1080
+ const projects = getProjectDirs(phrenPath, profile).map((d) => path.basename(d)).filter((p) => p !== "global");
1081
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1082
+ res.end(JSON.stringify({ ok: true, config, projects }));
1083
+ }
1084
+ catch (err) {
1085
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1086
+ res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
1087
+ }
1088
+ return;
1089
+ }
978
1090
  if (req.method === "GET" && pathname === "/api/csrf-token") {
979
1091
  if (!requireGetAuth(req, res, url, authToken, true))
980
1092
  return;
@@ -24,6 +24,8 @@ export const EXTRA_ENTITY_PATTERNS = [
24
24
  // ISO date references: 2025-03-11, 2025/03/11
25
25
  { re: /\b\d{4}[-/]\d{2}[-/]\d{2}\b/g, label: "date" },
26
26
  ];
27
+ /** Union of all directory names reserved by phren infrastructure — not valid project names. */
28
+ export const RESERVED_PROJECT_DIR_NAMES = new Set(["global", ".runtime", ".sessions", ".governance", "profiles", "templates"]);
27
29
  // Default timeout for execFileSync calls (30s for most operations, 10s for quick probes like `which`)
28
30
  export const EXEC_TIMEOUT_MS = 30_000;
29
31
  export const EXEC_TIMEOUT_QUICK_MS = 10_000;