@phren/cli 0.0.54 → 0.0.56
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 +70 -92
- package/{mcp/dist → dist}/cli/cli.js +15 -3
- package/{mcp/dist → dist}/cli/hooks-session.js +47 -0
- package/{mcp/dist → dist}/cli/namespaces.js +120 -0
- package/{mcp/dist → dist}/entrypoint.js +3 -0
- package/{mcp/dist → dist}/generated/memory-ui-graph.browser.js +26 -23
- package/{mcp/dist → dist}/memory-ui-graph.runtime.js +26 -23
- package/{mcp/dist → dist}/package-metadata.js +1 -1
- package/{mcp/dist → dist}/profile-store.js +3 -0
- package/{mcp/dist → dist}/session/utils.js +24 -1
- package/{mcp/dist → dist}/test-global-setup.js +4 -4
- package/{mcp/dist → dist}/tools/session.js +10 -37
- package/dist/ui/assets.js +8 -0
- package/{mcp/dist → dist}/ui/page.js +25 -1
- package/{mcp/dist → dist}/ui/scripts.js +303 -18
- package/{mcp/dist → dist}/ui/server.js +132 -3
- package/{mcp/dist → dist}/ui/styles.js +6 -0
- package/package.json +24 -38
- package/LICENSE +0 -21
- package/icon.svg +0 -416
- package/mcp/dist/ui/assets.js +0 -8
- /package/{mcp/dist → dist}/capabilities/cli.js +0 -0
- /package/{mcp/dist → dist}/capabilities/index.js +0 -0
- /package/{mcp/dist → dist}/capabilities/mcp.js +0 -0
- /package/{mcp/dist → dist}/capabilities/types.js +0 -0
- /package/{mcp/dist → dist}/capabilities/vscode.js +0 -0
- /package/{mcp/dist → dist}/capabilities/web-ui.js +0 -0
- /package/{mcp/dist → dist}/cli/actions.js +0 -0
- /package/{mcp/dist → dist}/cli/config.js +0 -0
- /package/{mcp/dist → dist}/cli/extract.js +0 -0
- /package/{mcp/dist → dist}/cli/govern.js +0 -0
- /package/{mcp/dist → dist}/cli/graph.js +0 -0
- /package/{mcp/dist → dist}/cli/hooks-citations.js +0 -0
- /package/{mcp/dist → dist}/cli/hooks-context.js +0 -0
- /package/{mcp/dist → dist}/cli/hooks-globs.js +0 -0
- /package/{mcp/dist → dist}/cli/hooks-output.js +0 -0
- /package/{mcp/dist → dist}/cli/hooks.js +0 -0
- /package/{mcp/dist → dist}/cli/ops.js +0 -0
- /package/{mcp/dist → dist}/cli/search.js +0 -0
- /package/{mcp/dist → dist}/cli/team.js +0 -0
- /package/{mcp/dist → dist}/cli-hooks-git.js +0 -0
- /package/{mcp/dist → dist}/cli-hooks-prompt.js +0 -0
- /package/{mcp/dist → dist}/cli-hooks-session-handlers.js +0 -0
- /package/{mcp/dist → dist}/cli-hooks-stop.js +0 -0
- /package/{mcp/dist → dist}/content/archive.js +0 -0
- /package/{mcp/dist → dist}/content/citation.js +0 -0
- /package/{mcp/dist → dist}/content/dedup.js +0 -0
- /package/{mcp/dist → dist}/content/learning.js +0 -0
- /package/{mcp/dist → dist}/content/metadata.js +0 -0
- /package/{mcp/dist → dist}/content/validate.js +0 -0
- /package/{mcp/dist → dist}/core/finding.js +0 -0
- /package/{mcp/dist → dist}/core/project.js +0 -0
- /package/{mcp/dist → dist}/core/search.js +0 -0
- /package/{mcp/dist → dist}/data/access.js +0 -0
- /package/{mcp/dist → dist}/data/tasks.js +0 -0
- /package/{mcp/dist → dist}/embedding.js +0 -0
- /package/{mcp/dist → dist}/finding/context.js +0 -0
- /package/{mcp/dist → dist}/finding/impact.js +0 -0
- /package/{mcp/dist → dist}/finding/journal.js +0 -0
- /package/{mcp/dist → dist}/finding/lifecycle.js +0 -0
- /package/{mcp/dist → dist}/governance/audit.js +0 -0
- /package/{mcp/dist → dist}/governance/locks.js +0 -0
- /package/{mcp/dist → dist}/governance/policy.js +0 -0
- /package/{mcp/dist → dist}/governance/rbac.js +0 -0
- /package/{mcp/dist → dist}/governance/scores.js +0 -0
- /package/{mcp/dist → dist}/hooks.js +0 -0
- /package/{mcp/dist → dist}/index-query.js +0 -0
- /package/{mcp/dist → dist}/index.js +0 -0
- /package/{mcp/dist → dist}/init/config.js +0 -0
- /package/{mcp/dist → dist}/init/init-configure.js +0 -0
- /package/{mcp/dist → dist}/init/init-hooks-mode.js +0 -0
- /package/{mcp/dist → dist}/init/init-mcp-mode.js +0 -0
- /package/{mcp/dist → dist}/init/init-uninstall.js +0 -0
- /package/{mcp/dist → dist}/init/init-walkthrough.js +0 -0
- /package/{mcp/dist → dist}/init/init.js +0 -0
- /package/{mcp/dist → dist}/init/preferences.js +0 -0
- /package/{mcp/dist → dist}/init/setup.js +0 -0
- /package/{mcp/dist → dist}/init/shared.js +0 -0
- /package/{mcp/dist → dist}/init-bootstrap.js +0 -0
- /package/{mcp/dist → dist}/init-detect.js +0 -0
- /package/{mcp/dist → dist}/init-env.js +0 -0
- /package/{mcp/dist → dist}/init-fresh.js +0 -0
- /package/{mcp/dist → dist}/init-hooks.js +0 -0
- /package/{mcp/dist → dist}/init-mcp.js +0 -0
- /package/{mcp/dist → dist}/init-modes.js +0 -0
- /package/{mcp/dist → dist}/init-npm.js +0 -0
- /package/{mcp/dist → dist}/init-project-local.js +0 -0
- /package/{mcp/dist → dist}/init-semantic.js +0 -0
- /package/{mcp/dist → dist}/init-types.js +0 -0
- /package/{mcp/dist → dist}/init-uninstall.js +0 -0
- /package/{mcp/dist → dist}/init-update.js +0 -0
- /package/{mcp/dist → dist}/init-walkthrough.js +0 -0
- /package/{mcp/dist → dist}/link/checksums.js +0 -0
- /package/{mcp/dist → dist}/link/context.js +0 -0
- /package/{mcp/dist → dist}/link/doctor.js +0 -0
- /package/{mcp/dist → dist}/link/link.js +0 -0
- /package/{mcp/dist → dist}/link/skills.js +0 -0
- /package/{mcp/dist → dist}/logger.js +0 -0
- /package/{mcp/dist → dist}/machine-identity.js +0 -0
- /package/{mcp/dist → dist}/phren-art.js +0 -0
- /package/{mcp/dist → dist}/phren-core.js +0 -0
- /package/{mcp/dist → dist}/phren-dotenv.js +0 -0
- /package/{mcp/dist → dist}/phren-paths.js +0 -0
- /package/{mcp/dist → dist}/proactivity.js +0 -0
- /package/{mcp/dist → dist}/project-config.js +0 -0
- /package/{mcp/dist → dist}/project-locator.js +0 -0
- /package/{mcp/dist → dist}/project-topics.js +0 -0
- /package/{mcp/dist → dist}/provider-adapters.js +0 -0
- /package/{mcp/dist → dist}/query-correlation.js +0 -0
- /package/{mcp/dist → dist}/runtime-profile.js +0 -0
- /package/{mcp/dist → dist}/session/checkpoints.js +0 -0
- /package/{mcp/dist → dist}/shared/content.js +0 -0
- /package/{mcp/dist → dist}/shared/data-utils.js +0 -0
- /package/{mcp/dist → dist}/shared/embedding-cache.js +0 -0
- /package/{mcp/dist → dist}/shared/fragment-graph.js +0 -0
- /package/{mcp/dist → dist}/shared/governance.js +0 -0
- /package/{mcp/dist → dist}/shared/index.js +0 -0
- /package/{mcp/dist → dist}/shared/ollama.js +0 -0
- /package/{mcp/dist → dist}/shared/process.js +0 -0
- /package/{mcp/dist → dist}/shared/retrieval.js +0 -0
- /package/{mcp/dist → dist}/shared/search-fallback.js +0 -0
- /package/{mcp/dist → dist}/shared/sqljs.js +0 -0
- /package/{mcp/dist → dist}/shared/stemmer.js +0 -0
- /package/{mcp/dist → dist}/shared/vector-index.js +0 -0
- /package/{mcp/dist → dist}/shared.js +0 -0
- /package/{mcp/dist → dist}/shell/entry.js +0 -0
- /package/{mcp/dist → dist}/shell/input.js +0 -0
- /package/{mcp/dist → dist}/shell/palette.js +0 -0
- /package/{mcp/dist → dist}/shell/render.js +0 -0
- /package/{mcp/dist → dist}/shell/shell.js +0 -0
- /package/{mcp/dist → dist}/shell/state-store.js +0 -0
- /package/{mcp/dist → dist}/shell/types.js +0 -0
- /package/{mcp/dist → dist}/shell/view-list.js +0 -0
- /package/{mcp/dist → dist}/shell/view.js +0 -0
- /package/{mcp/dist → dist}/skill/files.js +0 -0
- /package/{mcp/dist → dist}/skill/registry.js +0 -0
- /package/{mcp/dist → dist}/skill/state.js +0 -0
- /package/{mcp/dist → dist}/startup-embedding.js +0 -0
- /package/{mcp/dist → dist}/status.js +0 -0
- /package/{mcp/dist → dist}/store-registry.js +0 -0
- /package/{mcp/dist → dist}/store-routing.js +0 -0
- /package/{mcp/dist → dist}/synonyms.json +0 -0
- /package/{mcp/dist → dist}/task/github.js +0 -0
- /package/{mcp/dist → dist}/task/hygiene.js +0 -0
- /package/{mcp/dist → dist}/task/lifecycle.js +0 -0
- /package/{mcp/dist → dist}/telemetry.js +0 -0
- /package/{mcp/dist → dist}/tool-registry.js +0 -0
- /package/{mcp/dist → dist}/tools/config.js +0 -0
- /package/{mcp/dist → dist}/tools/data.js +0 -0
- /package/{mcp/dist → dist}/tools/extract-facts.js +0 -0
- /package/{mcp/dist → dist}/tools/extract.js +0 -0
- /package/{mcp/dist → dist}/tools/finding.js +0 -0
- /package/{mcp/dist → dist}/tools/graph.js +0 -0
- /package/{mcp/dist → dist}/tools/hooks.js +0 -0
- /package/{mcp/dist → dist}/tools/memory.js +0 -0
- /package/{mcp/dist → dist}/tools/ops.js +0 -0
- /package/{mcp/dist → dist}/tools/search.js +0 -0
- /package/{mcp/dist → dist}/tools/skills.js +0 -0
- /package/{mcp/dist → dist}/tools/tasks.js +0 -0
- /package/{mcp/dist → dist}/tools/types.js +0 -0
- /package/{mcp/dist → dist}/ui/data.js +0 -0
- /package/{mcp/dist → dist}/ui/graph.js +0 -0
- /package/{mcp/dist → dist}/ui/memory-ui.js +0 -0
- /package/{mcp/dist → dist}/update.js +0 -0
- /package/{mcp/dist → dist}/utils.js +0 -0
|
@@ -28,6 +28,62 @@ export function renderSharedWebUiHelpers(authToken) {
|
|
|
28
28
|
};
|
|
29
29
|
})();`;
|
|
30
30
|
}
|
|
31
|
+
export function renderProfileSwitcherScript(_authToken) {
|
|
32
|
+
return `(function() {
|
|
33
|
+
var esc = window._phrenEsc;
|
|
34
|
+
var authUrl = window._phrenAuthUrl;
|
|
35
|
+
|
|
36
|
+
function loadProfiles() {
|
|
37
|
+
fetch(authUrl('/api/profiles')).then(function(r) { return r.json(); }).then(function(data) {
|
|
38
|
+
var select = document.getElementById('profile-select');
|
|
39
|
+
if (!select) return;
|
|
40
|
+
if (!data.ok || !data.profiles) {
|
|
41
|
+
select.innerHTML = '<option>Error loading profiles</option>';
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
var html = '';
|
|
45
|
+
data.profiles.forEach(function(p) {
|
|
46
|
+
var selected = p.name === data.activeProfile ? ' selected' : '';
|
|
47
|
+
html += '<option value="' + esc(p.name) + '"' + selected + '>' + esc(p.name) + '</option>';
|
|
48
|
+
});
|
|
49
|
+
select.innerHTML = html;
|
|
50
|
+
select.onchange = function() { switchProfile(this.value); };
|
|
51
|
+
}).catch(function(err) {
|
|
52
|
+
var select = document.getElementById('profile-select');
|
|
53
|
+
if (select) select.innerHTML = '<option>Error loading</option>';
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function switchProfile(profileName) {
|
|
58
|
+
if (!profileName) return;
|
|
59
|
+
var status = document.getElementById('profile-status');
|
|
60
|
+
if (status) status.textContent = 'Switching...';
|
|
61
|
+
fetch(authUrl('/api/profile'), {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
64
|
+
body: window._phrenAuthBody('profile=' + encodeURIComponent(profileName))
|
|
65
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
66
|
+
if (data.ok) {
|
|
67
|
+
if (status) status.textContent = 'Reloading...';
|
|
68
|
+
setTimeout(function() { location.reload(); }, 500);
|
|
69
|
+
} else {
|
|
70
|
+
if (status) status.textContent = 'Error: ' + (data.error || 'Unknown');
|
|
71
|
+
}
|
|
72
|
+
}).catch(function(err) {
|
|
73
|
+
if (status) status.textContent = 'Error loading';
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
window.phrenLoadProfiles = loadProfiles;
|
|
78
|
+
window.phrenSwitchProfile = switchProfile;
|
|
79
|
+
|
|
80
|
+
if (document.readyState === 'loading') {
|
|
81
|
+
document.addEventListener('DOMContentLoaded', loadProfiles);
|
|
82
|
+
} else {
|
|
83
|
+
loadProfiles();
|
|
84
|
+
}
|
|
85
|
+
})();`;
|
|
86
|
+
}
|
|
31
87
|
export function renderSkillUiEnhancementScript(_authToken) {
|
|
32
88
|
return `(function() {
|
|
33
89
|
var _skillCurrent = null;
|
|
@@ -879,6 +935,48 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
879
935
|
});
|
|
880
936
|
}
|
|
881
937
|
|
|
938
|
+
function postGlobalRetention(field, value, clearField) {
|
|
939
|
+
var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
|
|
940
|
+
fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
|
|
941
|
+
var payload = { field: field, value: value || '', clear: clearField ? 'true' : 'false', globalUpdate: 'true' };
|
|
942
|
+
var body = new URLSearchParams(payload);
|
|
943
|
+
if (csrfData.token) body.set('_csrf', csrfData.token);
|
|
944
|
+
var url = _tsAuthToken ? tsAuthUrl('/api/settings/project-overrides') : '/api/settings/project-overrides';
|
|
945
|
+
return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
|
|
946
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
947
|
+
if (!data.ok) {
|
|
948
|
+
setSettingsStatus(data.error || 'Failed to update retention policy', 'err');
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
_settingsLoaded = false;
|
|
952
|
+
loadSettings();
|
|
953
|
+
setSettingsStatus('Retention policy updated', 'ok');
|
|
954
|
+
}).catch(function(err) {
|
|
955
|
+
setSettingsStatus('Failed: ' + String(err), 'err');
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function postGlobalWorkflow(field, value, clearField) {
|
|
960
|
+
var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
|
|
961
|
+
fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
|
|
962
|
+
var payload = { field: field, value: value || '', clear: clearField ? 'true' : 'false', globalUpdate: 'true' };
|
|
963
|
+
var body = new URLSearchParams(payload);
|
|
964
|
+
if (csrfData.token) body.set('_csrf', csrfData.token);
|
|
965
|
+
var url = _tsAuthToken ? tsAuthUrl('/api/settings/project-overrides') : '/api/settings/project-overrides';
|
|
966
|
+
return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
|
|
967
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
968
|
+
if (!data.ok) {
|
|
969
|
+
setSettingsStatus(data.error || 'Failed to update workflow policy', 'err');
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
_settingsLoaded = false;
|
|
973
|
+
loadSettings();
|
|
974
|
+
setSettingsStatus('Workflow policy updated', 'ok');
|
|
975
|
+
}).catch(function(err) {
|
|
976
|
+
setSettingsStatus('Failed: ' + String(err), 'err');
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
882
980
|
function loadSettings() {
|
|
883
981
|
var selectedProject = getSettingsProject();
|
|
884
982
|
var baseUrl = '/api/settings';
|
|
@@ -1058,12 +1156,11 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1058
1156
|
}
|
|
1059
1157
|
retHtml += '</div>';
|
|
1060
1158
|
if (note) retHtml += '<div class="settings-control-note">' + esc(note) + '</div>';
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
}
|
|
1159
|
+
// Show editable inputs for both global and per-project
|
|
1160
|
+
retHtml += '<div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
|
|
1161
|
+
'<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)">' +
|
|
1162
|
+
'<button data-ts-action="' + (isProject ? 'setProjectRetention' : 'setGlobalRetention') + '" data-field="' + esc(field) + '" class="settings-chip active" style="font-size:11px">Set</button>' +
|
|
1163
|
+
'</div>';
|
|
1067
1164
|
retHtml += '</div>';
|
|
1068
1165
|
}
|
|
1069
1166
|
retRow('TTL days', 'ttlDays', ret.ttlDays, 'Memories older than this are eligible for pruning.');
|
|
@@ -1085,14 +1182,11 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1085
1182
|
if (isProject && lctOverride) {
|
|
1086
1183
|
wfHtml += '<button data-ts-action="clearProjectOverride" data-field="lowConfidenceThreshold" class="settings-chip" style="font-size:11px">Clear</button>';
|
|
1087
1184
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
} else {
|
|
1094
|
-
wfHtml += '</div>';
|
|
1095
|
-
}
|
|
1185
|
+
// Show editable inputs for both global and per-project
|
|
1186
|
+
wfHtml += '</div><div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
|
|
1187
|
+
'<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)">' +
|
|
1188
|
+
'<button data-ts-action="' + (isProject ? 'setProjectWorkflow' : 'setGlobalWorkflow') + '" data-field="lowConfidenceThreshold" class="settings-chip active" style="font-size:11px">Set</button>' +
|
|
1189
|
+
'</div>';
|
|
1096
1190
|
wfHtml += '<div class="settings-control-note">Memories below this confidence score are flagged for review.</div></div>';
|
|
1097
1191
|
wfHtml += '<div class="settings-control">';
|
|
1098
1192
|
wfHtml += '<div class="settings-control-header"><span class="settings-control-label">Risky sections</span>' + sourceBadge(riskySectionsOverride);
|
|
@@ -1123,11 +1217,62 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1123
1217
|
} else if (integrationsEl && isProject) {
|
|
1124
1218
|
integrationsEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">Integration settings are global — switch to Global scope to edit them.</div>';
|
|
1125
1219
|
}
|
|
1220
|
+
|
|
1221
|
+
// Load stores if on global scope
|
|
1222
|
+
if (!isProject) {
|
|
1223
|
+
loadStores();
|
|
1224
|
+
}
|
|
1126
1225
|
}).catch(function(err) {
|
|
1127
1226
|
setSettingsStatus('Failed to load settings: ' + String(err), 'err');
|
|
1128
1227
|
});
|
|
1129
1228
|
}
|
|
1130
1229
|
|
|
1230
|
+
function loadStores() {
|
|
1231
|
+
var storesEl = document.getElementById('settings-stores');
|
|
1232
|
+
if (!storesEl) return;
|
|
1233
|
+
var url = _tsAuthToken ? tsAuthUrl('/api/stores') : '/api/stores';
|
|
1234
|
+
fetch(url).then(function(r) { return r.json(); }).then(function(data) {
|
|
1235
|
+
if (!data.ok || !Array.isArray(data.stores)) {
|
|
1236
|
+
storesEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">No team stores configured.</div>';
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (data.stores.length === 0) {
|
|
1240
|
+
storesEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">No team stores configured.</div>';
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
var html = '';
|
|
1244
|
+
data.stores.forEach(function(store) {
|
|
1245
|
+
html += '<div class="settings-control" style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;margin-bottom:12px">';
|
|
1246
|
+
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">';
|
|
1247
|
+
html += '<div>';
|
|
1248
|
+
html += '<div style="font-weight:600;color:var(--ink)">' + esc(store.name) + '</div>';
|
|
1249
|
+
html += '<div style="font-size:var(--text-sm);color:var(--muted)">Role: ' + esc(store.role) + '</div>';
|
|
1250
|
+
html += '</div>';
|
|
1251
|
+
html += '</div>';
|
|
1252
|
+
html += '<div style="font-size:var(--text-xs);color:var(--muted);margin-bottom:10px;font-family:var(--mono);word-break:break-all">' + esc(store.path) + '</div>';
|
|
1253
|
+
html += '<div style="font-size:var(--text-sm);color:var(--ink);margin-bottom:6px;font-weight:500">Projects</div>';
|
|
1254
|
+
if (Array.isArray(store.availableProjects) && store.availableProjects.length > 0) {
|
|
1255
|
+
var subscribed = Array.isArray(store.subscribedProjects) ? store.subscribedProjects : [];
|
|
1256
|
+
html += '<div style="display:flex;flex-direction:column;gap:6px">';
|
|
1257
|
+
store.availableProjects.forEach(function(proj) {
|
|
1258
|
+
var isSubscribed = subscribed.indexOf(proj) !== -1;
|
|
1259
|
+
html += '<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:var(--text-sm)">';
|
|
1260
|
+
html += '<input type="checkbox" class="store-project-checkbox" data-store="' + esc(store.name) + '" data-project="' + esc(proj) + '" ' + (isSubscribed ? 'checked' : '') + ' style="cursor:pointer;width:16px;height:16px">';
|
|
1261
|
+
html += '<span>' + esc(proj) + '</span>';
|
|
1262
|
+
html += '</label>';
|
|
1263
|
+
});
|
|
1264
|
+
html += '</div>';
|
|
1265
|
+
} else {
|
|
1266
|
+
html += '<div style="color:var(--muted);font-size:var(--text-sm)">No projects available</div>';
|
|
1267
|
+
}
|
|
1268
|
+
html += '</div>';
|
|
1269
|
+
});
|
|
1270
|
+
storesEl.innerHTML = html;
|
|
1271
|
+
}).catch(function(err) {
|
|
1272
|
+
storesEl.innerHTML = '<div style="color:var(--error);font-size:var(--text-sm)">Failed to load stores: ' + esc(String(err)) + '</div>';
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1131
1276
|
// Hook into switchTab to lazy-load
|
|
1132
1277
|
var _origSwitchTab = window.switchTab;
|
|
1133
1278
|
var _tasksLoaded = false;
|
|
@@ -1138,6 +1283,43 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1138
1283
|
if (tab === 'settings' && !_settingsLoaded) { _settingsLoaded = true; loadSettings(); }
|
|
1139
1284
|
};
|
|
1140
1285
|
|
|
1286
|
+
// Handle store project checkbox changes
|
|
1287
|
+
document.addEventListener('change', function(e) {
|
|
1288
|
+
var target = e.target;
|
|
1289
|
+
if (target && target.classList && target.classList.contains('store-project-checkbox')) {
|
|
1290
|
+
var storeName = target.getAttribute('data-store');
|
|
1291
|
+
var projectName = target.getAttribute('data-project');
|
|
1292
|
+
var isChecked = target.checked;
|
|
1293
|
+
if (storeName && projectName) {
|
|
1294
|
+
handleStoreProjectToggle(storeName, projectName, isChecked);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
function handleStoreProjectToggle(storeName, projectName, isSubscribing) {
|
|
1300
|
+
var endpoint = isSubscribing ? '/api/stores/subscribe' : '/api/stores/unsubscribe';
|
|
1301
|
+
var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
|
|
1302
|
+
fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
|
|
1303
|
+
var payload = { store: storeName, projects: [projectName] };
|
|
1304
|
+
var body = new URLSearchParams();
|
|
1305
|
+
body.set('store', storeName);
|
|
1306
|
+
body.set('projects', JSON.stringify([projectName]));
|
|
1307
|
+
if (csrfData.token) body.set('_csrf', csrfData.token);
|
|
1308
|
+
var url = _tsAuthToken ? tsAuthUrl(endpoint) : endpoint;
|
|
1309
|
+
return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
|
|
1310
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
1311
|
+
if (!data.ok) {
|
|
1312
|
+
setSettingsStatus((isSubscribing ? 'Subscribe' : 'Unsubscribe') + ' failed: ' + (data.error || ''), 'err');
|
|
1313
|
+
loadStores();
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
setSettingsStatus((isSubscribing ? 'Subscribed to' : 'Unsubscribed from') + ' ' + projectName, 'ok');
|
|
1317
|
+
loadStores();
|
|
1318
|
+
}).catch(function(err) {
|
|
1319
|
+
setSettingsStatus('Request failed: ' + String(err), 'err');
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1141
1323
|
// Event delegation for dynamically generated tasks/settings UI
|
|
1142
1324
|
document.addEventListener('click', function(e) {
|
|
1143
1325
|
var target = e.target;
|
|
@@ -1175,6 +1357,25 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1175
1357
|
var val = inputEl ? inputEl.value : '';
|
|
1176
1358
|
postProjectOverride(proj, field, val, false);
|
|
1177
1359
|
}
|
|
1360
|
+
else if (action === 'setGlobalRetention') {
|
|
1361
|
+
var field = actionEl.getAttribute('data-field') || '';
|
|
1362
|
+
var inputEl = document.getElementById('ret-input-' + field);
|
|
1363
|
+
var val = inputEl ? inputEl.value : '';
|
|
1364
|
+
postGlobalRetention(field, val);
|
|
1365
|
+
}
|
|
1366
|
+
else if (action === 'setProjectRetention') {
|
|
1367
|
+
var proj = getSettingsProject();
|
|
1368
|
+
var field = actionEl.getAttribute('data-field') || '';
|
|
1369
|
+
var inputEl = document.getElementById('ret-input-' + field);
|
|
1370
|
+
var val = inputEl ? inputEl.value : '';
|
|
1371
|
+
postProjectOverride(proj, field, val, false);
|
|
1372
|
+
}
|
|
1373
|
+
else if (action === 'setGlobalWorkflow') {
|
|
1374
|
+
var field = actionEl.getAttribute('data-field') || '';
|
|
1375
|
+
var inputEl = document.getElementById('wf-input-' + field);
|
|
1376
|
+
var val = inputEl ? inputEl.value : '';
|
|
1377
|
+
postGlobalWorkflow(field, val);
|
|
1378
|
+
}
|
|
1178
1379
|
else if (action === 'setProjectWorkflow') {
|
|
1179
1380
|
var proj = getSettingsProject();
|
|
1180
1381
|
var field = actionEl.getAttribute('data-field') || '';
|
|
@@ -1832,7 +2033,7 @@ export function renderGraphHostScript() {
|
|
|
1832
2033
|
if (priorityEl) updates.priority = priorityEl.value;
|
|
1833
2034
|
graphRequest('/api/tasks/update', 'POST', {
|
|
1834
2035
|
project: currentNode.projectName,
|
|
1835
|
-
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
|
|
2036
|
+
item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
|
|
1836
2037
|
text: updates.text,
|
|
1837
2038
|
section: updates.section || '',
|
|
1838
2039
|
priority: updates.priority || ''
|
|
@@ -1869,7 +2070,7 @@ export function renderGraphHostScript() {
|
|
|
1869
2070
|
if (currentNode.kind === 'task') {
|
|
1870
2071
|
graphRequest('/api/tasks/remove', 'POST', {
|
|
1871
2072
|
project: currentNode.projectName,
|
|
1872
|
-
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
|
|
2073
|
+
item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
|
|
1873
2074
|
}).then(function(result) {
|
|
1874
2075
|
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Delete failed');
|
|
1875
2076
|
graphToast('Task removed', 'ok');
|
|
@@ -1884,7 +2085,7 @@ export function renderGraphHostScript() {
|
|
|
1884
2085
|
if (!currentNode) return;
|
|
1885
2086
|
graphRequest('/api/tasks/complete', 'POST', {
|
|
1886
2087
|
project: currentNode.projectName,
|
|
1887
|
-
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
|
|
2088
|
+
item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
|
|
1888
2089
|
}).then(function(result) {
|
|
1889
2090
|
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Update failed');
|
|
1890
2091
|
graphToast('Task completed', 'ok');
|
|
@@ -1898,7 +2099,7 @@ export function renderGraphHostScript() {
|
|
|
1898
2099
|
if (!currentNode) return;
|
|
1899
2100
|
graphRequest('/api/tasks/update', 'POST', {
|
|
1900
2101
|
project: currentNode.projectName,
|
|
1901
|
-
item: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
|
|
2102
|
+
item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
|
|
1902
2103
|
section: section
|
|
1903
2104
|
}).then(function(result) {
|
|
1904
2105
|
if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Update failed');
|
|
@@ -2009,3 +2210,87 @@ export function renderGraphHostScript() {
|
|
|
2009
2210
|
}
|
|
2010
2211
|
})();`;
|
|
2011
2212
|
}
|
|
2213
|
+
export function renderReviewQueueKeyboardScript(_authToken) {
|
|
2214
|
+
return `(function() {
|
|
2215
|
+
var _reviewHighlightIndex = -1;
|
|
2216
|
+
var _currentTab = '';
|
|
2217
|
+
var esc = window._phrenEsc;
|
|
2218
|
+
var authUrl = window._phrenAuthUrl;
|
|
2219
|
+
var authBody = window._phrenAuthBody;
|
|
2220
|
+
var fetchCsrfToken = window._phrenFetchCsrfToken;
|
|
2221
|
+
|
|
2222
|
+
function updateReviewHighlight() {
|
|
2223
|
+
var cards = document.querySelectorAll('#review-cards-list [data-review-card]');
|
|
2224
|
+
cards.forEach(function(card, i) {
|
|
2225
|
+
if (i === _reviewHighlightIndex) {
|
|
2226
|
+
card.classList.add('review-card-highlight');
|
|
2227
|
+
card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
2228
|
+
} else {
|
|
2229
|
+
card.classList.remove('review-card-highlight');
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
function getApproveButton(card) {
|
|
2235
|
+
return card ? card.querySelector('[data-review-type="approve"]') : null;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function getRejectButton(card) {
|
|
2239
|
+
return card ? card.querySelector('[data-review-type="reject"]') : null;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
function triggerCardAction(card, action) {
|
|
2243
|
+
if (!card) return;
|
|
2244
|
+
if (action === 'approve') {
|
|
2245
|
+
var approveBtn = getApproveButton(card);
|
|
2246
|
+
if (approveBtn) approveBtn.click();
|
|
2247
|
+
} else if (action === 'reject') {
|
|
2248
|
+
var rejectBtn = getRejectButton(card);
|
|
2249
|
+
if (rejectBtn) rejectBtn.click();
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
var baseWindowSwitchTab = window.switchTab;
|
|
2254
|
+
if (typeof baseWindowSwitchTab === 'function') {
|
|
2255
|
+
window.switchTab = function(tab) {
|
|
2256
|
+
baseWindowSwitchTab(tab);
|
|
2257
|
+
_currentTab = tab;
|
|
2258
|
+
if (tab === 'review') {
|
|
2259
|
+
_reviewHighlightIndex = -1;
|
|
2260
|
+
setTimeout(function() { updateReviewHighlight(); }, 100);
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
document.addEventListener('keydown', function(e) {
|
|
2266
|
+
if (_currentTab !== 'review') return;
|
|
2267
|
+
var cards = document.querySelectorAll('#review-cards-list [data-review-card]');
|
|
2268
|
+
if (cards.length === 0) return;
|
|
2269
|
+
|
|
2270
|
+
if (e.key === 'j' || e.key === 'ArrowDown') {
|
|
2271
|
+
e.preventDefault();
|
|
2272
|
+
_reviewHighlightIndex = Math.min(_reviewHighlightIndex + 1, cards.length - 1);
|
|
2273
|
+
updateReviewHighlight();
|
|
2274
|
+
} else if (e.key === 'k' || e.key === 'ArrowUp') {
|
|
2275
|
+
e.preventDefault();
|
|
2276
|
+
_reviewHighlightIndex = Math.max(_reviewHighlightIndex - 1, -1);
|
|
2277
|
+
updateReviewHighlight();
|
|
2278
|
+
} else if (e.key === 'a') {
|
|
2279
|
+
e.preventDefault();
|
|
2280
|
+
if (_reviewHighlightIndex >= 0) {
|
|
2281
|
+
triggerCardAction(cards[_reviewHighlightIndex], 'approve');
|
|
2282
|
+
}
|
|
2283
|
+
} else if (e.key === 'd') {
|
|
2284
|
+
e.preventDefault();
|
|
2285
|
+
if (_reviewHighlightIndex >= 0) {
|
|
2286
|
+
triggerCardAction(cards[_reviewHighlightIndex], 'reject');
|
|
2287
|
+
}
|
|
2288
|
+
} else if (e.key === 'Enter') {
|
|
2289
|
+
e.preventDefault();
|
|
2290
|
+
if (_reviewHighlightIndex >= 0) {
|
|
2291
|
+
cards[_reviewHighlightIndex].click();
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
})();`;
|
|
2296
|
+
}
|
|
@@ -6,7 +6,7 @@ import * as path from "path";
|
|
|
6
6
|
import * as querystring from "querystring";
|
|
7
7
|
import { spawn, execFileSync } from "child_process";
|
|
8
8
|
import { computePhrenLiveStateToken, getProjectDirs, } from "../shared.js";
|
|
9
|
-
import { getNonPrimaryStores } from "../store-registry.js";
|
|
9
|
+
import { getNonPrimaryStores, subscribeStoreProjects, unsubscribeStoreProjects } from "../store-registry.js";
|
|
10
10
|
import { editFinding, readReviewQueue, removeFinding, readFindings, addFinding as addFindingStore, readTasksAcrossProjects, addTask as addTaskStore, completeTask as completeTaskStore, removeTask as removeTaskStore, updateTask as updateTaskStore, TASKS_FILENAME, } from "../data/access.js";
|
|
11
11
|
import { isValidProjectName, errorMessage, queueFilePath, safeProjectPath } from "../utils.js";
|
|
12
12
|
import { readInstallPreferences, writeInstallPreferences, writeGovernanceInstallPreferences } from "../init/preferences.js";
|
|
@@ -339,6 +339,28 @@ function handleGetProjects(res, ctx) {
|
|
|
339
339
|
function handleGetChangeToken(res, ctx) {
|
|
340
340
|
jsonOk(res, { token: computePhrenLiveStateToken(ctx.phrenPath) });
|
|
341
341
|
}
|
|
342
|
+
function handleGetStores(res, ctx) {
|
|
343
|
+
try {
|
|
344
|
+
const stores = getNonPrimaryStores(ctx.phrenPath);
|
|
345
|
+
const storeData = stores.map((store) => {
|
|
346
|
+
const availableProjects = getProjectDirs(store.path)
|
|
347
|
+
.map((dir) => path.basename(dir))
|
|
348
|
+
.filter((p) => p !== "global");
|
|
349
|
+
const subscribedProjects = store.projects || [];
|
|
350
|
+
return {
|
|
351
|
+
name: store.name,
|
|
352
|
+
role: store.role,
|
|
353
|
+
path: store.path,
|
|
354
|
+
availableProjects,
|
|
355
|
+
subscribedProjects,
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
jsonOk(res, { ok: true, stores: storeData });
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
jsonErr(res, errorMessage(err), 500);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
342
364
|
function handleGetRuntimeHealth(res, ctx) {
|
|
343
365
|
jsonOk(res, readSyncSnapshot(ctx.phrenPath));
|
|
344
366
|
}
|
|
@@ -816,12 +838,116 @@ function handlePostSettingsMcpEnabled(req, res, url, ctx) {
|
|
|
816
838
|
jsonOk(res, { ok: true, mcpEnabled: enabled });
|
|
817
839
|
});
|
|
818
840
|
}
|
|
841
|
+
function handleGetProfiles(res, ctx) {
|
|
842
|
+
try {
|
|
843
|
+
const { listProfiles } = require("../profile-store.js");
|
|
844
|
+
const profileResult = listProfiles(ctx.phrenPath);
|
|
845
|
+
if (!profileResult.ok) {
|
|
846
|
+
return jsonOk(res, { ok: false, error: profileResult.error, profiles: [] });
|
|
847
|
+
}
|
|
848
|
+
const profiles = profileResult.data || [];
|
|
849
|
+
jsonOk(res, { ok: true, profiles, activeProfile: ctx.profile || undefined });
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
jsonOk(res, { ok: false, error: errorMessage(err), profiles: [] });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function handlePostProfile(req, res, url, ctx) {
|
|
856
|
+
withPostBody(req, res, url, ctx, (parsed) => {
|
|
857
|
+
const profileName = String(parsed.profile || "").trim();
|
|
858
|
+
if (!profileName)
|
|
859
|
+
return jsonErr(res, "Profile name required", 400);
|
|
860
|
+
try {
|
|
861
|
+
const { setMachineProfile, getDefaultMachineAlias } = require("../profile-store.js");
|
|
862
|
+
const machineAlias = getDefaultMachineAlias();
|
|
863
|
+
const result = setMachineProfile(ctx.phrenPath, machineAlias, profileName);
|
|
864
|
+
if (!result.ok)
|
|
865
|
+
return jsonErr(res, result.error || "Failed to switch profile", 500);
|
|
866
|
+
jsonOk(res, { ok: true, message: `Switched to profile: ${profileName}`, profile: profileName });
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
jsonErr(res, errorMessage(err), 500);
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
function handlePostStoreSubscribe(req, res, url, ctx) {
|
|
874
|
+
withPostBody(req, res, url, ctx, (parsed) => {
|
|
875
|
+
const storeName = String(parsed.store || "");
|
|
876
|
+
const projects = Array.isArray(parsed.projects) ? parsed.projects : [];
|
|
877
|
+
if (!storeName)
|
|
878
|
+
return jsonErr(res, "Missing store name", 400);
|
|
879
|
+
try {
|
|
880
|
+
subscribeStoreProjects(ctx.phrenPath, storeName, projects);
|
|
881
|
+
jsonOk(res, { ok: true });
|
|
882
|
+
}
|
|
883
|
+
catch (err) {
|
|
884
|
+
jsonErr(res, errorMessage(err), 500);
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function handlePostStoreUnsubscribe(req, res, url, ctx) {
|
|
889
|
+
withPostBody(req, res, url, ctx, (parsed) => {
|
|
890
|
+
const storeName = String(parsed.store || "");
|
|
891
|
+
const projects = Array.isArray(parsed.projects) ? parsed.projects : [];
|
|
892
|
+
if (!storeName)
|
|
893
|
+
return jsonErr(res, "Missing store name", 400);
|
|
894
|
+
try {
|
|
895
|
+
unsubscribeStoreProjects(ctx.phrenPath, storeName, projects);
|
|
896
|
+
jsonOk(res, { ok: true });
|
|
897
|
+
}
|
|
898
|
+
catch (err) {
|
|
899
|
+
jsonErr(res, errorMessage(err), 500);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
819
903
|
function handlePostSettingsProjectOverrides(req, res, url, ctx) {
|
|
820
904
|
withPostBody(req, res, url, ctx, (parsed) => {
|
|
905
|
+
const globalUpdate = String(parsed.globalUpdate || "") === "true";
|
|
821
906
|
const project = String(parsed.project || "");
|
|
822
907
|
const field = String(parsed.field || "");
|
|
823
908
|
const value = String(parsed.value || "");
|
|
824
909
|
const clearField = String(parsed.clear || "") === "true";
|
|
910
|
+
const NUMERIC_RETENTION_FIELDS = ["ttlDays", "retentionDays", "autoAcceptThreshold", "minInjectConfidence"];
|
|
911
|
+
const NUMERIC_WORKFLOW_FIELDS = ["lowConfidenceThreshold"];
|
|
912
|
+
// Handle global retention/workflow policy updates
|
|
913
|
+
if (globalUpdate) {
|
|
914
|
+
try {
|
|
915
|
+
if (NUMERIC_RETENTION_FIELDS.includes(field)) {
|
|
916
|
+
const { updateRetentionPolicy } = require("../governance/policy.js");
|
|
917
|
+
let updateData = {};
|
|
918
|
+
if (!clearField) {
|
|
919
|
+
const num = parseFloat(value);
|
|
920
|
+
if (!Number.isFinite(num) || num < 0)
|
|
921
|
+
throw new Error("Invalid numeric value for " + field);
|
|
922
|
+
updateData[field] = num;
|
|
923
|
+
}
|
|
924
|
+
const result = updateRetentionPolicy(ctx.phrenPath, updateData);
|
|
925
|
+
if (!result.ok)
|
|
926
|
+
return jsonErr(res, result.error || "Failed to update retention policy", 500);
|
|
927
|
+
return jsonOk(res, { ok: true, retentionPolicy: result.data });
|
|
928
|
+
}
|
|
929
|
+
else if (NUMERIC_WORKFLOW_FIELDS.includes(field)) {
|
|
930
|
+
const { updateWorkflowPolicy } = require("../governance/policy.js");
|
|
931
|
+
let updateData = {};
|
|
932
|
+
if (!clearField) {
|
|
933
|
+
const num = parseFloat(value);
|
|
934
|
+
if (!Number.isFinite(num) || num < 0 || num > 1)
|
|
935
|
+
throw new Error("Invalid value for " + field + " (must be 0-1)");
|
|
936
|
+
updateData[field] = num;
|
|
937
|
+
}
|
|
938
|
+
const result = updateWorkflowPolicy(ctx.phrenPath, updateData);
|
|
939
|
+
if (!result.ok)
|
|
940
|
+
return jsonErr(res, result.error || "Failed to update workflow policy", 500);
|
|
941
|
+
return jsonOk(res, { ok: true, workflowPolicy: result.data });
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
return jsonErr(res, "Unknown config field: " + field, 400);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
catch (err) {
|
|
948
|
+
return jsonErr(res, errorMessage(err));
|
|
949
|
+
}
|
|
950
|
+
}
|
|
825
951
|
if (!project || !isValidProjectName(project))
|
|
826
952
|
return jsonErr(res, "Invalid project name", 400);
|
|
827
953
|
const registeredProjects = getProjectDirs(ctx.phrenPath, ctx.profile).map((d) => path.basename(d)).filter((p) => p !== "global");
|
|
@@ -831,8 +957,6 @@ function handlePostSettingsProjectOverrides(req, res, url, ctx) {
|
|
|
831
957
|
proactivity: ["high", "medium", "low"], proactivityFindings: ["high", "medium", "low"],
|
|
832
958
|
proactivityTask: ["high", "medium", "low"], taskMode: ["off", "manual", "suggest", "auto"],
|
|
833
959
|
};
|
|
834
|
-
const NUMERIC_RETENTION_FIELDS = ["ttlDays", "retentionDays", "autoAcceptThreshold", "minInjectConfidence"];
|
|
835
|
-
const NUMERIC_WORKFLOW_FIELDS = ["lowConfidenceThreshold"];
|
|
836
960
|
try {
|
|
837
961
|
updateProjectConfigOverrides(ctx.phrenPath, project, (current) => {
|
|
838
962
|
const next = { ...current };
|
|
@@ -944,6 +1068,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
944
1068
|
switch (pathname) {
|
|
945
1069
|
case "/api/projects": return handleGetProjects(res, ctx);
|
|
946
1070
|
case "/api/change-token": return handleGetChangeToken(res, ctx);
|
|
1071
|
+
case "/api/stores": return handleGetStores(res, ctx);
|
|
947
1072
|
case "/api/runtime-health": return handleGetRuntimeHealth(res, ctx);
|
|
948
1073
|
case "/api/review-queue": return handleGetReviewQueue(res, ctx);
|
|
949
1074
|
case "/api/review-activity": return handleGetReviewActivity(res, ctx);
|
|
@@ -957,6 +1082,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
957
1082
|
case "/api/settings": return handleGetSettings(res, url, ctx);
|
|
958
1083
|
case "/api/config": return handleGetConfig(res, url, ctx);
|
|
959
1084
|
case "/api/csrf-token": return handleGetCsrfToken(res, ctx);
|
|
1085
|
+
case "/api/profiles": return handleGetProfiles(res, ctx);
|
|
960
1086
|
case "/api/search": return await handleGetSearch(res, url, ctx);
|
|
961
1087
|
}
|
|
962
1088
|
// Prefix-matched GET routes
|
|
@@ -979,6 +1105,9 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
979
1105
|
case "/api/skill-save": return handlePostSkillSave(req, res, url, ctx);
|
|
980
1106
|
case "/api/skill-toggle": return handlePostSkillToggle(req, res, url, ctx);
|
|
981
1107
|
case "/api/hook-toggle": return handlePostHookToggle(req, res, url, ctx);
|
|
1108
|
+
case "/api/profile": return handlePostProfile(req, res, url, ctx);
|
|
1109
|
+
case "/api/stores/subscribe": return handlePostStoreSubscribe(req, res, url, ctx);
|
|
1110
|
+
case "/api/stores/unsubscribe": return handlePostStoreUnsubscribe(req, res, url, ctx);
|
|
982
1111
|
case "/api/project-topics/save": return handlePostTopicsSave(req, res, url, ctx);
|
|
983
1112
|
case "/api/project-topics/reclassify": return handlePostTopicsReclassify(req, res, url, ctx);
|
|
984
1113
|
case "/api/project-topics/pin": return handlePostTopicsPin(req, res, url, ctx);
|
|
@@ -655,4 +655,10 @@ export const REVIEW_UI_STYLES = `
|
|
|
655
655
|
.review-sync-indicator.error {
|
|
656
656
|
background: var(--danger);
|
|
657
657
|
}
|
|
658
|
+
/* ── Review Card Highlight (keyboard shortcuts) ──── */
|
|
659
|
+
.review-card-highlight {
|
|
660
|
+
border: 2px solid var(--blue) !important;
|
|
661
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.08), var(--shadow) !important;
|
|
662
|
+
background: color-mix(in srgb, var(--surface) 98%, var(--blue)) !important;
|
|
663
|
+
}
|
|
658
664
|
`;
|