aidimag 1.0.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 (91) hide show
  1. package/LICENSE +102 -0
  2. package/README.md +113 -0
  3. package/dist/capture/bootstrap.d.ts +25 -0
  4. package/dist/capture/bootstrap.js +188 -0
  5. package/dist/capture/commit-miner.d.ts +59 -0
  6. package/dist/capture/commit-miner.js +381 -0
  7. package/dist/capture/harvest.d.ts +50 -0
  8. package/dist/capture/harvest.js +207 -0
  9. package/dist/capture/pr-miner.d.ts +57 -0
  10. package/dist/capture/pr-miner.js +185 -0
  11. package/dist/capture/session-briefing.d.ts +36 -0
  12. package/dist/capture/session-briefing.js +150 -0
  13. package/dist/capture/session-extraction.d.ts +23 -0
  14. package/dist/capture/session-extraction.js +54 -0
  15. package/dist/capture/triage.d.ts +30 -0
  16. package/dist/capture/triage.js +108 -0
  17. package/dist/cli/commands/capture.d.ts +5 -0
  18. package/dist/cli/commands/capture.js +357 -0
  19. package/dist/cli/commands/hosts.d.ts +5 -0
  20. package/dist/cli/commands/hosts.js +98 -0
  21. package/dist/cli/commands/knowledge.d.ts +5 -0
  22. package/dist/cli/commands/knowledge.js +121 -0
  23. package/dist/cli/commands/memory.d.ts +6 -0
  24. package/dist/cli/commands/memory.js +392 -0
  25. package/dist/cli/commands/sync.d.ts +5 -0
  26. package/dist/cli/commands/sync.js +307 -0
  27. package/dist/cli/commands/tickets.d.ts +6 -0
  28. package/dist/cli/commands/tickets.js +328 -0
  29. package/dist/cli/commands/verify.d.ts +5 -0
  30. package/dist/cli/commands/verify.js +136 -0
  31. package/dist/cli/index.d.ts +19 -0
  32. package/dist/cli/index.js +46 -0
  33. package/dist/cli/shared.d.ts +37 -0
  34. package/dist/cli/shared.js +175 -0
  35. package/dist/config.d.ts +55 -0
  36. package/dist/config.js +51 -0
  37. package/dist/context/generate.d.ts +24 -0
  38. package/dist/context/generate.js +115 -0
  39. package/dist/critique/critique.d.ts +40 -0
  40. package/dist/critique/critique.js +110 -0
  41. package/dist/db/schema.d.ts +28 -0
  42. package/dist/db/schema.js +269 -0
  43. package/dist/db/store.d.ts +182 -0
  44. package/dist/db/store.js +906 -0
  45. package/dist/debug.d.ts +14 -0
  46. package/dist/debug.js +25 -0
  47. package/dist/embeddings/provider.d.ts +16 -0
  48. package/dist/embeddings/provider.js +100 -0
  49. package/dist/embeddings/search.d.ts +22 -0
  50. package/dist/embeddings/search.js +95 -0
  51. package/dist/index.d.ts +11 -0
  52. package/dist/index.js +12 -0
  53. package/dist/knowledge/chunk.d.ts +9 -0
  54. package/dist/knowledge/chunk.js +78 -0
  55. package/dist/knowledge/extract.d.ts +34 -0
  56. package/dist/knowledge/extract.js +113 -0
  57. package/dist/knowledge/ingest.d.ts +79 -0
  58. package/dist/knowledge/ingest.js +305 -0
  59. package/dist/knowledge/llm.d.ts +21 -0
  60. package/dist/knowledge/llm.js +110 -0
  61. package/dist/mcp/server.d.ts +11 -0
  62. package/dist/mcp/server.js +532 -0
  63. package/dist/security/evidence.d.ts +11 -0
  64. package/dist/security/evidence.js +15 -0
  65. package/dist/security/seal.d.ts +3 -0
  66. package/dist/security/seal.js +28 -0
  67. package/dist/security/sync-push.d.ts +2 -0
  68. package/dist/security/sync-push.js +37 -0
  69. package/dist/security/url.d.ts +6 -0
  70. package/dist/security/url.js +67 -0
  71. package/dist/sync/client.d.ts +84 -0
  72. package/dist/sync/client.js +391 -0
  73. package/dist/sync/server.d.ts +75 -0
  74. package/dist/sync/server.js +659 -0
  75. package/dist/tickets/provider.d.ts +80 -0
  76. package/dist/tickets/provider.js +375 -0
  77. package/dist/types.d.ts +133 -0
  78. package/dist/types.js +5 -0
  79. package/dist/ui/page.d.ts +5 -0
  80. package/dist/ui/page.js +841 -0
  81. package/dist/ui/server.d.ts +10 -0
  82. package/dist/ui/server.js +437 -0
  83. package/dist/verify/check.d.ts +38 -0
  84. package/dist/verify/check.js +121 -0
  85. package/dist/verify/engine.d.ts +39 -0
  86. package/dist/verify/engine.js +178 -0
  87. package/dist/verify/hooks.d.ts +24 -0
  88. package/dist/verify/hooks.js +125 -0
  89. package/dist/verify/runners.d.ts +26 -0
  90. package/dist/verify/runners.js +193 -0
  91. package/package.json +78 -0
@@ -0,0 +1,841 @@
1
+ /**
2
+ * Dashboard HTML — embedded as a template string so `tsc` is the whole build
3
+ * (no asset pipeline). D3 v7 from CDN renders the memory graph.
4
+ */
5
+ export const PAGE_HTML = /* html */ `<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="utf-8">
9
+ <title>aiDimag — repo brain</title>
10
+ <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
11
+ <meta name="color-scheme" content="light dark">
12
+ <style>
13
+ :root {
14
+ --background: 210 40% 98%;
15
+ --foreground: 222 47% 11%;
16
+ --card: 0 0% 100%;
17
+ --muted: 214 32% 94%;
18
+ --muted-foreground: 215 16% 47%;
19
+ --primary: 217 91% 53%;
20
+ --primary-foreground: 0 0% 100%;
21
+ --secondary: 214 32% 94%;
22
+ --border: 214 32% 91%;
23
+ --ring: 199 89% 48%;
24
+ --radius: 0.75rem;
25
+ --surface-glow: 0 0 0 1px rgba(37, 99, 235, 0.08), 0 8px 32px rgba(37, 99, 235, 0.08);
26
+ --verified: #22c55e;
27
+ --unverified: #64748b;
28
+ --stale: #eab308;
29
+ --refuted: #ef4444;
30
+ --path: #2563eb;
31
+ }
32
+ .dark {
33
+ --background: 222 47% 6%;
34
+ --foreground: 210 40% 98%;
35
+ --card: 222 47% 8%;
36
+ --muted: 217 33% 14%;
37
+ --muted-foreground: 215 20% 65%;
38
+ --primary: 213 94% 68%;
39
+ --primary-foreground: 222 47% 6%;
40
+ --secondary: 217 33% 14%;
41
+ --border: 217 33% 16%;
42
+ --surface-glow: 0 0 0 1px rgba(96, 165, 250, 0.12), 0 8px 32px rgba(0, 0, 0, 0.35);
43
+ --path: #60a5fa;
44
+ --unverified: #94a3b8;
45
+ }
46
+ * { box-sizing: border-box; margin: 0; }
47
+ html { color-scheme: light dark; }
48
+ body {
49
+ background-color: hsl(var(--background));
50
+ background-image:
51
+ radial-gradient(at 0% 0%, rgba(37, 99, 235, 0.12) 0, transparent 50%),
52
+ radial-gradient(at 100% 0%, rgba(14, 165, 233, 0.1) 0, transparent 50%),
53
+ radial-gradient(at 50% 100%, rgba(6, 182, 212, 0.08) 0, transparent 50%);
54
+ color: hsl(var(--foreground));
55
+ font: 14px/1.5 "Inter", ui-sans-serif, system-ui, sans-serif;
56
+ font-feature-settings: "cv02", "cv03", "cv04", "cv11";
57
+ height: 100vh;
58
+ display: flex;
59
+ flex-direction: column;
60
+ -webkit-font-smoothing: antialiased;
61
+ }
62
+ html.dark body {
63
+ background-image:
64
+ radial-gradient(at 0% 0%, rgba(37, 99, 235, 0.18) 0, transparent 50%),
65
+ radial-gradient(at 100% 0%, rgba(14, 165, 233, 0.12) 0, transparent 50%),
66
+ radial-gradient(at 50% 100%, rgba(6, 182, 212, 0.1) 0, transparent 50%);
67
+ }
68
+ header {
69
+ display: flex; align-items: center; gap: 8px 12px; flex-wrap: wrap;
70
+ padding: 10px 16px;
71
+ border-bottom: 1px solid hsl(var(--border) / 0.6);
72
+ background: hsl(var(--card) / 0.82);
73
+ backdrop-filter: blur(16px);
74
+ }
75
+ .brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
76
+ .logo { width: 32px; height: 32px; flex-shrink: 0; border-radius: 10px; }
77
+ .brand-text { min-width: 0; }
78
+ header h1 { font-size: 15px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; }
79
+ header .subtitle {
80
+ display: block; font-size: 11px; font-weight: 500; color: hsl(var(--muted-foreground));
81
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 280px;
82
+ }
83
+ .pill {
84
+ padding: 3px 10px; border-radius: 999px; font-size: 12px;
85
+ border: 1px solid hsl(var(--border)); white-space: nowrap;
86
+ background: hsl(var(--muted) / 0.6); color: hsl(var(--muted-foreground));
87
+ }
88
+ .pill b { font-weight: 600; color: hsl(var(--foreground)); }
89
+ .spacer { flex: 1; }
90
+ .toolbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
91
+ button {
92
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap;
93
+ background: hsl(var(--secondary)); color: hsl(var(--foreground));
94
+ border: 1px solid hsl(var(--border)); border-radius: calc(var(--radius) - 2px);
95
+ padding: 6px 12px; font-size: 12px; font-weight: 500; cursor: pointer;
96
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
97
+ }
98
+ button svg { width: 14px; height: 14px; flex: 0 0 auto; }
99
+ button:hover:not(.primary) { background: hsl(var(--muted)); border-color: hsl(var(--primary) / 0.35); }
100
+ button:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
101
+ button.primary {
102
+ background: hsl(var(--primary)); border-color: transparent;
103
+ color: hsl(var(--primary-foreground));
104
+ }
105
+ button.primary:hover { opacity: 0.9; background: hsl(var(--primary)); border-color: transparent; }
106
+ button.icon { padding: 8px; width: 36px; height: 36px; }
107
+ button.danger:hover { background: color-mix(in srgb, var(--refuted) 12%, transparent); border-color: var(--refuted); }
108
+ main { flex: 1; display: flex; min-height: 0; }
109
+ #graph { flex: 1; min-width: 0; background: hsl(var(--background) / 0.35); }
110
+ aside {
111
+ width: 460px; border-left: 1px solid hsl(var(--border) / 0.6);
112
+ overflow-y: auto; padding: 16px;
113
+ background: hsl(var(--card) / 0.55); backdrop-filter: blur(12px);
114
+ }
115
+ h2 { font-size: 14px; font-weight: 600; color: hsl(var(--foreground)); margin: 16px 0 10px; letter-spacing: -0.01em; }
116
+ .card {
117
+ background: hsl(var(--card) / 0.9);
118
+ border: 1px solid hsl(var(--border) / 0.6);
119
+ border-radius: var(--radius); padding: 12px 14px; margin-bottom: 10px;
120
+ box-shadow: var(--surface-glow);
121
+ transition: border-color 0.15s, transform 0.15s;
122
+ }
123
+ .card:hover { border-color: hsl(var(--primary) / 0.3); }
124
+ .card .claim { font-size: 13px; margin-bottom: 6px; line-height: 1.5; }
125
+ .card .meta { font-size: 11px; color: hsl(var(--muted-foreground)); display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
126
+ .badge { padding: 2px 8px; border-radius: 999px; font-size: 10px; font-weight: 600; border: 1px solid transparent; }
127
+ .badge.VERIFIED { background: color-mix(in srgb, var(--verified) 15%, transparent); color: var(--verified); }
128
+ .badge.UNVERIFIED { background: color-mix(in srgb, var(--unverified) 15%, transparent); color: var(--unverified); }
129
+ .badge.STALE { background: color-mix(in srgb, var(--stale) 15%, transparent); color: var(--stale); }
130
+ .badge.REFUTED { background: color-mix(in srgb, var(--refuted) 15%, transparent); color: var(--refuted); }
131
+ .kind { color: hsl(var(--primary)); font-weight: 500; }
132
+ .actions { margin-top: 10px; display: flex; gap: 6px; flex-wrap: wrap; }
133
+ .evidence { font-size: 11px; color: hsl(var(--muted-foreground)); font-family: ui-monospace, monospace; margin-top: 4px; word-break: break-all; }
134
+ .legend {
135
+ display: flex; gap: 14px; flex-wrap: wrap; padding: 8px 16px; font-size: 11px;
136
+ color: hsl(var(--muted-foreground)); border-top: 1px solid hsl(var(--border) / 0.6);
137
+ background: hsl(var(--card) / 0.65); backdrop-filter: blur(12px);
138
+ }
139
+ .dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 5px; vertical-align: -1px; }
140
+ #toast {
141
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
142
+ background: hsl(var(--card)); color: hsl(var(--foreground));
143
+ border: 1px solid hsl(var(--border) / 0.6);
144
+ padding: 10px 18px;
145
+ border-radius: var(--radius); display: none; font-size: 13px; font-weight: 500; z-index: 50;
146
+ box-shadow: var(--surface-glow);
147
+ }
148
+ .empty { color: hsl(var(--muted-foreground)); font-size: 12px; padding: 8px 0; }
149
+ svg text { fill: hsl(var(--muted-foreground)); font-size: 10px; pointer-events: none; }
150
+ dialog {
151
+ background: hsl(var(--card)); color: hsl(var(--foreground));
152
+ border: 1px solid hsl(var(--border) / 0.6);
153
+ border-radius: calc(var(--radius) + 2px); padding: 20px; width: 480px; max-width: 92vw;
154
+ box-shadow: var(--surface-glow);
155
+ }
156
+ dialog::backdrop { background: rgba(0,0,0,.55); backdrop-filter: blur(4px); }
157
+ dialog h3 { font-size: 15px; font-weight: 700; margin-bottom: 12px; letter-spacing: -0.02em; }
158
+ dialog label {
159
+ display: block; font-size: 12px; color: hsl(var(--muted-foreground));
160
+ margin: 12px 0 4px; font-weight: 500;
161
+ }
162
+ dialog input, dialog select, dialog textarea {
163
+ width: 100%; background: hsl(var(--background)); color: hsl(var(--foreground));
164
+ border: 1px solid hsl(var(--border)); border-radius: calc(var(--radius) - 2px);
165
+ padding: 8px 10px; font-size: 13px; font-family: inherit;
166
+ }
167
+ dialog input:focus, dialog select:focus, dialog textarea:focus {
168
+ outline: 2px solid hsl(var(--ring)); outline-offset: 1px; border-color: transparent;
169
+ }
170
+ dialog textarea { min-height: 64px; resize: vertical; }
171
+ .dialog-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
172
+ .ev-row { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
173
+ .ev-row select { width: 160px; }
174
+ .searchbar { display: flex; gap: 6px; margin-bottom: 10px; }
175
+ .searchbar input {
176
+ flex: 1; background: hsl(var(--background)); color: hsl(var(--foreground));
177
+ border: 1px solid hsl(var(--border)); border-radius: calc(var(--radius) - 2px);
178
+ padding: 8px 10px; font-size: 13px;
179
+ }
180
+ .searchbar input:focus { outline: 2px solid hsl(var(--ring)); outline-offset: 1px; border-color: transparent; }
181
+ .searchbar select {
182
+ background: hsl(var(--background)); color: hsl(var(--foreground));
183
+ border: 1px solid hsl(var(--border)); border-radius: calc(var(--radius) - 2px);
184
+ font-size: 12px; padding: 6px 8px;
185
+ }
186
+ .keyrow {
187
+ font-size: 11px; font-family: ui-monospace, monospace;
188
+ display: flex; justify-content: space-between; align-items: center;
189
+ padding: 4px 0; border-bottom: 1px solid hsl(var(--border));
190
+ }
191
+ .hint { font-size: 11px; color: hsl(var(--muted-foreground)); margin-top: 6px; line-height: 1.5; }
192
+ .theme-icon-sun { display: none; }
193
+ html:not(.dark) .theme-icon-sun { display: block; }
194
+ html:not(.dark) .theme-icon-moon { display: none; }
195
+ html.dark .theme-icon-sun { display: none; }
196
+ html.dark .theme-icon-moon { display: block; }
197
+ </style>
198
+ <script>
199
+ (function () {
200
+ var k = "aidimag-ui-theme";
201
+ var saved = localStorage.getItem(k);
202
+ // Default dark; only use light when explicitly chosen.
203
+ if (saved !== "light") document.documentElement.classList.add("dark");
204
+ })();
205
+ </script>
206
+ <link rel="preconnect" href="https://fonts.googleapis.com">
207
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
208
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
209
+ </head>
210
+ <body>
211
+ <header>
212
+ <div class="brand">
213
+ <svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" aria-hidden="true">
214
+ <defs>
215
+ <linearGradient id="dimGrad" x1="8" y1="8" x2="56" y2="56" gradientUnits="userSpaceOnUse">
216
+ <stop offset="0%" stop-color="#2563eb"/><stop offset="55%" stop-color="#0ea5e9"/><stop offset="100%" stop-color="#06b6d4"/>
217
+ </linearGradient>
218
+ <linearGradient id="dimGradSoft" x1="8" y1="8" x2="56" y2="56" gradientUnits="userSpaceOnUse">
219
+ <stop offset="0%" stop-color="#2563eb" stop-opacity="0.18"/><stop offset="100%" stop-color="#06b6d4" stop-opacity="0.18"/>
220
+ </linearGradient>
221
+ </defs>
222
+ <rect x="2" y="2" width="60" height="60" rx="16" fill="url(#dimGradSoft)"/>
223
+ <g transform="translate(8 8) scale(2)" stroke="url(#dimGrad)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
224
+ <path d="M12 18V5"/><path d="M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4"/>
225
+ <path d="M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5"/>
226
+ <path d="M17.997 5.125a4 4 0 0 1 2.526 5.77"/><path d="M18 18a4 4 0 0 0 2-7.464"/>
227
+ <path d="M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517"/>
228
+ <path d="M6 18a4 4 0 0 1-2-7.464"/><path d="M6.003 5.125a4 4 0 0 0-2.526 5.77"/>
229
+ </g>
230
+ <circle cx="49" cy="49" r="11" fill="#10b981"/>
231
+ <path d="M44 49.2l3.4 3.4L54.5 45" stroke="#ffffff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
232
+ </svg>
233
+ <div class="brand-text">
234
+ <h1>aiDimag</h1>
235
+ <span class="subtitle" id="repo">Repo brain</span>
236
+ </div>
237
+ </div>
238
+ <span class="pill" id="counts"></span>
239
+ <div class="spacer"></div>
240
+ <div class="toolbar">
241
+ <button class="primary" onclick="document.getElementById('dlg-new').showModal()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>New memory</button>
242
+ <button onclick="runMine()" title="Mine new commits since the last run (Shift+click: rescan all history)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5.5 18 9"/><path d="M2 22l8-8"/><path d="M20.5 7.5 22 6a2.83 2.83 0 0 0-4-4l-1.5 1.5"/><path d="m9 11 4 4"/><path d="M16 2 8.5 9.5"/></svg>Mine commits</button>
243
+ <button class="primary" onclick="runVerify(false)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/></svg>Verify</button>
244
+ <button onclick="runVerify(true)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="m8 11 2 2 4-4"/></svg>Verify --deep</button>
245
+ <button onclick="runSync()" id="btn-sync"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg>Sync</button>
246
+ <button onclick="runReindex()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/></svg>Reindex</button>
247
+ <button onclick="openCloud()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/></svg>Cloud</button>
248
+ <button onclick="openTickets()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M13 5v2"/><path d="M13 17v2"/><path d="M13 11v2"/></svg>Tickets</button>
249
+ <button onclick="load()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>Refresh</button>
250
+ <button class="icon" type="button" onclick="toggleTheme()" id="btn-theme" aria-label="Toggle light/dark theme">
251
+ <svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
252
+ <svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
253
+ </button>
254
+ </div>
255
+ </header>
256
+ <main>
257
+ <div id="graph"></div>
258
+ <aside>
259
+ <h2 id="proposals-h">Pending proposals</h2>
260
+ <div id="proposals"></div>
261
+ <h2>Memories</h2>
262
+ <div class="searchbar">
263
+ <input id="q" placeholder="Search memories… (semantic when embeddings configured)" oninput="debouncedSearch()">
264
+ <select id="q-kind" onchange="doSearch()">
265
+ <option value="">all kinds</option>
266
+ <option>DECISION</option><option>CONVENTION</option><option>GOTCHA</option>
267
+ <option>FAILED_APPROACH</option><option>ARCHITECTURE</option><option>INVARIANT</option>
268
+ <option>TODO_CONTEXT</option><option>GUARDRAIL</option><option>SKILL</option>
269
+ </select>
270
+ </div>
271
+ <div id="memories"></div>
272
+ </aside>
273
+ </main>
274
+
275
+ <!-- New memory dialog (dim remember) -->
276
+ <dialog id="dlg-new">
277
+ <h3>+ New memory</h3>
278
+ <label>Claim (write it falsifiable — something a check could verify)</label>
279
+ <textarea id="nm-claim" placeholder="All DB access goes through src/db/store.ts; nothing else imports better-sqlite3"></textarea>
280
+ <label>Kind</label>
281
+ <select id="nm-kind" onchange="toggleGuardrailLevel()">
282
+ <option>DECISION</option><option>CONVENTION</option><option>GOTCHA</option>
283
+ <option>FAILED_APPROACH</option><option>ARCHITECTURE</option><option>INVARIANT</option>
284
+ <option>TODO_CONTEXT</option><option>GUARDRAIL</option><option>SKILL</option>
285
+ </select>
286
+ <div id="guardrail-section" style="display:none;">
287
+ <label>Guardrail Level</label>
288
+ <select id="nm-guardrail-level">
289
+ <option value="ask-first">🤚 Ask First - Confirm before doing it</option>
290
+ <option value="always">✅ Always - Block completely, refuse to proceed</option>
291
+ <option value="never">🚫 Never - Just a suggestion</option>
292
+ </select>
293
+ </div>
294
+ <label>Scope paths (comma-separated, empty = repo-wide)</label>
295
+ <input id="nm-paths" placeholder="src/db, src/api/auth.ts">
296
+ <label>Symbols (comma-separated, optional)</label>
297
+ <input id="nm-symbols" placeholder="UserService, authenticate()">
298
+ <label>
299
+ <input type="checkbox" id="nm-pinned">
300
+ 📌 Pin this memory (exempt from time decay)
301
+ </label>
302
+ <label>Evidence (optional but recommended)</label>
303
+ <div id="nm-evidence"></div>
304
+ <button style="margin-top:6px" onclick="addEvidenceRow()">+ add evidence</button>
305
+ <div class="hint">STATIC_CHECK: shell command, exit 0 = claim holds · COMMIT_REF: sha · EXEC_TRACE: cmd :: regex · TEST_RESULT: test cmd</div>
306
+ <div class="dialog-actions">
307
+ <button onclick="document.getElementById('dlg-new').close()">Cancel</button>
308
+ <button class="primary" onclick="saveMemory()">Save memory</button>
309
+ </div>
310
+ </dialog>
311
+
312
+ <!-- Cloud settings dialog (dim cloud link / dim keys) -->
313
+ <dialog id="dlg-cloud">
314
+ <h3>☁ Team sync</h3>
315
+ <div id="cloud-status" class="hint"></div>
316
+ <label>Server URL</label>
317
+ <input id="cl-server" placeholder="http://localhost:3000">
318
+ <label>Brain (team memory name)</label>
319
+ <input id="cl-brain" placeholder="myrepo">
320
+ <label>Access token (stored on this machine only, never in the repo)</label>
321
+ <input id="cl-token" type="password" placeholder="aidimag_sk_…">
322
+ <div class="dialog-actions">
323
+ <button onclick="cloudUnlink()">Unlink</button>
324
+ <button class="primary" onclick="cloudLink()">Link</button>
325
+ </div>
326
+ <h3 style="margin-top:18px">🔑 API keys (admin)</h3>
327
+ <label>Admin token (used for this request only — not stored)</label>
328
+ <input id="k-admin" type="password" placeholder="server admin token">
329
+ <div class="ev-row">
330
+ <input id="k-brain" placeholder="brain">
331
+ <input id="k-label" placeholder="label (alice-laptop)">
332
+ <button class="primary" onclick="keyCreate()">Create</button>
333
+ <button onclick="keyList()">List</button>
334
+ </div>
335
+ <div id="keys-out"></div>
336
+ <div class="dialog-actions">
337
+ <button onclick="document.getElementById('dlg-cloud').close()">Close</button>
338
+ </div>
339
+ </dialog>
340
+
341
+ <!-- Tickets dialog (dim ticket connect / share) -->
342
+ <dialog id="dlg-tickets">
343
+ <h3>🎫 Tickets</h3>
344
+ <div id="tk-status" class="hint"></div>
345
+ <label>Provider</label>
346
+ <select id="tk-provider" onchange="ticketsProviderHint()">
347
+ <option value="jira">Jira</option>
348
+ <option value="github">GitHub Issues</option>
349
+ <option value="linear">Linear</option>
350
+ <option value="http">HTTP middleware (your own)</option>
351
+ <option value="remote">Remote (team sync server — zero local credentials)</option>
352
+ </select>
353
+ <div id="tk-url-row">
354
+ <label id="tk-url-label">Base URL</label>
355
+ <input id="tk-url" placeholder="https://acme.atlassian.net">
356
+ </div>
357
+ <div id="tk-token-row">
358
+ <label id="tk-token-label">Credential (stored on this machine only, never in the repo)</label>
359
+ <input id="tk-token" type="password" placeholder="email:apiToken">
360
+ </div>
361
+ <label>Ticket-id pattern (extracted from branch names &amp; commit messages)</label>
362
+ <input id="tk-pattern" placeholder="[A-Z][A-Z0-9]+-\\\\d+">
363
+ <label>Validate with a real ticket id (optional)</label>
364
+ <input id="tk-test" placeholder="XXX-2100">
365
+ <div class="dialog-actions">
366
+ <button onclick="ticketsDisconnect()">Disconnect</button>
367
+ <button class="primary" onclick="ticketsConnect()">Connect</button>
368
+ </div>
369
+ <h3 style="margin-top:18px">👥 Team credentials (admin)</h3>
370
+ <div class="hint">Stores the provider + token on the linked sync server — teammates connect with provider “remote” and never hold a ticket credential.</div>
371
+ <label>Admin token (used for this request only — not stored)</label>
372
+ <input id="tk-admin" type="password" placeholder="server admin token">
373
+ <div class="ev-row">
374
+ <button class="primary" onclick="ticketsShare()">Share current config</button>
375
+ <button class="danger" onclick="ticketsShare(true)">Remove from server</button>
376
+ </div>
377
+ <div class="dialog-actions">
378
+ <button onclick="document.getElementById('dlg-tickets').close()">Close</button>
379
+ </div>
380
+ </dialog>
381
+
382
+ <div class="legend">
383
+ <span><span class="dot" style="background:var(--verified)"></span>VERIFIED</span>
384
+ <span><span class="dot" style="background:var(--unverified)"></span>UNVERIFIED</span>
385
+ <span><span class="dot" style="background:var(--stale)"></span>STALE</span>
386
+ <span><span class="dot" style="background:var(--refuted)"></span>REFUTED</span>
387
+ <span><span class="dot" style="background:var(--path); border-radius:2px"></span>scope path</span>
388
+ <span style="margin-left:auto">node size = confidence · drag to rearrange · scroll to zoom</span>
389
+ </div>
390
+ <div id="toast"></div>
391
+
392
+ <script>
393
+ const COLORS = { VERIFIED: "#22c55e", UNVERIFIED: "#94a3b8", STALE: "#eab308", REFUTED: "#ef4444" };
394
+ let state = null;
395
+ let csrfToken = null;
396
+
397
+ function cssVar(name, fallback) {
398
+ const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
399
+ return v || fallback;
400
+ }
401
+
402
+ function graphPalette() {
403
+ return {
404
+ VERIFIED: cssVar("--verified", COLORS.VERIFIED),
405
+ UNVERIFIED: cssVar("--unverified", COLORS.UNVERIFIED),
406
+ STALE: cssVar("--stale", COLORS.STALE),
407
+ REFUTED: cssVar("--refuted", COLORS.REFUTED),
408
+ path: cssVar("--path", "#60a5fa"),
409
+ link: "hsl(" + cssVar("--border", "217 33% 16%") + ")",
410
+ primary: "hsl(" + cssVar("--primary", "217 91% 53%") + ")",
411
+ };
412
+ }
413
+
414
+ function toggleTheme() {
415
+ const root = document.documentElement;
416
+ const dark = !root.classList.contains("dark");
417
+ root.classList.toggle("dark", dark);
418
+ localStorage.setItem("aidimag-ui-theme", dark ? "dark" : "light");
419
+ if (state) renderGraph();
420
+ }
421
+
422
+ function toast(msg) {
423
+ const t = document.getElementById("toast");
424
+ t.textContent = msg; t.style.display = "block";
425
+ setTimeout(() => t.style.display = "none", 2500);
426
+ }
427
+
428
+ async function api(path, opts) {
429
+ opts = opts || {};
430
+ const method = (opts.method || "GET").toUpperCase();
431
+ if (method !== "GET" && method !== "HEAD") {
432
+ if (!csrfToken) throw new Error("missing CSRF token — reload the page");
433
+ opts.headers = { ...(opts.headers || {}), "X-Aidimag-Csrf-Token": csrfToken };
434
+ }
435
+ const r = await fetch(path, opts);
436
+ const body = await r.json();
437
+ if (!r.ok) throw new Error(body.error || r.status);
438
+ return body;
439
+ }
440
+
441
+ async function load() {
442
+ state = await api("/api/state");
443
+ csrfToken = state.csrfToken;
444
+ document.getElementById("repo").textContent = state.repoRoot;
445
+ const s = state.summary.byStatus;
446
+ document.getElementById("counts").innerHTML =
447
+ \`<b>\${state.summary.total}</b> memories · ✓\${s.VERIFIED} ?\${s.UNVERIFIED} ~\${s.STALE} ✗\${s.REFUTED}\`;
448
+ renderProposals(); renderMemories(); renderGraph();
449
+ }
450
+
451
+ function esc(s) { return s.replace(/[&<>"']/g, c => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[c])); }
452
+
453
+ function renderProposals() {
454
+ const el = document.getElementById("proposals");
455
+ document.getElementById("proposals-h").textContent = \`Pending proposals (\${state.proposals.length})\`;
456
+ if (!state.proposals.length) { el.innerHTML = '<div class="empty">Queue is empty.</div>'; return; }
457
+ el.innerHTML = state.proposals.map(p => \`
458
+ <div class="card">
459
+ <div class="claim">\${esc(p.claim)}</div>
460
+ <div class="meta"><span class="kind">\${p.kind}</span><span>via \${esc(p.source)}</span>\${p.ticketRef ? \`<span>🎫 \${esc(p.ticketRef)}</span>\` : ""}</div>
461
+ <div class="actions">
462
+ <button class="primary" onclick="act('/api/proposals/\${p.id}/approve','approved')">Approve</button>
463
+ <button class="danger" onclick="act('/api/proposals/\${p.id}/reject','rejected')">Reject</button>
464
+ </div>
465
+ </div>\`).join("");
466
+ }
467
+
468
+ function renderMemories(list) {
469
+ const el = document.getElementById("memories");
470
+ const items = list ?? state.memories;
471
+ if (!items.length) { el.innerHTML = '<div class="empty">No matching memories.</div>'; return; }
472
+ el.innerHTML = items.map(m => \`
473
+ <div class="card" id="mem-\${m.id}">
474
+ <div class="claim">\${esc(m.claim)}</div>
475
+ <div class="meta">
476
+ <span class="badge \${m.status}">\${m.status}</span>
477
+ \${m.pinned ? '<span class="badge" title="Pinned: never decays with age (evidence failure can still mark it stale)">📌 PINNED</span>' : ""}
478
+ <span class="kind">\${m.kind}</span>
479
+ <span>conf \${m.confidence.toFixed(2)}</span>
480
+ \${m.scope.paths.length ? "<span>📁 " + esc(m.scope.paths.join(", ")) + "</span>" : "<span>repo-wide</span>"}
481
+ </div>
482
+ \${m.grounding.map(e => \`<div class="evidence">\${e.type}(\${e.result}) \${esc(e.payload)}</div>\`).join("")}
483
+ <div class="actions">
484
+ \${m.pinned
485
+ ? \`<button onclick="act('/api/memories/\${m.id}/unpin','unpinned')">Unpin</button>\`
486
+ : \`<button onclick="act('/api/memories/\${m.id}/pin','pinned 📌')">Pin</button>\`}
487
+ \${m.status !== "REFUTED" ? \`<button class="danger" onclick="act('/api/memories/\${m.id}/refute','refuted')">Refute</button>\` : ""}
488
+ <button class="danger" onclick="if(confirm('Delete permanently?'))act('/api/memories/\${m.id}/forget','forgotten')">Forget</button>
489
+ </div>
490
+ </div>\`).join("");
491
+ }
492
+
493
+ async function act(path, verb) {
494
+ try { await api(path, { method: "POST" }); toast("Memory " + verb); load(); }
495
+ catch (e) { toast("Error: " + e.message); }
496
+ }
497
+
498
+ async function runVerify(deep) {
499
+ toast(deep ? "Running deep verification…" : "Verifying…");
500
+ try {
501
+ const r = await api("/api/verify" + (deep ? "?deep=1" : ""), { method: "POST" });
502
+ toast(\`Checked \${r.checked}: \${r.verified} verified, \${r.stale} stale, \${r.decayed} decayed\`);
503
+ load();
504
+ } catch (e) { toast("Error: " + e.message); }
505
+ }
506
+
507
+ // ---------------------------------------------------------------- new memory
508
+
509
+ function addEvidenceRow() {
510
+ const row = document.createElement("div");
511
+ row.className = "ev-row";
512
+ row.innerHTML = \`
513
+ <select>
514
+ <option>STATIC_CHECK</option><option>COMMIT_REF</option>
515
+ <option>TEST_RESULT</option><option>EXEC_TRACE</option><option>HUMAN_ATTESTED</option>
516
+ </select>
517
+ <input placeholder="payload">
518
+ <button onclick="this.parentElement.remove()">✕</button>\`;
519
+ document.getElementById("nm-evidence").appendChild(row);
520
+ }
521
+
522
+ function toggleGuardrailLevel() {
523
+ const kind = document.getElementById("nm-kind").value;
524
+ const section = document.getElementById("guardrail-section");
525
+ section.style.display = kind === "GUARDRAIL" ? "block" : "none";
526
+ }
527
+
528
+ async function saveMemory() {
529
+ const claim = document.getElementById("nm-claim").value.trim();
530
+ if (claim.length < 10) { toast("Claim is too short"); return; }
531
+ const kind = document.getElementById("nm-kind").value;
532
+ const evidence = [...document.querySelectorAll("#nm-evidence .ev-row")]
533
+ .map(r => ({ type: r.querySelector("select").value, payload: r.querySelector("input").value.trim() }))
534
+ .filter(e => e.payload);
535
+ const paths = document.getElementById("nm-paths").value.split(",").map(s => s.trim()).filter(Boolean);
536
+ const symbols = document.getElementById("nm-symbols").value.split(",").map(s => s.trim()).filter(Boolean);
537
+ const pinned = document.getElementById("nm-pinned").checked;
538
+ const guardrailLevel = kind === "GUARDRAIL" ? document.getElementById("nm-guardrail-level").value : undefined;
539
+
540
+ try {
541
+ await api("/api/memories", {
542
+ method: "POST",
543
+ body: JSON.stringify({ kind, claim, paths, symbols, evidence, pinned, guardrailLevel }),
544
+ });
545
+ document.getElementById("dlg-new").close();
546
+ document.getElementById("nm-claim").value = "";
547
+ document.getElementById("nm-paths").value = "";
548
+ document.getElementById("nm-symbols").value = "";
549
+ document.getElementById("nm-pinned").checked = false;
550
+ document.getElementById("nm-evidence").innerHTML = "";
551
+ toast("Memory saved");
552
+ load();
553
+ } catch (e) { toast("Error: " + e.message); }
554
+ }
555
+
556
+ // ---------------------------------------------------------------- search
557
+
558
+ let searchTimer = null;
559
+ function debouncedSearch() {
560
+ clearTimeout(searchTimer);
561
+ searchTimer = setTimeout(doSearch, 300);
562
+ }
563
+
564
+ async function doSearch() {
565
+ const q = document.getElementById("q").value.trim();
566
+ const kind = document.getElementById("q-kind").value;
567
+ if (!q && !kind) { renderMemories(); return; }
568
+ try {
569
+ const r = await api(\`/api/search?q=\${encodeURIComponent(q)}&kind=\${encodeURIComponent(kind)}\`);
570
+ renderMemories(r.results);
571
+ if (q && !r.semantic) toast("Keyword match only — configure embeddings for semantic search");
572
+ } catch (e) { toast("Error: " + e.message); }
573
+ }
574
+
575
+ // ---------------------------------------------------------------- mine / sync / reindex
576
+
577
+ async function runMine(ev) {
578
+ const full = ev && ev.shiftKey;
579
+ toast(full ? "Rescanning full git history…" : "Mining git history…");
580
+ try {
581
+ const r = await api("/api/mine" + (full ? "?full=1" : ""), { method: "POST" });
582
+ if (r.noCommits) {
583
+ toast("No git commits yet — make an initial commit, then try again.");
584
+ return;
585
+ }
586
+ if (r.noNewCommits) {
587
+ toast("No new commits since the last mine — Shift+click Mine commits to rescan all history.");
588
+ return;
589
+ }
590
+ if (r.scanned > 0 && r.proposed === 0) {
591
+ toast("Scanned " + r.scanned + " commit(s): none matched memory-worthy signals (try descriptive messages or dim mine --llm)");
592
+ return;
593
+ }
594
+ toast(\`Scanned \${r.scanned} commit(s): \${r.proposed} proposal(s) queued\`);
595
+ load();
596
+ } catch (e) { toast("Error: " + e.message); }
597
+ }
598
+
599
+ async function runSync() {
600
+ toast("Syncing…");
601
+ try {
602
+ const r = await api("/api/sync", { method: "POST" });
603
+ const mem = (n) => n + (n === 1 ? " memory" : " memories");
604
+ let msg;
605
+ if (r.memoriesPushed) msg = "Sent " + mem(r.memoriesPushed);
606
+ else if (r.memoriesQueued) msg = "Already on server (" + mem(r.memoriesQueued) + " unchanged)";
607
+ else msg = "Nothing to send";
608
+ if (r.applied) msg += ", received " + r.applied + " update" + (r.applied === 1 ? "" : "s");
609
+ else if (r.pulled) msg += ", pulled " + r.pulled + " (already up to date locally)";
610
+ else msg += ", nothing new from team";
611
+ if (r.needsFullUploadConfirm) {
612
+ msg += " — run dim sync in terminal to confirm upload";
613
+ }
614
+ toast(msg);
615
+ load();
616
+ } catch (e) { toast("Sync: " + e.message); }
617
+ }
618
+
619
+ async function runReindex() {
620
+ toast("Reindexing embeddings…");
621
+ try {
622
+ const r = await api("/api/reindex", { method: "POST" });
623
+ toast(r.provider ? \`Indexed \${r.indexed} with \${r.provider}\` : "No embedding provider — run Ollama or set OPENAI_API_KEY");
624
+ } catch (e) { toast("Error: " + e.message); }
625
+ }
626
+
627
+ // ---------------------------------------------------------------- cloud + keys
628
+
629
+ function openCloud() {
630
+ const c = state && state.cloud;
631
+ document.getElementById("cloud-status").textContent = c
632
+ ? \`Linked: \${c.server} → brain '\${c.brain}' (\${c.hasToken ? "token stored" : "⚠ NO TOKEN — paste API key and Link"})\`
633
+ : "Not linked to a team server yet. For local aidimag-cloud dev use http://localhost:3000";
634
+ if (c) {
635
+ document.getElementById("cl-server").value = c.server;
636
+ document.getElementById("cl-brain").value = c.brain;
637
+ }
638
+ document.getElementById("dlg-cloud").showModal();
639
+ }
640
+
641
+ async function cloudLink() {
642
+ try {
643
+ await api("/api/cloud/link", {
644
+ method: "POST",
645
+ body: JSON.stringify({
646
+ server: document.getElementById("cl-server").value.trim(),
647
+ brain: document.getElementById("cl-brain").value.trim(),
648
+ token: document.getElementById("cl-token").value.trim() || undefined,
649
+ }),
650
+ });
651
+ document.getElementById("cl-token").value = "";
652
+ toast("Linked — use Sync to exchange memory");
653
+ load(); openCloud();
654
+ } catch (e) { toast("Error: " + e.message); }
655
+ }
656
+
657
+ async function cloudUnlink() {
658
+ try { await api("/api/cloud/unlink", { method: "POST" }); toast("Unlinked"); load(); openCloud(); }
659
+ catch (e) { toast("Error: " + e.message); }
660
+ }
661
+
662
+ function keyParams() {
663
+ return {
664
+ server: document.getElementById("cl-server").value.trim(),
665
+ adminToken: document.getElementById("k-admin").value.trim(),
666
+ };
667
+ }
668
+
669
+ async function keyCreate() {
670
+ const p = keyParams();
671
+ if (!p.adminToken) { toast("Admin token required"); return; }
672
+ try {
673
+ const r = await api("/api/keys", {
674
+ method: "POST",
675
+ body: JSON.stringify({ ...p, brain: document.getElementById("k-brain").value.trim(), label: document.getElementById("k-label").value.trim() || undefined }),
676
+ });
677
+ document.getElementById("keys-out").innerHTML =
678
+ \`<div class="hint">New key (shown once — copy now):</div><div class="keyrow"><span>\${esc(r.key)}</span><button onclick="navigator.clipboard.writeText('\${esc(r.key)}').then(()=>toast('Copied'))">Copy</button></div>\`;
679
+ } catch (e) { toast("Error: " + e.message); }
680
+ }
681
+
682
+ async function keyList() {
683
+ const p = keyParams();
684
+ if (!p.adminToken) { toast("Admin token required"); return; }
685
+ try {
686
+ const r = await api(\`/api/keys?server=\${encodeURIComponent(p.server)}\`, {
687
+ headers: { "X-Aidimag-Admin-Token": p.adminToken },
688
+ });
689
+ document.getElementById("keys-out").innerHTML = r.keys.length
690
+ ? r.keys.map(k => \`<div class="keyrow"><span>\${k.revoked_at ? "✗" : "✓"} \${esc(k.key)} → \${esc(k.brain)}\${k.label ? " (" + esc(k.label) + ")" : ""}</span></div>\`).join("")
691
+ : '<div class="hint">No keys yet.</div>';
692
+ } catch (e) { toast("Error: " + e.message); }
693
+ }
694
+
695
+ // ---------------------------------------------------------------- tickets
696
+
697
+ const TK_HINTS = {
698
+ jira: { url: "Jira site URL", urlPh: "https://acme.atlassian.net", token: "email:apiToken (or a PAT)", needUrl: true, needToken: true },
699
+ github: { url: "Repo URL", urlPh: "https://github.com/acme/api", token: "GitHub token (repo read)", needUrl: true, needToken: true },
700
+ linear: { url: "", urlPh: "", token: "Linear API key", needUrl: false, needToken: true },
701
+ http: { url: "Middleware endpoint (GET /ticket/:id)", urlPh: "https://tickets.internal.acme.com", token: "Bearer token (optional)", needUrl: true, needToken: true },
702
+ remote: { url: "", urlPh: "", token: "", needUrl: false, needToken: false },
703
+ };
704
+
705
+ function ticketsProviderHint() {
706
+ const h = TK_HINTS[document.getElementById("tk-provider").value];
707
+ document.getElementById("tk-url-row").style.display = h.needUrl ? "" : "none";
708
+ document.getElementById("tk-token-row").style.display = h.needToken ? "" : "none";
709
+ if (h.needUrl) {
710
+ document.getElementById("tk-url-label").textContent = h.url;
711
+ document.getElementById("tk-url").placeholder = h.urlPh;
712
+ }
713
+ if (h.needToken) document.getElementById("tk-token-label").textContent = h.token + " — stored on this machine only, never in the repo";
714
+ }
715
+
716
+ function openTickets() {
717
+ const t = state && state.tickets;
718
+ document.getElementById("tk-status").textContent = t
719
+ ? \`Connected: \${t.provider}\${t.baseUrl ? " at " + t.baseUrl : ""} (\${t.hasCredential ? "credential stored" : "⚠ NO CREDENTIAL"})\`
720
+ : "No ticketing app connected — proposals will miss the why from your tickets.";
721
+ if (t) {
722
+ document.getElementById("tk-provider").value = t.provider;
723
+ if (t.baseUrl) document.getElementById("tk-url").value = t.baseUrl;
724
+ document.getElementById("tk-pattern").value = t.pattern || "";
725
+ }
726
+ ticketsProviderHint();
727
+ document.getElementById("dlg-tickets").showModal();
728
+ }
729
+
730
+ async function ticketsConnect() {
731
+ try {
732
+ const r = await api("/api/tickets/connect", {
733
+ method: "POST",
734
+ body: JSON.stringify({
735
+ provider: document.getElementById("tk-provider").value,
736
+ baseUrl: document.getElementById("tk-url").value.trim() || undefined,
737
+ token: document.getElementById("tk-token").value.trim() || undefined,
738
+ pattern: document.getElementById("tk-pattern").value.trim() || undefined,
739
+ testId: document.getElementById("tk-test").value.trim() || undefined,
740
+ }),
741
+ });
742
+ document.getElementById("tk-token").value = "";
743
+ toast(r.validated ? \`Connected ✓ validated with \${r.validated.id}: \${r.validated.title}\` : "Tickets connected");
744
+ load(); openTickets();
745
+ } catch (e) { toast("Error: " + e.message); }
746
+ }
747
+
748
+ async function ticketsDisconnect() {
749
+ try { await api("/api/tickets/disconnect", { method: "POST" }); toast("Tickets disconnected"); load(); openTickets(); }
750
+ catch (e) { toast("Error: " + e.message); }
751
+ }
752
+
753
+ async function ticketsShare(remove) {
754
+ const adminToken = document.getElementById("tk-admin").value.trim();
755
+ if (!adminToken) { toast("Admin token required"); return; }
756
+ try {
757
+ await api("/api/tickets/share", {
758
+ method: "POST",
759
+ body: JSON.stringify(remove ? { adminToken, remove: true } : {
760
+ adminToken,
761
+ provider: document.getElementById("tk-provider").value,
762
+ baseUrl: document.getElementById("tk-url").value.trim() || undefined,
763
+ credential: document.getElementById("tk-token").value.trim() || undefined,
764
+ }),
765
+ });
766
+ document.getElementById("tk-token").value = "";
767
+ toast(remove ? "Team ticket config removed" : "Team credentials stored on the server — teammates use provider 'remote'");
768
+ } catch (e) { toast("Error: " + e.message); }
769
+ }
770
+
771
+ let sim = null;
772
+ function renderGraph() {
773
+ const container = document.getElementById("graph");
774
+ container.innerHTML = "";
775
+ const W = container.clientWidth, H = container.clientHeight;
776
+
777
+ const nodes = [], links = [], pathNodes = new Map();
778
+ for (const m of state.memories) {
779
+ nodes.push({ id: m.id, type: "memory", label: m.claim.slice(0, 36) + (m.claim.length > 36 ? "…" : ""), status: m.status, conf: m.confidence });
780
+ for (const p of m.scope.paths) {
781
+ if (!pathNodes.has(p)) { pathNodes.set(p, { id: "path:" + p, type: "path", label: p }); }
782
+ links.push({ source: m.id, target: "path:" + p, kind: "scope" });
783
+ }
784
+ for (const l of m.links) {
785
+ if (l.fromId === m.id) links.push({ source: l.fromId, target: l.toId, kind: l.relation });
786
+ }
787
+ }
788
+ nodes.push(...pathNodes.values());
789
+
790
+ const svg = d3.select(container).append("svg").attr("width", W).attr("height", H);
791
+ const g = svg.append("g");
792
+ svg.call(d3.zoom().scaleExtent([0.3, 4]).on("zoom", e => g.attr("transform", e.transform)));
793
+
794
+ if (sim) sim.stop();
795
+ sim = d3.forceSimulation(nodes)
796
+ .force("link", d3.forceLink(links).id(d => d.id).distance(90))
797
+ .force("charge", d3.forceManyBody().strength(-220))
798
+ .force("center", d3.forceCenter(W / 2, H / 2))
799
+ .force("collide", d3.forceCollide(28));
800
+
801
+ const palette = graphPalette();
802
+
803
+ const link = g.append("g").selectAll("line").data(links).join("line")
804
+ .attr("stroke", d => d.kind === "contradicts" ? palette.REFUTED : palette.link)
805
+ .attr("stroke-width", 1.2);
806
+
807
+ const node = g.append("g").selectAll("g").data(nodes).join("g")
808
+ .style("cursor", "pointer")
809
+ .call(d3.drag()
810
+ .on("start", (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
811
+ .on("drag", (e, d) => { d.fx = e.x; d.fy = e.y; })
812
+ .on("end", (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }));
813
+
814
+ node.filter(d => d.type === "memory").append("circle")
815
+ .attr("r", d => 7 + d.conf * 10)
816
+ .attr("fill", d => palette[d.status] || palette.UNVERIFIED)
817
+ .attr("fill-opacity", 0.85);
818
+ node.filter(d => d.type === "path").append("rect")
819
+ .attr("x", -7).attr("y", -7).attr("width", 14).attr("height", 14).attr("rx", 3)
820
+ .attr("fill", palette.path).attr("fill-opacity", 0.85);
821
+
822
+ node.append("text").attr("dy", d => d.type === "memory" ? 7 + d.conf * 10 + 12 : 22).attr("text-anchor", "middle").text(d => d.label);
823
+
824
+ node.on("click", (e, d) => {
825
+ if (d.type !== "memory") return;
826
+ const card = document.getElementById("mem-" + d.id);
827
+ if (card) { card.scrollIntoView({ behavior: "smooth", block: "center" }); card.style.outline = "2px solid " + palette.primary; setTimeout(() => card.style.outline = "", 1500); }
828
+ });
829
+
830
+ sim.on("tick", () => {
831
+ link.attr("x1", d => d.source.x).attr("y1", d => d.source.y).attr("x2", d => d.target.x).attr("y2", d => d.target.y);
832
+ node.attr("transform", d => \`translate(\${d.x},\${d.y})\`);
833
+ });
834
+ }
835
+
836
+ window.addEventListener("resize", () => state && renderGraph());
837
+ load();
838
+ </script>
839
+ </body>
840
+ </html>`;
841
+ //# sourceMappingURL=page.js.map