saga-mcp 1.7.0 → 1.7.1

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 CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/saga-mcp)](https://www.npmjs.com/package/saga-mcp)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/saga-mcp)](https://www.npmjs.com/package/saga-mcp)
5
+ [![license](https://img.shields.io/npm/l/saga-mcp)](https://github.com/spranab/saga-mcp/blob/master/LICENSE)
5
6
  [![IdeaCred](https://ideacred.com/api/badge/spranab/saga-mcp)](https://ideacred.com/profile/spranab)
6
7
 
7
8
  Your coding agent loses the plan between sessions. You come back tomorrow and
@@ -386,6 +387,7 @@ What you get:
386
387
  - **Notes** and **Activity** — decisions and the complete change history
387
388
  - **Task drawer** — edit any field, tick subtasks, comment, remove or restore a comment
388
389
  - **Project switcher** — every project in the database, so one central `.tracker.db` covers all your repos; every tab, including Activity, is scoped to the selected project
390
+ - **Shareable, refreshable URLs** — the open project, tab and task live in the address bar, so a browser refresh puts you back where you were and back/forward move between tasks. A ⟳ button in the task drawer re-reads that task without a page reload, for picking up what an agent just wrote
389
391
 
390
392
  Writes from the UI call the *same handlers* the MCP tools do, so edits you make by hand are
391
393
  validated identically and land in the same activity log as the agent's — an agent calling
package/dist/web/ui.js CHANGED
@@ -400,6 +400,39 @@ function modal(title, fields, submitLabel, onSubmit) {
400
400
 
401
401
  /* ---------- shell ---------- */
402
402
 
403
+ /* ---------- url state ---------- */
404
+
405
+ /**
406
+ * Project, tab and the open task live in the location hash. A browser refresh
407
+ * then puts you back exactly where you were instead of dumping you on the
408
+ * overview with the task drawer closed, and back/forward work.
409
+ */
410
+ var applyingHash = false;
411
+
412
+ function syncHash() {
413
+ if (applyingHash) return;
414
+ var parts = [];
415
+ if (S.projectId) parts.push('p=' + S.projectId);
416
+ if (S.tab && S.tab !== 'overview') parts.push('tab=' + S.tab);
417
+ if (S.task) parts.push('task=' + S.task.id);
418
+ var next = parts.length ? '#' + parts.join('&') : '#';
419
+ if (next !== location.hash) history.replaceState(null, '', next);
420
+ }
421
+
422
+ function readHash() {
423
+ var out = {};
424
+ var raw = location.hash.replace(/^#/, '');
425
+ if (!raw) return out;
426
+ raw.split('&').forEach(function (pair) {
427
+ var i = pair.indexOf('=');
428
+ if (i < 0) return;
429
+ var k = pair.slice(0, i), v = pair.slice(i + 1);
430
+ if (k === 'p' || k === 'task') { var n = Number(v); if (n > 0) out[k] = n; }
431
+ if (k === 'tab' && TABS.some(function (t) { return t[0] === v; })) out.tab = v;
432
+ });
433
+ return out;
434
+ }
435
+
403
436
  function renderTabs() {
404
437
  el('tabs').innerHTML = TABS.map(function (t) {
405
438
  return '<button data-tab="' + t[0] + '" class="' + (S.tab === t[0] ? 'active' : '') + '">' + t[1] + '</button>';
@@ -447,6 +480,7 @@ function refresh() {
447
480
 
448
481
  function render() {
449
482
  renderTabs();
483
+ syncHash();
450
484
  el('roBadge').hidden = !S.readOnly;
451
485
  if (S.query) return renderSearch();
452
486
  var fns = { overview: viewOverview, board: viewBoard, epics: viewEpics,
@@ -680,11 +714,13 @@ function openTask(id) {
680
714
  return get('/api/tasks/' + id + (S.showDeleted ? '?include_deleted=1' : '')).then(function (t) {
681
715
  S.task = t;
682
716
  drawTask();
717
+ syncHash();
683
718
  }).catch(oops);
684
719
  }
685
720
 
686
721
  function closeDrawer() {
687
722
  S.task = null;
723
+ syncHash();
688
724
  var d = document.querySelector('.drawer');
689
725
  var b = document.querySelector('.drawer-backdrop');
690
726
  if (d) d.remove();
@@ -710,6 +746,7 @@ function drawTask() {
710
746
  d.className = 'drawer';
711
747
 
712
748
  var h = '<div class="row"><span class="grow"></span>' +
749
+ '<button class="btn" id="refreshTask" title="Re-read this task from the database">⟳ Refresh</button>' +
713
750
  (ed ? '<button class="btn" id="editTask">Edit task</button>' : '') +
714
751
  '<button class="btn" id="closeDrawer">Close ✕</button></div>';
715
752
  h += '<h2 style="margin:6px 0 8px;font-size:18px">' + esc(t.title) + '</h2>';
@@ -951,6 +988,12 @@ document.addEventListener('click', function (ev) {
951
988
  if (tabBtn) { S.tab = tabBtn.dataset.tab; S.query = ''; el('q').value = ''; render(); return; }
952
989
 
953
990
  if (target.id === 'closeDrawer') return closeDrawer();
991
+ if (target.id === 'refreshTask') {
992
+ var btn = target;
993
+ btn.disabled = true;
994
+ btn.textContent = '⟳ …';
995
+ return refresh().then(function () { toast('Refreshed'); }).catch(oops);
996
+ }
954
997
  if (target.id === 'clearSearch') { S.query = ''; el('q').value = ''; render(); return; }
955
998
  if (target.id === 'reload') { toast('Reloaded'); refresh(); return; }
956
999
  if (target.id === 'expandAll') { S.overview.epics.forEach(function (e) { S.epicOpen[e.id] = true; }); render(); return; }
@@ -1177,16 +1220,50 @@ document.addEventListener('drop', function (ev) {
1177
1220
 
1178
1221
  /* ---------- boot ---------- */
1179
1222
 
1223
+ // Back and forward move between tasks and tabs rather than leaving the page.
1224
+ // Registered before boot so it survives a failed first fetch.
1225
+ window.addEventListener('hashchange', function () {
1226
+ var want = readHash();
1227
+ var openId = S.task ? S.task.id : null;
1228
+ if (want.task === openId && (want.tab || 'overview') === S.tab &&
1229
+ (!want.p || want.p === S.projectId)) return;
1230
+
1231
+ applyingHash = true;
1232
+ try {
1233
+ if (want.p && want.p !== S.projectId && S.projects.some(function (p) { return p.id === want.p; })) {
1234
+ S.projectId = want.p;
1235
+ el('projectSel').value = String(want.p);
1236
+ S.epicOpen = {};
1237
+ loadProject();
1238
+ }
1239
+ S.tab = want.tab || 'overview';
1240
+ render();
1241
+ if (want.task) openTask(want.task);
1242
+ else if (S.task) closeDrawer();
1243
+ } finally {
1244
+ applyingHash = false;
1245
+ }
1246
+ });
1247
+
1180
1248
  get('/api/projects').then(function (r) {
1181
1249
  S.projects = r.projects;
1182
1250
  S.readOnly = !!r.read_only;
1183
1251
  el('dbpath').textContent = r.db_path;
1184
1252
  el('roBadge').hidden = !S.readOnly;
1185
1253
  if (S.readOnly) el('newProject').hidden = true;
1186
- if (S.projects.length) S.projectId = S.projects[0].id;
1254
+
1255
+ var want = readHash();
1256
+ var known = S.projects.some(function (p) { return p.id === want.p; });
1257
+ S.projectId = known ? want.p : (S.projects.length ? S.projects[0].id : null);
1258
+ if (want.tab) S.tab = want.tab;
1259
+
1187
1260
  renderProjects();
1188
- loadProject();
1261
+ loadProject().then(function () {
1262
+ // Reopen whatever task the URL names, so a browser refresh keeps your place.
1263
+ if (want.task) return openTask(want.task);
1264
+ });
1189
1265
  }).catch(fail);
1266
+
1190
1267
  </script>
1191
1268
  </body>
1192
1269
  </html>`;
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,IAAI,GAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA8pCpB,CAAC"}
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,IAAI,GAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA2uCpB,CAAC"}
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "saga-mcp",
4
4
  "display_name": "Saga — Project Tracker for AI Agents",
5
- "version": "1.7.0",
5
+ "version": "1.7.1",
6
6
  "description": "A Jira-like project tracker MCP server for AI agents. SQLite-backed, per-project scoped, with full hierarchy and activity logging — so LLMs never lose track.",
7
7
  "long_description": "Saga gives your AI assistant a structured SQLite database to track projects, epics, tasks, subtasks, notes, and decisions across sessions. No more scattered markdown files — one `tracker_dashboard` call gives full project context to resume work.\n\n**Key features:**\n- Full hierarchy: Projects > Epics > Tasks > Subtasks\n- Task dependencies: Express sequencing with auto-block/unblock\n- Comments: Threaded discussions on tasks for decision trails, with reversible soft-delete\n- Templates: Reusable task sets with variable substitution\n- Dashboard: One tool call gives full overview with natural language summary\n- SQLite: Self-contained `.tracker.db` file per project — zero setup\n- Activity log: Every mutation is automatically tracked\n- Notes system: Decisions, context, meeting notes, blockers\n- Batch operations: Create multiple subtasks or update multiple tasks in one call\n- 33 focused tools with safety annotations\n- Web UI: `saga-web` serves a local dashboard for browsing and editing the same database\n- Import/export: Full project backup and migration as JSON",
8
8
  "author": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "saga-mcp",
3
3
  "mcpName": "io.github.spranab/saga-mcp",
4
- "version": "1.7.0",
4
+ "version": "1.7.1",
5
5
  "description": "A Jira-like project tracker MCP server for AI agents. SQLite-backed, per-project scoped, with full hierarchy (Projects > Epics > Tasks > Subtasks), activity logging, and a dashboard \u2014 so LLMs never lose track.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",