memoir-cli 3.12.0 → 3.15.0

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 (74) hide show
  1. package/README.md +128 -137
  2. package/bin/memoir-work.js +9 -0
  3. package/bin/memoir.js +50 -8
  4. package/docs/AUDIT-REMEDIATION.md +55 -0
  5. package/docs/CASE_TAPE_AMNESIA.md +39 -0
  6. package/docs/HANDOFF-SECURITY-AUDIT.md +106 -0
  7. package/docs/LOCAL-HANDOFF-VALIDATION.md +129 -0
  8. package/docs/MCP-V2-MIGRATION.md +17 -0
  9. package/docs/PROJECT-HANDOFF.md +301 -0
  10. package/docs/PROJECT-MAP-TRIAL.md +149 -0
  11. package/docs/PROJECT-VIEW-DEBUG.md +66 -0
  12. package/docs/PROJECT-VIEW-VALIDATION.md +136 -0
  13. package/docs/RELEASE-3.14-VALIDATION.md +36 -0
  14. package/docs/RELIABILITY-ROLLOUT.md +57 -0
  15. package/docs/RETRIEVAL-INDEX.md +45 -0
  16. package/docs/RETRIEVAL-RESULTS.md +26 -0
  17. package/docs/SPEC.md +684 -0
  18. package/evals/CONTINUITY-PROTOCOL.md +45 -0
  19. package/evals/cases.json +200 -0
  20. package/evals/results/retrieval-2026-09-05.json +5333 -0
  21. package/evals/retrieval-performance.mjs +99 -0
  22. package/evals/run.mjs +87 -0
  23. package/package.json +13 -5
  24. package/src/adapters/index.js +13 -6
  25. package/src/adapters/restore.js +83 -36
  26. package/src/cloud/storage.js +130 -93
  27. package/src/commands/activate.js +18 -7
  28. package/src/commands/cloud.js +55 -4
  29. package/src/commands/consolidate.js +49 -10
  30. package/src/commands/diff.js +2 -2
  31. package/src/commands/doctor.js +3 -3
  32. package/src/commands/push.js +156 -161
  33. package/src/commands/recall.js +1 -1
  34. package/src/commands/restore.js +32 -44
  35. package/src/commands/resume.js +15 -164
  36. package/src/commands/session.js +51 -9
  37. package/src/commands/snapshot.js +6 -7
  38. package/src/commands/status.js +23 -1
  39. package/src/commands/upgrade.js +11 -9
  40. package/src/commands/validate.js +3 -0
  41. package/src/commands/view.js +2 -2
  42. package/src/commands/why.js +4 -3
  43. package/src/config.js +9 -40
  44. package/src/context/capture.js +126 -32
  45. package/src/context/handoffs.js +72 -0
  46. package/src/events/summary.js +122 -0
  47. package/src/integrations/setup.js +88 -0
  48. package/src/mcp.js +105 -152
  49. package/src/memory/lexical-index.js +65 -0
  50. package/src/memory/repository.js +16 -0
  51. package/src/memory/scope.js +65 -0
  52. package/src/memory/search.js +165 -70
  53. package/src/memory/store.js +141 -0
  54. package/src/providers/index.js +182 -51
  55. package/src/providers/restore.js +5 -1
  56. package/src/security/encryption.js +34 -60
  57. package/src/security/files.js +155 -0
  58. package/src/session/brief.js +47 -0
  59. package/src/session/inject.js +12 -6
  60. package/src/session/lock.js +39 -118
  61. package/src/session/migrations.js +6 -0
  62. package/src/session/render.js +34 -4
  63. package/src/session/state.js +200 -33
  64. package/src/work/cli.js +64 -0
  65. package/src/work/errors.js +8 -0
  66. package/src/work/server.js +28 -0
  67. package/src/work/setup.js +96 -0
  68. package/src/work/store.js +340 -0
  69. package/src/work/ui/app.js +398 -0
  70. package/src/work/ui/index.html +45 -0
  71. package/src/work/ui/style.css +248 -0
  72. package/src/work/view.js +93 -0
  73. package/src/workspace/tracker.js +84 -332
  74. package/supabase/migrations/202609050001_backup_versions.sql +50 -0
@@ -0,0 +1,248 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ --ink: #eceeea;
4
+ --muted: #a0a59f;
5
+ --paper: #151816;
6
+ --panel: #1b1e1b;
7
+ --line: #343a34;
8
+ --accent: #c6dda5;
9
+ --soft: #2d3828;
10
+ --warn: #e7c48d;
11
+ --warn-bg: #382f20;
12
+ font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
13
+ }
14
+ * { box-sizing: border-box; }
15
+ body { margin: 0; background: var(--paper); color: var(--ink); }
16
+ button, input, select, textarea { font: inherit; }
17
+ button { cursor: pointer; border: 1px solid transparent; border-radius: 6px; padding: 8px 12px; font-weight: 500; color: inherit; background: var(--panel); transition: background .15s, border-color .15s; }
18
+ button:hover { background: #30372e; border-color: #59644f; }
19
+ button:disabled { opacity: .5; cursor: default; }
20
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
21
+ [hidden] { display: none !important; }
22
+ .workspace-bar { min-height: 68px; padding: 12px 32px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 20px; }
23
+ .workspace-brand { display: inline-flex; align-items: center; gap: 10px; color: var(--ink); text-decoration: none; font-size: 22px; letter-spacing: -.8px; font-weight: 600; }
24
+ .workspace-brand > span { color: var(--accent); font: 700 28px/1 Georgia,serif; border: 1px solid #617050; border-radius: 6px 6px 2px 2px; width: 30px; height: 31px; text-align: center; padding-bottom: 3px; }
25
+ .workspace-divider { color: #737a6d; font-size: 21px; font-weight: 300; }
26
+ #project { display: flex; align-items: center; gap: 12px; min-width: 0; }
27
+ .project-name { font-size: 13px; font-weight: 550; }
28
+ .branch-name { color: var(--muted); font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; border: 1px solid var(--line); padding: 3px 8px; border-radius: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 230px; }
29
+ .workspace-local { margin-left: auto; display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 11px; white-space: nowrap; }
30
+ .workspace-local i { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
31
+ .quiet { background: transparent; border-color: var(--line); font-size: 12px; }
32
+ #refresh { margin-left: auto; }
33
+ .workspace-local + #refresh { margin-left: 0; }
34
+ .workspace { max-width: 1380px; margin: auto; padding: 28px 32px 24px; }
35
+ .workspace-heading { display: flex; align-items: center; justify-content: space-between; gap: 28px; margin-bottom: 30px; }
36
+ .eyebrow { margin: 0 0 10px; color: #b4c49e; font-size: 10px; font-weight: 550; letter-spacing: 1.6px; }
37
+ h1 { font-size: clamp(29px, 3vw, 38px); line-height: 1.15; letter-spacing: -1.2px; margin: 0 0 12px; font-weight: 550; }
38
+ .subtitle { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.7; }
39
+ .primary, .map-add { background: var(--accent); color: #20271a; border-color: var(--accent); font-size: 12px; padding: 10px 16px; white-space: nowrap; }
40
+ .primary:hover, .map-add:hover { background: #d5e9b9; border-color: #d5e9b9; }
41
+ .workspace-tools { display: flex; align-items: center; gap: 18px; margin-bottom: 18px; }
42
+ .search-label { position: relative; display: block; flex: 1; max-width: 470px; }
43
+ .search-icon { position: absolute; top: 15px; left: 15px; width: 12px; height: 12px; border: 1.5px solid var(--muted); border-radius: 50%; }
44
+ .search-icon::after { content: ''; position: absolute; width: 5px; height: 1.5px; background: var(--muted); transform: rotate(45deg); right: -4px; bottom: -2px; }
45
+ input[type=search] { background: var(--panel); border: 1px solid var(--line); border-radius: 7px; padding: 11px 15px 11px 39px; width: 100%; font-size: 12px; color: var(--ink); }
46
+ input::placeholder { color: #9ba296; }
47
+ .view-switch { display: flex; gap: 3px; padding: 4px; background: #101310; border: 1px solid var(--line); border-radius: 7px; margin-left: auto; }
48
+ .view-switch button { color: var(--muted); background: transparent; font-size: 12px; padding: 6px 17px; border-radius: 4px; border: 0; }
49
+ .view-switch button[aria-pressed=true] { color: var(--ink); background: #343c2f; }
50
+ nav { display: flex; align-items: center; gap: 20px; border-bottom: 1px solid var(--line); overflow-x: auto; scrollbar-width: thin; scrollbar-color: #535d49 transparent; }
51
+ nav button { display: flex; align-items: center; gap: 8px; background: transparent; color: var(--muted); padding: 10px 1px 15px; font-size: 12px; border: 0; border-radius: 0; white-space: nowrap; }
52
+ nav button:hover { background: transparent; color: var(--ink); }
53
+ nav button[aria-current=page] { color: var(--accent); box-shadow: inset 0 -2px var(--accent); }
54
+ .nav-icon { display: none; }
55
+ nav .count { color: #b6bcae; font-size: 10px; background: #2b3028; padding: 0 5px; border-radius: 4px; font-variant-numeric: tabular-nums; }
56
+ .review-summary { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 0 24px; }
57
+ .summary-strip { display: flex; flex-wrap: wrap; gap: 8px 24px; padding: 16px 0; flex: 1; }
58
+ #goal { max-width: 440px; }
59
+ .summary-item { display: flex; align-items: center; gap: 7px; padding: 0; background: transparent; border: 0; font-size: 12px; color: var(--muted); font-weight: 400; }
60
+ .summary-item:hover { background: transparent; color: var(--ink); }
61
+ .summary-item strong { color: var(--ink); font-size: 13px; font-weight: 550; font-variant-numeric: tabular-nums; }
62
+ .summary-item.needs-attention strong { color: var(--warn); }
63
+ .summary-arrow { font-size: 11px; color: #a9b19c; }
64
+ .goal { padding: 16px 0; }
65
+ .goal summary { cursor: pointer; color: #c8d3b9; font-size: 12px; }
66
+ .goal p { margin: 12px 0; font-size: 13px; line-height: 1.7; overflow-wrap: anywhere; }
67
+ .section-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin: 18px 0; }
68
+ h2 { font-size: 19px; letter-spacing: -.4px; margin: 0; font-weight: 550; }
69
+ .section-heading p { color: var(--muted); font-size: 12px; margin: 4px 0 0; }
70
+ #view-caption { font-size: 11px; color: var(--muted); white-space: nowrap; }
71
+ .group { margin-bottom: 26px; }
72
+ .group-title { display: flex; justify-content: space-between; align-items: center; margin-bottom: 9px; }
73
+ .group-title h3 { font-size: 12px; font-weight: 500; margin: 0; color: #bcc7ae; }
74
+ .group-title button { font-size: 11px; padding: 3px 0 3px 10px; background: transparent; color: var(--muted); border: 0; }
75
+ .cards { display: grid; grid-template-columns: 1fr; gap: 0; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; background: var(--panel); }
76
+ .card { padding: 20px 24px 17px; min-width: 0; }
77
+ .cards > .card + .card { border-top: 1px solid var(--line); }
78
+ .card-heading { display: flex; align-items: center; gap: 12px; margin-bottom: 7px; }
79
+ .record-date { font-size: 11px; color: var(--muted); margin-left: auto; }
80
+ .card h3 { font-size: 14px; margin: 5px 0 8px; font-weight: 550; line-height: 1.6; overflow-wrap: anywhere; color: var(--ink); }
81
+ .card p { margin: 8px 0; overflow-wrap: anywhere; }
82
+ .record-text, .card .answer { font-size: 13px; line-height: 1.75; color: #bdc4b6; white-space: pre-wrap; max-width: 100ch; }
83
+ .has-preview:not(.expanded) .record-copy p, .has-preview:not(.expanded) .record-copy h3 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
84
+ .read-more { color: #c4d3ad; font-size: 11px; padding: 2px 0; background: transparent; border: 0; }
85
+ .read-more:hover { background: transparent; text-decoration: underline; }
86
+ .badge { display: inline-flex; align-items: center; padding: 2px 6px; border-radius: 3px; background: #2c3626; color: #c5d6b0; font-size: 9px; letter-spacing: .5px; font-weight: 550; }
87
+ .badge.warn { background: var(--warn-bg); color: var(--warn); }
88
+ .badge.neutral, .done .badge { background: #30332e; color: var(--muted); }
89
+ .card-actions { display: flex; align-items: center; gap: 16px; margin-top: 12px; flex-wrap: wrap; }
90
+ .card-actions button { font-size: 11px; padding: 2px 0; background: transparent; border: 0; border-radius: 2px; color: #c0c9b5; }
91
+ .card-actions button:hover { color: var(--accent); text-decoration: underline; }
92
+ .card-actions .remove { margin-left: auto; color: #a0a69a; }
93
+ #content[data-compact=true] .card.has-preview:not(.expanded) { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 2px 18px; padding: 15px 20px; }
94
+ #content[data-compact=true] .card.has-preview:not(.expanded) .card-heading { margin: 0; }
95
+ #content[data-compact=true] .card.has-preview:not(.expanded) .record-date { display: none; }
96
+ #content[data-compact=true] .card.has-preview:not(.expanded) .record-copy { grid-column: 1; }
97
+ #content[data-compact=true] .card.has-preview:not(.expanded) .record-copy h3 { margin: 4px 0 0; }
98
+ #content[data-compact=true] .card.has-preview:not(.expanded) .record-copy p,
99
+ #content[data-compact=true] .card.has-preview:not(.expanded) .card-actions,
100
+ #content[data-compact=true] .card.has-preview:not(.expanded) .metadata { display: none; }
101
+ #content[data-compact=true] .card.has-preview:not(.expanded) .read-more { grid-column: 2; grid-row: 1 / 3; align-self: center; border: 1px solid var(--line); padding: 6px 12px; }
102
+ .archive-group { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 14px; }
103
+ .archive-group > summary { cursor: pointer; color: #b8c1ad; font-size: 12px; }
104
+ .archive-group > .group { margin-top: 16px; }
105
+ .metadata { font-size: 11px; color: var(--muted); margin-top: 12px; }
106
+ .metadata p { white-space: pre-wrap; line-height: 1.8; }
107
+ .metadata summary { cursor: pointer; color: #a8b29d; }
108
+ .metadata[open] summary { color: var(--accent); }
109
+ .metadata ul { padding-left: 19px; }
110
+ .metadata code { font-size: 10px; word-break: break-all; }
111
+ .metadata li { overflow-wrap: anywhere; }
112
+ .reasons { border-left: 2px solid #b29768; padding-left: 11px; color: var(--warn); font-size: 12px; list-style: none; overflow-wrap: anywhere; }
113
+ .hidden-note { color: var(--muted); font-size: 12px; line-height: 1.7; }
114
+ .done .record-copy p { color: var(--muted); }
115
+ .empty { padding: 45px 25px; border: 1px solid var(--line); background: var(--panel); border-radius: 9px; color: var(--muted); text-align: center; font-size: 13px; }
116
+ .empty strong { display: block; color: var(--ink); margin-bottom: 6px; font-size: 16px; font-weight: 500; }
117
+ .notice { padding: 12px 16px; color: #d3e4c8; background: #253320; border: 1px solid #46583b; border-radius: 7px; font-size: 12px; margin-bottom: 20px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
118
+ .notice.error { background: #38271f; color: #f0cab4; border-color: #785641; }
119
+ .notice button { font-size: 12px; background: transparent; text-decoration: underline; }
120
+ footer { margin-top: 28px; border-top: 1px solid var(--line); padding-top: 18px; color: var(--muted); font-size: 11px; display: flex; justify-content: space-between; align-items: flex-start; gap: 22px; flex-wrap: wrap; }
121
+ footer details { max-width: 550px; }
122
+ footer summary { cursor: pointer; }
123
+ footer p { line-height: 1.8; }
124
+ .map-controls { display: flex; flex-wrap: wrap; gap: 10px 18px; align-items: center; justify-content: space-between; margin-bottom: 14px; }
125
+ .map-controls p { margin: 0; font-size: 12px; color: var(--muted); }
126
+ #map-suggestions { padding: 6px 10px; background: transparent; border-color: var(--line); color: #c5cdba; font-size: 11px; }
127
+ #map-suggestions[aria-pressed=true] { color: var(--accent); border-color: #81946b; background: var(--soft); }
128
+ .map-layout { display: grid; grid-template-columns: minmax(0,1fr) 320px; gap: 18px; align-items: start; }
129
+ .map-surface { position: relative; min-width: 0; background: var(--panel); border: 1px solid var(--line); border-radius: 9px; overflow: hidden; }
130
+ .canvas-caption { position: relative; z-index: 1; padding: 17px 20px 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--muted); font-size: 11px; }
131
+ .canvas-caption button { background: transparent; color: #c2cbb6; font-size: 11px; border: 0; padding: 3px 0; }
132
+ .map-canvas { position: relative; height: 440px; isolation: isolate; background-image: radial-gradient(#87937b25 .7px,transparent .7px); background-size: 22px 22px; }
133
+ .map-lines { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
134
+ .map-line { stroke: #7b896e; stroke-width: 1; fill: none; vector-effect: non-scaling-stroke; opacity: .4; }
135
+ .map-line.suggested { stroke: #b3a6c6; stroke-dasharray: 3 6; opacity: .5; }
136
+ .map-line.focused { stroke: #bed49d; stroke-width: 1.5; opacity: .9; }
137
+ .map-node { position: absolute; z-index: 1; transform: translate(-50%,-12px); width: 128px; padding: 0 5px 5px; border: 0; background: transparent; text-align: center; border-radius: 7px; color: #d6decf; --node-color: #b9a8d2; }
138
+ .map-node:hover { background: transparent; color: white; }
139
+ .map-node[data-kind=next], .index-entry[data-kind=next] { --node-color: #cbd69d; }
140
+ .map-node[data-kind=answer], .index-entry[data-kind=answer] { --node-color: #9ccbb0; }
141
+ .map-node[data-kind=check], .index-entry[data-kind=check] { --node-color: #9ebfd7; }
142
+ .map-node[data-kind=goal], .index-entry[data-kind=goal] { --node-color: #d7b397; }
143
+ .map-node[data-kind=project] { --node-color: var(--accent); transform: translate(-50%,-28px); width: 135px; }
144
+ .node-dot { display: block; width: 10px; height: 10px; border-radius: 50%; margin: 5px auto 10px; background: var(--node-color); box-shadow: 0 0 0 5px var(--panel),0 0 0 6px #6c76534d; }
145
+ .node-label { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; line-height: 1.5; font-size: 12px; font-weight: 500; overflow-wrap: anywhere; background: var(--panel); padding: 1px 3px; }
146
+ .node-kind { display: block; margin-top: 3px; font-size: 10px; color: #a7b198; font-weight: 400; }
147
+ .map-node.selected:not([data-kind=project]) .node-dot { box-shadow: 0 0 0 5px var(--panel),0 0 0 6px var(--node-color); }
148
+ .map-node[data-kind=project] .node-dot { width: 39px; height: 39px; margin-bottom: 12px; background: #303b27; border: 1px solid #9ab584; }
149
+ .map-node[data-kind=project] .node-dot::after { content: 'm'; color: var(--accent); font: 700 31px/32px Georgia,serif; }
150
+ .map-node[data-kind=project] .node-label { font-size: 15px; font-weight: 600; }
151
+ .map-node[data-center=true]:not([data-kind=project]) .node-dot { width: 16px; height: 16px; margin-top: 2px; }
152
+ .map-bottom { padding: 13px 20px 17px; }
153
+ .map-legend { display: flex; flex-wrap: wrap; gap: 18px; color: var(--muted); font-size: 10px; }
154
+ .map-legend span { display: inline-flex; align-items: center; gap: 7px; }
155
+ .line-solid, .line-dashed { width: 19px; border-top: 1px solid #909e80; }
156
+ .line-dashed { border-top: 1px dashed #b6a5c7; }
157
+ .map-empty { position: absolute; left: 12px; right: 12px; bottom: 25px; text-align: center; font-size: 12px; color: var(--muted); }
158
+ .map-inspector { min-width: 0; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); padding: 22px; max-height: 533px; overflow: auto; scrollbar-color: #56624b var(--panel); }
159
+ .inspector-eyebrow { color: #b5c2a4; font-size: 9px; letter-spacing: 1.4px; margin: 0 0 9px; }
160
+ .map-inspector > h2 { font-size: 21px; font-weight: 500; margin: 0 0 16px; line-height: 1.4; overflow-wrap: anywhere; }
161
+ .project-brief { color: #c4ceba; font-size: 13px; line-height: 1.8; margin: 0 0 16px; }
162
+ .inspector-hint { color: var(--muted); font-size: 12px; line-height: 1.8; margin: 0 0 20px; }
163
+ .map-add { width: 100%; }
164
+ .inspector-index, .inspector-connections { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
165
+ .inspector-index h3, .inspector-connections h3 { font-size: 12px; font-weight: 500; color: #c2cdb7; margin: 0 0 12px; }
166
+ .index-entry { --node-color: #b9a8d2; display: flex; align-items: center; gap: 9px; background: transparent; color: #c2cdb7; border: 0; padding: 10px 3px; width: 100%; text-align: left; font-size: 12px; font-weight: 400; }
167
+ .index-entry > i { width: 5px; height: 5px; flex-shrink: 0; border-radius: 50%; background: var(--node-color); }
168
+ .index-entry > span:nth-child(2) { flex: 1; }
169
+ .index-entry:hover { background: #2a3325; }
170
+ .inspector-back { display: block; background: transparent; color: var(--accent); font-size: 11px; padding: 10px 0 17px; border: 0; }
171
+ .inspector-back:hover { background: transparent; text-decoration: underline; }
172
+ .map-inspector .card { padding: 0; margin: 0; }
173
+ .map-inspector .record-copy .record-text, .map-inspector .record-copy .answer { display: block; }
174
+ .map-inspector .card .read-more { display: none; }
175
+ .map-inspector .card-actions .remove { margin-left: 0; }
176
+ .connection-row { display: grid; text-align: left; gap: 6px; padding: 12px; background: #232a20; border: 1px solid #3a4532; color: #c8d5ba; width: 100%; margin-bottom: 9px; border-radius: 6px; }
177
+ .connection-type { font-size: 9px; letter-spacing: .7px; color: #c1d6a5; font-weight: 500; }
178
+ .connection-type.suggested { color: #c5b3d8; }
179
+ .connection-row strong { font-size: 12px; font-weight: 500; }
180
+ .connection-reason, .connection-note { font-size: 11px; color: #aebc9e; font-weight: 400; line-height: 1.8; overflow-wrap: anywhere; }
181
+ .map-footnote { color: var(--muted); font-size: 11px; margin: 16px 0 0; line-height: 1.8; }
182
+ dialog { border: 1px solid #545f48; border-radius: 12px; padding: 26px; width: min(560px, calc(100% - 32px)); max-height: 90vh; color: var(--ink); background: var(--panel); box-shadow: 0 22px 90px #0008; }
183
+ dialog::backdrop { background: #050805b0; backdrop-filter: blur(3px); }
184
+ dialog form { display: grid; gap: 15px; }
185
+ .dialog-heading, .dialog-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
186
+ .dialog-heading { padding-bottom: 8px; }
187
+ .dialog-heading h2 { font-size: 21px; }
188
+ .dialog-heading button { font-size: 22px; padding: 0 10px; border: 0; }
189
+ .dialog-actions { justify-content: flex-end; padding-top: 14px; border-top: 1px solid var(--line); }
190
+ label { display: grid; gap: 6px; font-size: 12px; font-weight: 500; }
191
+ textarea, select { width: 100%; border: 1px solid #4b5640; border-radius: 6px; padding: 9px 11px; font-size: 13px; color: var(--ink); background: #151a12; line-height: 1.7; }
192
+ textarea { resize: vertical; }
193
+ .hint, .optional { font-size: 11px; font-weight: 400; color: var(--muted); }
194
+ .hint { margin: 0; line-height: 1.7; }
195
+ .form-error { color: #f0cab4; font-size: 12px; margin: 0; }
196
+ .comparison { padding: 14px; background: var(--soft); border-radius: 7px; font-size: 12px; }
197
+ .comparison h3 { margin: 0 0 8px; font-size: 13px; }
198
+ .comparison pre { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
199
+ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
200
+ .skip-link { position: fixed; top: -100px; left: 12px; z-index: 10; padding: 12px; background: var(--accent); color: #20271a; }
201
+ .skip-link:focus { top: 12px; }
202
+ @media (max-width: 1050px) { .map-layout { grid-template-columns: minmax(0,1fr) 280px; gap: 14px; } .map-node { width: 108px; } }
203
+ @media (max-width: 950px) {
204
+ .workspace-bar { padding: 12px 24px; gap: 15px; }
205
+ .workspace-local { display: none; }
206
+ .workspace-local + #refresh { margin-left: auto; }
207
+ .workspace { padding: 28px 24px 22px; }
208
+ .workspace-heading { margin-bottom: 24px; }
209
+ .branch-name { max-width: 190px; }
210
+ nav { gap: 19px; }
211
+ .map-layout { grid-template-columns: 1fr; }
212
+ .map-node { width: 128px; }
213
+ .map-inspector { max-height: none; }
214
+ }
215
+ @media (max-width: 560px) {
216
+ .workspace-bar { padding: 12px 17px; gap: 12px; min-height: 62px; }
217
+ .workspace-brand { font-size: 20px; }
218
+ .branch-name { display: none; }
219
+ .project-name { font-size: 12px; }
220
+ .workspace { padding: 25px 17px 20px; }
221
+ .workspace-heading { align-items: flex-start; flex-wrap: wrap; gap: 16px; }
222
+ .workspace-heading .primary { padding: 8px 12px; }
223
+ .eyebrow { font-size: 9px; }
224
+ h1 { font-size: 30px; }
225
+ .subtitle { font-size: 12px; }
226
+ .workspace-tools { gap: 10px; }
227
+ .view-switch button { padding: 6px 10px; font-size: 11px; }
228
+ input[type=search] { padding-left: 32px; font-size: 11px; }
229
+ .search-icon { left: 12px; }
230
+ nav { gap: 18px; }
231
+ nav button { font-size: 11px; }
232
+ .summary-strip { gap: 12px 18px; }
233
+ .summary-item { font-size: 11px; }
234
+ .section-heading { margin: 24px 0 18px; }
235
+ #view-caption { display: none; }
236
+ .card { padding: 17px; }
237
+ .card-actions { gap: 14px; }
238
+ .card-actions .remove { margin-left: 0; }
239
+ .map-node { width: min(96px,24vw); }
240
+ .node-label { font-size: 11px; }
241
+ .node-kind { font-size: 9px; }
242
+ .map-node[data-kind=project] { width: 98px; }
243
+ .canvas-caption { font-size: 10px; padding: 13px 12px 0; }
244
+ .canvas-caption button { font-size: 10px; }
245
+ .map-bottom { padding: 13px 12px; }
246
+ .map-inspector { padding: 20px; }
247
+ }
248
+ @media (prefers-reduced-motion: reduce) { button { transition: none; } }
@@ -0,0 +1,93 @@
1
+ // A bounded local editor, never a shell or general filesystem endpoint.
2
+ import http from 'node:http';
3
+ import fs from 'node:fs/promises';
4
+ import crypto from 'node:crypto';
5
+ import { z } from 'zod';
6
+ import { workRoot, reviewWork, recordWork, retractWork, restoreWork } from './store.js';
7
+ import { workErrorMessage } from './errors.js';
8
+
9
+ const assets = new Map([
10
+ ['/', ['index.html', 'text/html; charset=utf-8']],
11
+ ['/app.js', ['app.js', 'text/javascript; charset=utf-8']],
12
+ ['/style.css', ['style.css', 'text/css; charset=utf-8']],
13
+ ]);
14
+ const actionSchema = z.object({
15
+ action: z.enum(['save', 'remove', 'restore']),
16
+ branch: z.string().max(1024).nullable(),
17
+ id: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/),
18
+ expected_revision: z.number().int().nonnegative(),
19
+ category: z.enum(['record', 'check']).default('record'),
20
+ fields: z.object({ kind: z.enum(['goal', 'answer', 'decision', 'next']), text: z.string().min(1).max(2000), answer: z.string().max(2000).optional(), why: z.string().max(2000).optional(), status: z.enum(['open', 'done']).default('open') }).strict().optional(),
21
+ }).strict();
22
+
23
+ const headers = {
24
+ 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff',
25
+ 'Referrer-Policy': 'no-referrer', 'X-Frame-Options': 'DENY',
26
+ 'Cross-Origin-Resource-Policy': 'same-origin', 'Cross-Origin-Opener-Policy': 'same-origin',
27
+ 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
28
+ 'Content-Security-Policy': "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'none'",
29
+ };
30
+ function reply(res, status, value, type = 'application/json; charset=utf-8') {
31
+ res.writeHead(status, { ...headers, 'Content-Type': type });
32
+ res.end(type.startsWith('application/json') ? JSON.stringify(value) : value);
33
+ }
34
+ async function body(req) {
35
+ const parts = []; let bytes = 0;
36
+ for await (const chunk of req) {
37
+ bytes += chunk.length;
38
+ if (bytes > 16384) throw new Error('Request is too large. Nothing was saved.');
39
+ parts.push(chunk);
40
+ }
41
+ return JSON.parse(Buffer.concat(parts).toString());
42
+ }
43
+
44
+ export async function startWorkView(project, { port = 0 } = {}) {
45
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Choose a port between 0 and 65535.');
46
+ const root = await workRoot(project);
47
+ await reviewWork(root); // Refuse damaged data before announcing a working view.
48
+ const token = crypto.randomBytes(32).toString('base64url');
49
+ const expectedAuth = Buffer.from('Bearer ' + token);
50
+ let origin;
51
+ const server = http.createServer(async (req, res) => {
52
+ try {
53
+ // Reject alternate Host names (including DNS rebinding) and cross-site
54
+ // browser requests. No CORS permission is granted, including preflight.
55
+ if (req.headers.host !== new URL(origin).host) return reply(res, 403, { error: 'This view only accepts its local address.' });
56
+ if (req.headers.origin && req.headers.origin !== origin || req.headers['sec-fetch-site'] && !['same-origin', 'none'].includes(req.headers['sec-fetch-site'])) return reply(res, 403, { error: 'Open the local Memoir view directly.' });
57
+ const url = new URL(req.url, origin);
58
+ const asset = assets.get(url.pathname);
59
+ if (req.method === 'GET' && asset && !url.search) return reply(res, 200, await fs.readFile(new URL('./ui/' + asset[0], import.meta.url)), asset[1]);
60
+ if (!url.pathname.startsWith('/api/')) return reply(res, 404, { error: 'Not found.' });
61
+ const auth = Buffer.from(req.headers.authorization || '');
62
+ if (auth.length !== expectedAuth.length || !crypto.timingSafeEqual(auth, expectedAuth)) return reply(res, 401, { error: 'Reopen the view with its local link.' });
63
+ if (req.method === 'GET' && url.pathname === '/api/state' && !url.search) return reply(res, 200, await reviewWork(root));
64
+ if (req.method !== 'POST' || url.pathname !== '/api/action' || url.search) return reply(res, 405, { error: 'This action is unavailable.' });
65
+ if (req.headers.origin !== origin || req.headers['content-type'] !== 'application/json') return reply(res, 403, { error: 'Save changes from the local view.' });
66
+ const input = actionSchema.parse(await body(req));
67
+ const guard = { expectedBranch: input.branch };
68
+ if (input.action === 'save') {
69
+ if (input.category !== 'record' || !input.fields) return reply(res, 400, { error: 'Only project records can be edited.' });
70
+ const { answer, why, ...fields } = input.fields;
71
+ await recordWork(root, { ...fields, ...(answer ? { answer } : {}), ...(why ? { why } : {}), id: input.id, expected_revision: input.expected_revision, scope: 'project', source: 'Saved in the local project view; previous versions remain in history.' }, guard);
72
+ } else if (input.action === 'remove') {
73
+ await retractWork(root, input, guard);
74
+ } else {
75
+ if (input.category !== 'record') return reply(res, 400, { error: 'Run a new authorized check to replace a removed receipt.' });
76
+ await restoreWork(root, input, guard);
77
+ }
78
+ return reply(res, 200, await reviewWork(root));
79
+ } catch (error) {
80
+ const message = workErrorMessage(error);
81
+ const conflict = /branch changed|Record changed|Record was removed|expected revision|before retracting/.test(message);
82
+ if (!res.headersSent && !res.destroyed) reply(res, 409, conflict
83
+ ? { error: 'Another session changed this item or branch. Review the latest version before saving. Your draft has been kept.', code: 'refresh_required' }
84
+ : { error: message });
85
+ }
86
+ });
87
+ server.requestTimeout = 10000; server.headersTimeout = 10000; server.keepAliveTimeout = 1000;
88
+ await new Promise((resolve, reject) => {
89
+ server.once('error', reject);
90
+ server.listen(port, '127.0.0.1', () => { origin = `http://127.0.0.1:${server.address().port}`; resolve(); });
91
+ });
92
+ return { server, origin, url: `${origin}/#token=${token}`, close: () => new Promise(resolve => { server.close(resolve); server.closeIdleConnections?.(); }) };
93
+ }