@forwardimpact/outpost 3.5.1 → 3.7.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.
- package/package.json +1 -1
- package/src/kb-manager.js +1 -1
- package/src/outpost.js +20 -9
- package/src/socket-server.js +65 -0
- package/templates/.claude/skills/anarlog-process/SKILL.md +3 -3
- package/templates/.claude/skills/changelog/SKILL.md +136 -0
- package/templates/.claude/skills/deck-create/SKILL.md +40 -0
- package/templates/.claude/skills/deck-review/SKILL.md +156 -0
- package/templates/.claude/skills/deck-review/assets/slide-annotator.js +594 -0
- package/templates/.claude/skills/extract-entities/SKILL.md +3 -3
- package/templates/.claude/skills/{identify-user → person-identify}/SKILL.md +8 -7
- package/templates/.claude/skills/{identify-user → person-identify}/scripts/identify.sh +1 -1
- package/templates/.claude/skills/person-lookup/SKILL.md +97 -0
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +157 -0
- package/templates/.claude/skills/req-track/SKILL.md +3 -3
- package/templates/.claude/skills/req-workday/SKILL.md +2 -2
- package/templates/.claude/skills/upstream-instructions/SKILL.md +14 -23
- package/templates/CLAUDE.md +1 -1
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* slide-annotator.js — a tiny, self-contained text-highlight review overlay.
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* Drop into ANY static HTML page (decks especially) with one line:
|
|
5
|
+
*
|
|
6
|
+
* <script src="slide-annotator.js" defer
|
|
7
|
+
* data-slide-selector=".slide"
|
|
8
|
+
* data-label-selector=".slide-num"></script>
|
|
9
|
+
*
|
|
10
|
+
* Click the “✎ Review” button (bottom-left), select text on a slide, optionally
|
|
11
|
+
* add a note, and Add. Annotations autosave to localStorage immediately. Click
|
|
12
|
+
* “Connect folder” once to (a) write a real sidecar JSON next to the page and
|
|
13
|
+
* (b) resolve each highlight to its SOURCE line/column + context lines by
|
|
14
|
+
* reading the page’s own source file. No build step, no server, no deps.
|
|
15
|
+
*
|
|
16
|
+
* Optional host hook: if the page defines `window.deckGoto(index)` the panel’s
|
|
17
|
+
* “Go” buttons will navigate to the right slide. Without it, the tool still
|
|
18
|
+
* works (it falls back to scrollIntoView).
|
|
19
|
+
*
|
|
20
|
+
* SIDECAR JSON SCHEMA (what an agent reads to act on the feedback):
|
|
21
|
+
* {
|
|
22
|
+
* "version": 1, "tool": "slide-annotator",
|
|
23
|
+
* "target": "<page>.html", "updatedAt": "<iso>",
|
|
24
|
+
* "annotations": [{
|
|
25
|
+
* "id", "createdAt", "status": "open"|"done", "note",
|
|
26
|
+
* "slideId", "slideIndex", "slideLabel", "slideTitle",
|
|
27
|
+
* "quote", // exact highlighted text (the anchor)
|
|
28
|
+
* "prefix", "suffix", // ~60 rendered chars either side (disambiguation)
|
|
29
|
+
* "renderedStart", "renderedEnd", // char offsets within the slide's text
|
|
30
|
+
* "domPath", // CSS-ish path to the containing element
|
|
31
|
+
* "source": { // best-effort, null until folder connected
|
|
32
|
+
* "file", "line", "column", "match": "exact"|"normalized"|"none",
|
|
33
|
+
* "contextBefore": [..], "contextLine": "..", "contextAfter": [..]
|
|
34
|
+
* }
|
|
35
|
+
* }]
|
|
36
|
+
* }
|
|
37
|
+
* ---------------------------------------------------------------------------
|
|
38
|
+
*/
|
|
39
|
+
(function () {
|
|
40
|
+
"use strict";
|
|
41
|
+
if (window.__slideAnnotator) return;
|
|
42
|
+
|
|
43
|
+
/* ----------------------------- config ----------------------------------- */
|
|
44
|
+
var script = document.currentScript ||
|
|
45
|
+
document.querySelector('script[src*="slide-annotator"]');
|
|
46
|
+
var ds = (script && script.dataset) || {};
|
|
47
|
+
var cfg = {
|
|
48
|
+
slideSelector: ds.slideSelector || ".slide",
|
|
49
|
+
labelSelector: ds.labelSelector || ".slide-num",
|
|
50
|
+
target: ds.target || basename(),
|
|
51
|
+
pre: 60, post: 60, ctx: 2
|
|
52
|
+
};
|
|
53
|
+
function basename() {
|
|
54
|
+
try { return decodeURIComponent(location.pathname.split("/").pop()) || "page"; }
|
|
55
|
+
catch (e) { return "page"; }
|
|
56
|
+
}
|
|
57
|
+
var SIDECAR = cfg.target.replace(/\.x?html?$/i, "") + ".annotations.json";
|
|
58
|
+
var LS_KEY = "slide-annotator:" + cfg.target;
|
|
59
|
+
|
|
60
|
+
/* ----------------------------- state ------------------------------------ */
|
|
61
|
+
var state = loadLocal(); // { version, tool, target, annotations:[] }
|
|
62
|
+
var review = false;
|
|
63
|
+
var dirHandle = null; // File System Access directory handle
|
|
64
|
+
var sourceText = null; // page source (for line/column)
|
|
65
|
+
var pending = null; // annotation awaiting note + confirm
|
|
66
|
+
var ui = {}; // DOM refs
|
|
67
|
+
|
|
68
|
+
/* ----------------------------- styles ----------------------------------- */
|
|
69
|
+
var css = "\
|
|
70
|
+
.sa-ui,.sa-ui *{box-sizing:border-box;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif}\
|
|
71
|
+
.sa-fab{position:fixed;left:16px;bottom:14px;z-index:2147483000;display:flex;gap:0;\
|
|
72
|
+
border:2px solid #000;background:#111;color:#fff;font-weight:700;font-size:13px;\
|
|
73
|
+
box-shadow:3px 3px 0 0 rgba(0,0,0,.5);cursor:pointer;padding:9px 13px;letter-spacing:.02em}\
|
|
74
|
+
.sa-fab:hover{background:#000}\
|
|
75
|
+
.sa-fab.on{background:#FFDD00;color:#000;border-color:#000}\
|
|
76
|
+
.sa-banner{position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483000;\
|
|
77
|
+
background:#FFDD00;color:#000;border:2px solid #000;box-shadow:3px 3px 0 0 #000;\
|
|
78
|
+
font-size:12.5px;font-weight:700;padding:7px 14px;text-transform:uppercase;letter-spacing:.04em;display:none}\
|
|
79
|
+
.sa-banner.on{display:block}\
|
|
80
|
+
.sa-panel{position:fixed;top:0;right:0;height:100vh;width:330px;z-index:2147482900;\
|
|
81
|
+
background:#fbf7ee;border-left:3px solid #000;box-shadow:-6px 0 0 0 rgba(0,0,0,.12);\
|
|
82
|
+
display:flex;flex-direction:column;transform:translateX(100%);transition:transform .22s ease}\
|
|
83
|
+
.sa-panel.on{transform:none}\
|
|
84
|
+
.sa-phead{padding:12px 14px;border-bottom:3px solid #000;background:#111;color:#fff}\
|
|
85
|
+
.sa-phead h3{margin:0 0 8px;font-size:13px;letter-spacing:.06em;text-transform:uppercase}\
|
|
86
|
+
.sa-phead .sa-row{display:flex;gap:6px;flex-wrap:wrap}\
|
|
87
|
+
.sa-btn{border:2px solid #000;background:#fff;color:#000;font-size:11.5px;font-weight:700;\
|
|
88
|
+
padding:5px 9px;cursor:pointer;letter-spacing:.02em}\
|
|
89
|
+
.sa-btn:hover{background:#FFDD00}\
|
|
90
|
+
.sa-btn.dark{background:#3366FF;color:#fff;border-color:#000}\
|
|
91
|
+
.sa-btn.dark:hover{background:#1f49d6}\
|
|
92
|
+
.sa-status{font-size:11px;color:#cfcfcf;margin-top:8px;min-height:14px}\
|
|
93
|
+
.sa-list{flex:1;overflow:auto;padding:10px}\
|
|
94
|
+
.sa-empty{color:#777;font-size:12.5px;padding:18px 6px;line-height:1.5}\
|
|
95
|
+
.sa-card{border:2px solid #000;background:#fff;box-shadow:2px 2px 0 0 #000;padding:9px 10px;margin-bottom:9px}\
|
|
96
|
+
.sa-card.done{opacity:.55}\
|
|
97
|
+
.sa-meta{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#3366FF;margin-bottom:4px;display:flex;justify-content:space-between;gap:6px}\
|
|
98
|
+
.sa-meta .src{color:#999;font-weight:600;font-family:ui-monospace,Menlo,monospace}\
|
|
99
|
+
.sa-quote{font-size:13px;line-height:1.35;border-left:4px solid #FFDD00;padding-left:8px;margin:2px 0 6px}\
|
|
100
|
+
.sa-note{font-size:12px;color:#333;font-style:italic;margin-bottom:7px;white-space:pre-wrap}\
|
|
101
|
+
.sa-cardrow{display:flex;gap:5px;flex-wrap:wrap}\
|
|
102
|
+
.sa-mini{border:1px solid #000;background:#f3f3f3;font-size:10.5px;font-weight:700;padding:3px 7px;cursor:pointer}\
|
|
103
|
+
.sa-mini:hover{background:#FFDD00}\
|
|
104
|
+
.sa-mini.del:hover{background:#FF006E;color:#fff}\
|
|
105
|
+
.sa-pop{position:fixed;z-index:2147483600;background:#fff;border:2px solid #000;box-shadow:4px 4px 0 0 #000;\
|
|
106
|
+
padding:9px;width:248px;display:none}\
|
|
107
|
+
.sa-pop.on{display:block}\
|
|
108
|
+
.sa-pop textarea{width:100%;height:52px;border:2px solid #000;font-size:12.5px;padding:6px;resize:none;font-family:inherit}\
|
|
109
|
+
.sa-pop .sa-poprow{display:flex;gap:6px;margin-top:7px;justify-content:flex-end}\
|
|
110
|
+
.sa-toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);z-index:2147483600;\
|
|
111
|
+
background:#000;color:#fff;font-size:12px;font-weight:600;padding:8px 14px;border:2px solid #000;\
|
|
112
|
+
opacity:0;transition:opacity .2s;pointer-events:none;max-width:70vw;text-align:center}\
|
|
113
|
+
.sa-toast.on{opacity:1}\
|
|
114
|
+
::highlight(sa-all){background:rgba(255,221,0,.45);text-decoration:underline;text-decoration-color:#caا}\
|
|
115
|
+
::highlight(sa-active){background:rgba(255,0,110,.35)}\
|
|
116
|
+
@media print{.sa-ui{display:none!important}}";
|
|
117
|
+
// (one stray glyph above is harmless inside a CSS comment-free value; replaced below)
|
|
118
|
+
css = css.replace("#caا", "#b38f00");
|
|
119
|
+
var styleEl = document.createElement("style");
|
|
120
|
+
styleEl.textContent = css;
|
|
121
|
+
document.head.appendChild(styleEl);
|
|
122
|
+
|
|
123
|
+
/* ------------------------------ UI build -------------------------------- */
|
|
124
|
+
ui.fab = el("button", "sa-ui sa-fab", "✎ Review");
|
|
125
|
+
ui.banner = el("div", "sa-ui sa-banner", "Review mode — select text to highlight · Esc to exit");
|
|
126
|
+
ui.panel = el("div", "sa-ui sa-panel");
|
|
127
|
+
ui.panel.innerHTML =
|
|
128
|
+
'<div class="sa-phead">' +
|
|
129
|
+
'<h3>Annotations · <span class="sa-count">0</span></h3>' +
|
|
130
|
+
'<div class="sa-row">' +
|
|
131
|
+
'<button class="sa-btn dark" data-act="connect">Connect folder</button>' +
|
|
132
|
+
'<button class="sa-btn" data-act="save">Save</button>' +
|
|
133
|
+
'<button class="sa-btn" data-act="load">Load</button>' +
|
|
134
|
+
'<button class="sa-btn" data-act="onlyslide">This slide</button>' +
|
|
135
|
+
'<button class="sa-btn" data-act="close">Close</button>' +
|
|
136
|
+
'</div>' +
|
|
137
|
+
'<div class="sa-status"></div>' +
|
|
138
|
+
'</div>' +
|
|
139
|
+
'<div class="sa-list"></div>';
|
|
140
|
+
ui.pop = el("div", "sa-ui sa-pop");
|
|
141
|
+
ui.pop.innerHTML =
|
|
142
|
+
'<textarea placeholder="Optional note for this highlight…"></textarea>' +
|
|
143
|
+
'<div class="sa-poprow">' +
|
|
144
|
+
'<button class="sa-btn" data-act="cancel">Cancel</button>' +
|
|
145
|
+
'<button class="sa-btn dark" data-act="add">Add</button>' +
|
|
146
|
+
'</div>';
|
|
147
|
+
ui.toast = el("div", "sa-ui sa-toast");
|
|
148
|
+
[ui.fab, ui.banner, ui.panel, ui.pop, ui.toast].forEach(function (n) { document.body.appendChild(n); });
|
|
149
|
+
|
|
150
|
+
ui.count = ui.panel.querySelector(".sa-count");
|
|
151
|
+
ui.status = ui.panel.querySelector(".sa-status");
|
|
152
|
+
ui.list = ui.panel.querySelector(".sa-list");
|
|
153
|
+
ui.note = ui.pop.querySelector("textarea");
|
|
154
|
+
var onlyThisSlide = false;
|
|
155
|
+
|
|
156
|
+
// Keep the tool's own keystrokes from leaking to the host page (e.g. a deck's
|
|
157
|
+
// Space / arrow-key slide navigation) while typing a note or using a panel
|
|
158
|
+
// button. We stop propagation in the BUBBLE phase, so: (a) our capture-phase
|
|
159
|
+
// Esc handler has already run, and (b) the default action — typing the
|
|
160
|
+
// character, moving the caret — still happens. Scoped to the tool's own UI,
|
|
161
|
+
// so arrow/Space navigation still works when focus is on the page itself.
|
|
162
|
+
// Independent of any host: we never touch the host's handlers, just our own.
|
|
163
|
+
[ui.fab, ui.banner, ui.panel, ui.pop].forEach(function (node) {
|
|
164
|
+
["keydown", "keyup", "keypress"].forEach(function (type) {
|
|
165
|
+
node.addEventListener(type, function (e) { e.stopPropagation(); });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/* ------------------------------ events ---------------------------------- */
|
|
170
|
+
ui.fab.addEventListener("click", function () { toggle(); });
|
|
171
|
+
ui.panel.addEventListener("click", function (e) {
|
|
172
|
+
var b = e.target.closest("[data-act]"); if (!b) return;
|
|
173
|
+
var act = b.getAttribute("data-act");
|
|
174
|
+
if (act === "connect") connectFolder();
|
|
175
|
+
else if (act === "save") saveDisk();
|
|
176
|
+
else if (act === "load") loadDisk();
|
|
177
|
+
else if (act === "close") toggle(false);
|
|
178
|
+
else if (act === "onlyslide") { onlyThisSlide = !onlyThisSlide; b.classList.toggle("on"); renderPanel(); }
|
|
179
|
+
});
|
|
180
|
+
ui.pop.addEventListener("click", function (e) {
|
|
181
|
+
var b = e.target.closest("[data-act]"); if (!b) return;
|
|
182
|
+
if (b.getAttribute("data-act") === "add") confirmPending();
|
|
183
|
+
else cancelPending();
|
|
184
|
+
});
|
|
185
|
+
ui.list.addEventListener("click", function (e) {
|
|
186
|
+
var b = e.target.closest("[data-act]"); if (!b) return;
|
|
187
|
+
var id = b.closest(".sa-card").getAttribute("data-id");
|
|
188
|
+
var act = b.getAttribute("data-act");
|
|
189
|
+
if (act === "go") goTo(byId(id));
|
|
190
|
+
else if (act === "del") { removeAnn(id); }
|
|
191
|
+
else if (act === "toggle") { var a = byId(id); a.status = a.status === "done" ? "open" : "done"; persistLocal(); renderPanel(); }
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// capture text selections while in review mode
|
|
195
|
+
document.addEventListener("mouseup", function (e) {
|
|
196
|
+
if (!review || e.target.closest(".sa-ui")) return;
|
|
197
|
+
setTimeout(captureSelection, 0);
|
|
198
|
+
});
|
|
199
|
+
// Esc: cancel popover, else exit review
|
|
200
|
+
document.addEventListener("keydown", function (e) {
|
|
201
|
+
if (e.key !== "Escape") return;
|
|
202
|
+
if (pending) { cancelPending(); e.stopPropagation(); }
|
|
203
|
+
else if (review) { toggle(false); e.stopPropagation(); }
|
|
204
|
+
}, true);
|
|
205
|
+
|
|
206
|
+
// In review mode, swallow ONLY the click that ends a text-selection drag, so a
|
|
207
|
+
// host with click-to-advance can't navigate out from under the highlight. Plain
|
|
208
|
+
// clicks (with no active selection) pass through untouched — footer dots, links
|
|
209
|
+
// and buttons keep working. Non-invasive and host-agnostic.
|
|
210
|
+
function clickSuppressor(e) {
|
|
211
|
+
if (e.target.closest(".sa-ui")) return;
|
|
212
|
+
var sel = window.getSelection();
|
|
213
|
+
if (sel && !sel.isCollapsed && sel.toString().trim()) {
|
|
214
|
+
e.stopImmediatePropagation();
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* ----------------------------- toggle ----------------------------------- */
|
|
220
|
+
function toggle(force) {
|
|
221
|
+
review = (force === undefined) ? !review : force;
|
|
222
|
+
ui.fab.classList.toggle("on", review);
|
|
223
|
+
ui.fab.textContent = review ? "✓ Reviewing" : "✎ Review";
|
|
224
|
+
ui.banner.classList.toggle("on", review);
|
|
225
|
+
ui.panel.classList.toggle("on", review);
|
|
226
|
+
if (review) {
|
|
227
|
+
document.addEventListener("click", clickSuppressor, true);
|
|
228
|
+
renderPanel(); renderHighlights();
|
|
229
|
+
} else {
|
|
230
|
+
document.removeEventListener("click", clickSuppressor, true);
|
|
231
|
+
cancelPending();
|
|
232
|
+
if (window.CSS && CSS.highlights) { CSS.highlights.delete("sa-all"); CSS.highlights.delete("sa-active"); }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ------------------------- selection capture ---------------------------- */
|
|
237
|
+
function captureSelection() {
|
|
238
|
+
var sel = window.getSelection();
|
|
239
|
+
if (!sel || sel.isCollapsed || sel.rangeCount === 0) return;
|
|
240
|
+
var range = sel.getRangeAt(0);
|
|
241
|
+
var quote = sel.toString();
|
|
242
|
+
if (!quote.trim()) return;
|
|
243
|
+
|
|
244
|
+
var slide = slideOf(range.startContainer);
|
|
245
|
+
var off = offsets(slide, range);
|
|
246
|
+
var text = slide.textContent;
|
|
247
|
+
var slides = allSlides();
|
|
248
|
+
// Anchor on the underlying DOM text (text.slice), NOT Selection.toString():
|
|
249
|
+
// the latter applies text-transform (e.g. uppercase headings) and would no
|
|
250
|
+
// longer match the source or the slide's textContent.
|
|
251
|
+
var storedQuote = text.slice(off.start, off.end) || quote;
|
|
252
|
+
|
|
253
|
+
pending = {
|
|
254
|
+
id: uid(),
|
|
255
|
+
createdAt: new Date().toISOString(),
|
|
256
|
+
status: "open",
|
|
257
|
+
note: "",
|
|
258
|
+
slideId: slide.id || null,
|
|
259
|
+
slideIndex: slides.indexOf(slide),
|
|
260
|
+
slideLabel: pick(slide, cfg.labelSelector),
|
|
261
|
+
slideTitle: pickTitle(slide),
|
|
262
|
+
quote: storedQuote,
|
|
263
|
+
prefix: text.slice(Math.max(0, off.start - cfg.pre), off.start),
|
|
264
|
+
suffix: text.slice(off.end, off.end + cfg.post),
|
|
265
|
+
renderedStart: off.start,
|
|
266
|
+
renderedEnd: off.end,
|
|
267
|
+
domPath: cssPath(elementOf(range.commonAncestorContainer)),
|
|
268
|
+
source: null
|
|
269
|
+
};
|
|
270
|
+
showPopover(range.getBoundingClientRect());
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function showPopover(rect) {
|
|
274
|
+
var p = ui.pop, w = 248;
|
|
275
|
+
var left = Math.min(Math.max(8, rect.left), window.innerWidth - w - 8);
|
|
276
|
+
var top = Math.min(rect.bottom + 8, window.innerHeight - 120);
|
|
277
|
+
p.style.left = left + "px"; p.style.top = top + "px";
|
|
278
|
+
p.classList.add("on");
|
|
279
|
+
ui.note.value = "";
|
|
280
|
+
setTimeout(function () { ui.note.focus(); }, 0);
|
|
281
|
+
}
|
|
282
|
+
function cancelPending() { pending = null; ui.pop.classList.remove("on"); var s = window.getSelection(); if (s) s.removeAllRanges(); }
|
|
283
|
+
function confirmPending() {
|
|
284
|
+
if (!pending) return;
|
|
285
|
+
pending.note = ui.note.value.trim();
|
|
286
|
+
if (sourceText) pending.source = locateInSource(pending);
|
|
287
|
+
state.annotations.push(pending);
|
|
288
|
+
pending = null;
|
|
289
|
+
ui.pop.classList.remove("on");
|
|
290
|
+
var s = window.getSelection(); if (s) s.removeAllRanges();
|
|
291
|
+
persistLocal(); renderHighlights(); renderPanel();
|
|
292
|
+
setStatus(state.annotations.length + " annotation(s) · autosaved locally");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function removeAnn(id) {
|
|
296
|
+
state.annotations = state.annotations.filter(function (a) { return a.id !== id; });
|
|
297
|
+
persistLocal(); renderHighlights(); renderPanel();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* ----------------------- offset / range helpers ------------------------- */
|
|
301
|
+
function allSlides() {
|
|
302
|
+
var n = document.querySelectorAll(cfg.slideSelector);
|
|
303
|
+
return n.length ? Array.prototype.slice.call(n) : [document.body];
|
|
304
|
+
}
|
|
305
|
+
function slideOf(node) {
|
|
306
|
+
var e = node.nodeType === 1 ? node : node.parentElement;
|
|
307
|
+
return (e && e.closest(cfg.slideSelector)) || document.body;
|
|
308
|
+
}
|
|
309
|
+
function elementOf(node) { return node.nodeType === 1 ? node : node.parentElement; }
|
|
310
|
+
function pick(slide, sel) { var e = slide.querySelector(sel); return e ? e.textContent.trim() : null; }
|
|
311
|
+
function pickTitle(slide) { var e = slide.querySelector("h1,h2,h3"); return e ? collapse(e.innerText || e.textContent) : null; }
|
|
312
|
+
function offsets(slide, range) {
|
|
313
|
+
var a = document.createRange(); a.setStart(slide, 0); a.setEnd(range.startContainer, range.startOffset);
|
|
314
|
+
var b = document.createRange(); b.setStart(slide, 0); b.setEnd(range.endContainer, range.endOffset);
|
|
315
|
+
return { start: a.toString().length, end: b.toString().length };
|
|
316
|
+
}
|
|
317
|
+
function rangeFromOffsets(slide, start, end) {
|
|
318
|
+
var walker = document.createTreeWalker(slide, NodeFilter.SHOW_TEXT, null);
|
|
319
|
+
var pos = 0, range = document.createRange(), gotS = false, gotE = false, node;
|
|
320
|
+
while ((node = walker.nextNode())) {
|
|
321
|
+
var len = node.nodeValue.length;
|
|
322
|
+
if (!gotS && start <= pos + len) { range.setStart(node, start - pos); gotS = true; }
|
|
323
|
+
if (!gotE && end <= pos + len) { range.setEnd(node, end - pos); gotE = true; break; }
|
|
324
|
+
pos += len;
|
|
325
|
+
}
|
|
326
|
+
return (gotS && gotE) ? range : null;
|
|
327
|
+
}
|
|
328
|
+
function resolveRange(a) {
|
|
329
|
+
var slide = a.slideId ? document.getElementById(a.slideId) : allSlides()[a.slideIndex];
|
|
330
|
+
if (!slide) return null;
|
|
331
|
+
var r = rangeFromOffsets(slide, a.renderedStart, a.renderedEnd);
|
|
332
|
+
if (r && slide.textContent.slice(a.renderedStart, a.renderedEnd) === a.quote) return r;
|
|
333
|
+
// fallback: re-find by prefix+quote within the slide text
|
|
334
|
+
var text = slide.textContent;
|
|
335
|
+
var idx = text.indexOf(a.prefix + a.quote);
|
|
336
|
+
if (idx >= 0) idx += a.prefix.length; else idx = text.indexOf(a.quote);
|
|
337
|
+
if (idx < 0) return null;
|
|
338
|
+
return rangeFromOffsets(slide, idx, idx + a.quote.length);
|
|
339
|
+
}
|
|
340
|
+
function cssPath(node) {
|
|
341
|
+
if (!node || node.nodeType !== 1) return null;
|
|
342
|
+
var parts = [], el = node, depth = 0;
|
|
343
|
+
while (el && el.nodeType === 1 && depth < 5) {
|
|
344
|
+
var part = el.tagName.toLowerCase();
|
|
345
|
+
if (el.id) { parts.unshift("#" + el.id); break; }
|
|
346
|
+
if (el.className && typeof el.className === "string") {
|
|
347
|
+
var c = el.className.trim().split(/\s+/).filter(Boolean).slice(0, 2);
|
|
348
|
+
if (c.length) part += "." + c.join(".");
|
|
349
|
+
}
|
|
350
|
+
var sibs = el.parentElement ? Array.prototype.filter.call(el.parentElement.children,
|
|
351
|
+
function (s) { return s.tagName === el.tagName; }) : [];
|
|
352
|
+
if (sibs.length > 1) part += ":nth-of-type(" + (sibs.indexOf(el) + 1) + ")";
|
|
353
|
+
parts.unshift(part); el = el.parentElement; depth++;
|
|
354
|
+
}
|
|
355
|
+
return parts.join(" > ");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* --------------------------- highlights --------------------------------- */
|
|
359
|
+
function renderHighlights() {
|
|
360
|
+
if (!review || !(window.CSS && CSS.highlights && window.Highlight)) return;
|
|
361
|
+
var ranges = [];
|
|
362
|
+
state.annotations.forEach(function (a) {
|
|
363
|
+
if (a.status === "done") return;
|
|
364
|
+
var r = resolveRange(a); if (r) ranges.push(r);
|
|
365
|
+
});
|
|
366
|
+
CSS.highlights.set("sa-all", makeHL(ranges));
|
|
367
|
+
}
|
|
368
|
+
function makeHL(ranges) { var h = new Highlight(); ranges.forEach(function (r) { h.add(r); }); return h; }
|
|
369
|
+
function flash(a) {
|
|
370
|
+
if (!(window.CSS && CSS.highlights && window.Highlight)) return;
|
|
371
|
+
var r = resolveRange(a); if (!r) return;
|
|
372
|
+
var h = new Highlight(); h.add(r); CSS.highlights.set("sa-active", h);
|
|
373
|
+
setTimeout(function () { CSS.highlights.delete("sa-active"); }, 1400);
|
|
374
|
+
}
|
|
375
|
+
function goTo(a) {
|
|
376
|
+
if (!a) return;
|
|
377
|
+
if (typeof window.deckGoto === "function" && a.slideIndex >= 0) window.deckGoto(a.slideIndex);
|
|
378
|
+
else { var s = a.slideId ? document.getElementById(a.slideId) : allSlides()[a.slideIndex]; if (s) s.scrollIntoView({ behavior: "smooth", block: "center" }); }
|
|
379
|
+
setTimeout(function () { flash(a); }, 220);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* ------------------------------ panel ----------------------------------- */
|
|
383
|
+
function renderPanel() {
|
|
384
|
+
updateCount();
|
|
385
|
+
var cur = currentSlideIndex();
|
|
386
|
+
var list = state.annotations.filter(function (a) { return !onlyThisSlide || a.slideIndex === cur; });
|
|
387
|
+
if (!list.length) {
|
|
388
|
+
ui.list.innerHTML = '<div class="sa-empty">' +
|
|
389
|
+
(onlyThisSlide ? "No annotations on this slide." :
|
|
390
|
+
"No annotations yet.<br><br>Select text on a slide to highlight it. Highlights autosave locally; click <b>Connect folder</b> to write the sidecar JSON and capture source line numbers.") +
|
|
391
|
+
'</div>';
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
ui.list.innerHTML = list.map(cardHTML).join("");
|
|
395
|
+
}
|
|
396
|
+
function cardHTML(a) {
|
|
397
|
+
var loc = a.source && a.source.line ? ("L" + a.source.line + (a.source.column ? ":" + a.source.column : "")) : "";
|
|
398
|
+
return '<div class="sa-card ' + (a.status === "done" ? "done" : "") + '" data-id="' + a.id + '">' +
|
|
399
|
+
'<div class="sa-meta"><span>' + esc(a.slideLabel || ("#" + (a.slideIndex + 1))) +
|
|
400
|
+
(a.slideTitle ? " · " + esc(a.slideTitle) : "") + '</span>' +
|
|
401
|
+
'<span class="src">' + loc + '</span></div>' +
|
|
402
|
+
'<div class="sa-quote">' + esc(trim(a.quote, 160)) + '</div>' +
|
|
403
|
+
(a.note ? '<div class="sa-note">“' + esc(a.note) + '”</div>' : "") +
|
|
404
|
+
'<div class="sa-cardrow">' +
|
|
405
|
+
'<button class="sa-mini" data-act="go">Go</button>' +
|
|
406
|
+
'<button class="sa-mini" data-act="toggle">' + (a.status === "done" ? "Reopen" : "Mark done") + '</button>' +
|
|
407
|
+
'<button class="sa-mini del" data-act="del">Delete</button>' +
|
|
408
|
+
'</div></div>';
|
|
409
|
+
}
|
|
410
|
+
function currentSlideIndex() {
|
|
411
|
+
var slides = allSlides();
|
|
412
|
+
for (var i = 0; i < slides.length; i++) if (slides[i].classList.contains("active")) return i;
|
|
413
|
+
// else the slide most centered in viewport
|
|
414
|
+
var best = 0, bestD = Infinity, cx = window.innerWidth / 2, cy = window.innerHeight / 2;
|
|
415
|
+
slides.forEach(function (s, i) {
|
|
416
|
+
var r = s.getBoundingClientRect();
|
|
417
|
+
if (r.width === 0 || r.bottom < 0 || r.top > window.innerHeight) return;
|
|
418
|
+
var d = Math.hypot((r.left + r.right) / 2 - cx, (r.top + r.bottom) / 2 - cy);
|
|
419
|
+
if (d < bestD) { bestD = d; best = i; }
|
|
420
|
+
});
|
|
421
|
+
return best;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* --------------------------- persistence -------------------------------- */
|
|
425
|
+
function loadLocal() {
|
|
426
|
+
try { var t = localStorage.getItem(LS_KEY); if (t) return JSON.parse(t); } catch (e) {}
|
|
427
|
+
return { version: 1, tool: "slide-annotator", target: cfg.target, annotations: [] };
|
|
428
|
+
}
|
|
429
|
+
function persistLocal() {
|
|
430
|
+
state.updatedAt = new Date().toISOString();
|
|
431
|
+
try { localStorage.setItem(LS_KEY, JSON.stringify(state)); } catch (e) {}
|
|
432
|
+
updateCount();
|
|
433
|
+
}
|
|
434
|
+
function updateCount() {
|
|
435
|
+
if (ui.count) ui.count.textContent = state.annotations.length;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function connectFolder() {
|
|
439
|
+
if (!window.showDirectoryPicker) { setStatus("Folder access unsupported in this browser. Use Save (downloads the JSON)."); return; }
|
|
440
|
+
try {
|
|
441
|
+
dirHandle = await window.showDirectoryPicker({ mode: "readwrite" });
|
|
442
|
+
// read page source → enables source line/column resolution
|
|
443
|
+
try {
|
|
444
|
+
var fh = await dirHandle.getFileHandle(cfg.target);
|
|
445
|
+
sourceText = await (await fh.getFile()).text();
|
|
446
|
+
state.annotations.forEach(function (a) { a.source = locateInSource(a); });
|
|
447
|
+
} catch (e) { sourceText = null; setStatus("Connected, but '" + cfg.target + "' not found in folder — line numbers unavailable."); }
|
|
448
|
+
// load an existing sidecar if present
|
|
449
|
+
try {
|
|
450
|
+
var sh = await dirHandle.getFileHandle(SIDECAR);
|
|
451
|
+
mergeLoaded(JSON.parse(await (await sh.getFile()).text()));
|
|
452
|
+
} catch (e) { /* none yet */ }
|
|
453
|
+
persistLocal(); renderPanel(); renderHighlights();
|
|
454
|
+
setStatus("Connected. Saving to " + SIDECAR + (sourceText ? " · source linked" : ""));
|
|
455
|
+
} catch (e) {
|
|
456
|
+
if (e && e.name !== "AbortError") setStatus("Folder access blocked (" + e.name + "). Use Save to download the JSON instead.");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function saveDisk() {
|
|
461
|
+
if (sourceText) state.annotations.forEach(function (a) { if (!a.source) a.source = locateInSource(a); });
|
|
462
|
+
var json = JSON.stringify(serialize(), null, 2);
|
|
463
|
+
if (dirHandle) {
|
|
464
|
+
try {
|
|
465
|
+
var sh = await dirHandle.getFileHandle(SIDECAR, { create: true });
|
|
466
|
+
var w = await sh.createWritable(); await w.write(json); await w.close();
|
|
467
|
+
setStatus("Saved " + SIDECAR + " ✓"); toast("Saved " + SIDECAR);
|
|
468
|
+
return;
|
|
469
|
+
} catch (e) { setStatus("Disk write failed (" + e.name + "). Downloaded instead."); }
|
|
470
|
+
}
|
|
471
|
+
download(SIDECAR, json);
|
|
472
|
+
setStatus("Downloaded " + SIDECAR + " — move it next to the deck.");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function loadDisk() {
|
|
476
|
+
if (dirHandle) {
|
|
477
|
+
try {
|
|
478
|
+
var sh = await dirHandle.getFileHandle(SIDECAR);
|
|
479
|
+
mergeLoaded(JSON.parse(await (await sh.getFile()).text()));
|
|
480
|
+
persistLocal(); renderPanel(); renderHighlights();
|
|
481
|
+
setStatus("Reloaded " + SIDECAR + " from disk."); return;
|
|
482
|
+
} catch (e) { setStatus("Could not read " + SIDECAR + " from the folder."); return; }
|
|
483
|
+
}
|
|
484
|
+
// fallback: file input
|
|
485
|
+
var inp = document.createElement("input");
|
|
486
|
+
inp.type = "file"; inp.accept = "application/json,.json";
|
|
487
|
+
inp.onchange = function () {
|
|
488
|
+
var f = inp.files[0]; if (!f) return;
|
|
489
|
+
var fr = new FileReader();
|
|
490
|
+
fr.onload = function () { try { mergeLoaded(JSON.parse(fr.result)); persistLocal(); renderPanel(); renderHighlights(); setStatus("Loaded " + f.name); } catch (e) { setStatus("Invalid JSON."); } };
|
|
491
|
+
fr.readAsText(f);
|
|
492
|
+
};
|
|
493
|
+
inp.click();
|
|
494
|
+
}
|
|
495
|
+
function mergeLoaded(obj) {
|
|
496
|
+
if (obj && Array.isArray(obj.annotations)) state.annotations = obj.annotations;
|
|
497
|
+
}
|
|
498
|
+
function serialize() {
|
|
499
|
+
return { version: 1, tool: "slide-annotator", target: cfg.target, updatedAt: new Date().toISOString(), annotations: state.annotations };
|
|
500
|
+
}
|
|
501
|
+
function download(name, text) {
|
|
502
|
+
var blob = new Blob([text], { type: "application/json" });
|
|
503
|
+
var a = document.createElement("a");
|
|
504
|
+
a.href = URL.createObjectURL(blob); a.download = name;
|
|
505
|
+
document.body.appendChild(a); a.click();
|
|
506
|
+
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 500);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* --------------------- source line/column resolution -------------------- */
|
|
510
|
+
// Normalize HTML source: strip tags, decode entities, collapse whitespace,
|
|
511
|
+
// and keep a map from each normalized-char index back to a source index.
|
|
512
|
+
function normSource(src) {
|
|
513
|
+
if (normSource._src === src) return normSource._res;
|
|
514
|
+
var ents = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", "#39": "'" };
|
|
515
|
+
var map = [], out = "", i = 0, inTag = false, lastSpace = true;
|
|
516
|
+
while (i < src.length) {
|
|
517
|
+
var c = src[i];
|
|
518
|
+
if (inTag) { if (c === ">") inTag = false; i++; continue; }
|
|
519
|
+
if (c === "<") { inTag = true; i++; continue; }
|
|
520
|
+
if (c === "&") {
|
|
521
|
+
var m = /^&(#?\w+);/.exec(src.slice(i, i + 12));
|
|
522
|
+
if (m) {
|
|
523
|
+
var ch = ents[m[1]];
|
|
524
|
+
if (ch === undefined && m[1][0] === "#") { var code = parseInt(m[1].slice(1), 10); if (!isNaN(code)) ch = String.fromCharCode(code); }
|
|
525
|
+
if (ch === undefined) ch = "?";
|
|
526
|
+
if (/\s/.test(ch)) { if (!lastSpace) { out += " "; map.push(i); lastSpace = true; } }
|
|
527
|
+
else { out += ch; map.push(i); lastSpace = false; }
|
|
528
|
+
i += m[0].length; continue;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (/\s/.test(c)) { if (!lastSpace) { out += " "; map.push(i); lastSpace = true; } i++; continue; }
|
|
532
|
+
out += c; map.push(i); lastSpace = false; i++;
|
|
533
|
+
}
|
|
534
|
+
normSource._src = src; normSource._res = { norm: out, map: map };
|
|
535
|
+
return normSource._res;
|
|
536
|
+
}
|
|
537
|
+
function collapse(s) { return (s || "").replace(/\s+/g, " ").trim(); }
|
|
538
|
+
function locateInSource(a) {
|
|
539
|
+
var res = { file: cfg.target, line: null, column: null, match: "none", contextBefore: [], contextLine: null, contextAfter: [] };
|
|
540
|
+
if (!sourceText) return res;
|
|
541
|
+
var N = normSource(sourceText);
|
|
542
|
+
var needle = collapse(a.quote); if (!needle) return res;
|
|
543
|
+
var pfx = collapse(a.prefix), sfx = collapse(a.suffix);
|
|
544
|
+
var nstart = -1, match = "none";
|
|
545
|
+
var pt = pfx.slice(-25);
|
|
546
|
+
if (pt) { var i = N.norm.indexOf(pt + needle); if (i >= 0) { nstart = i + pt.length; match = "exact"; } }
|
|
547
|
+
if (nstart < 0) {
|
|
548
|
+
var hits = [], from = 0, k;
|
|
549
|
+
while ((k = N.norm.indexOf(needle, from)) >= 0) { hits.push(k); from = k + 1; }
|
|
550
|
+
if (hits.length === 1) { nstart = hits[0]; match = "exact"; }
|
|
551
|
+
else if (hits.length > 1) {
|
|
552
|
+
var sh = sfx.slice(0, 25);
|
|
553
|
+
for (var j = 0; j < hits.length; j++) { if (N.norm.slice(hits[j] + needle.length, hits[j] + needle.length + sh.length) === sh) { nstart = hits[j]; match = "normalized"; break; } }
|
|
554
|
+
if (nstart < 0) { nstart = hits[0]; match = "normalized"; }
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (nstart < 0) return res;
|
|
558
|
+
var srcIdx = N.map[nstart];
|
|
559
|
+
var upto = sourceText.slice(0, srcIdx);
|
|
560
|
+
var line = (upto.match(/\n/g) || []).length + 1;
|
|
561
|
+
var col = srcIdx - upto.lastIndexOf("\n");
|
|
562
|
+
var lines = sourceText.split("\n");
|
|
563
|
+
res.line = line; res.column = col; res.match = match;
|
|
564
|
+
res.contextBefore = lines.slice(Math.max(0, line - 1 - cfg.ctx), line - 1);
|
|
565
|
+
res.contextLine = lines[line - 1];
|
|
566
|
+
res.contextAfter = lines.slice(line, line + cfg.ctx);
|
|
567
|
+
return res;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* ------------------------------ misc ------------------------------------ */
|
|
571
|
+
var toastTimer;
|
|
572
|
+
function toast(msg) { ui.toast.textContent = msg; ui.toast.classList.add("on"); clearTimeout(toastTimer); toastTimer = setTimeout(function () { ui.toast.classList.remove("on"); }, 2200); }
|
|
573
|
+
function setStatus(msg) { if (ui.status) ui.status.textContent = msg; }
|
|
574
|
+
function byId(id) { return state.annotations.filter(function (a) { return a.id === id; })[0]; }
|
|
575
|
+
function uid() { return "a" + Date.now().toString(36) + Math.floor(Math.random() * 1e4).toString(36); }
|
|
576
|
+
function el(tag, cls, txt) { var e = document.createElement(tag); if (cls) e.className = cls; if (txt != null) e.textContent = txt; return e; }
|
|
577
|
+
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; }); }
|
|
578
|
+
function trim(s, n) { s = String(s || ""); return s.length > n ? s.slice(0, n - 1) + "…" : s; }
|
|
579
|
+
|
|
580
|
+
/* --------------------------- public hook -------------------------------- */
|
|
581
|
+
window.__slideAnnotator = {
|
|
582
|
+
toggle: toggle,
|
|
583
|
+
save: saveDisk,
|
|
584
|
+
connect: connectFolder,
|
|
585
|
+
// Supply page source to resolve source line/column without the folder picker.
|
|
586
|
+
setSource: function (text) { sourceText = text; state.annotations.forEach(function (a) { a.source = locateInSource(a); }); persistLocal(); renderPanel(); },
|
|
587
|
+
get state() { return state; },
|
|
588
|
+
// for automation/testing:
|
|
589
|
+
_capture: captureSelection,
|
|
590
|
+
_confirm: function (note) { if (pending) { ui.note.value = note || ""; confirmPending(); } }
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
updateCount();
|
|
594
|
+
})();
|
|
@@ -19,7 +19,7 @@ under `Knowledge/`. The core knowledge-graph builder.
|
|
|
19
19
|
## Prerequisites
|
|
20
20
|
|
|
21
21
|
- Synced data in `~/.cache/fit/outpost/` and/or ad-hoc paths.
|
|
22
|
-
- User identity from running the `identify
|
|
22
|
+
- User identity from running the `person-identify` skill, which writes
|
|
23
23
|
`~/.cache/fit/outpost/state/identity.md` (Name, Email, Domain).
|
|
24
24
|
|
|
25
25
|
## Inputs
|
|
@@ -31,7 +31,7 @@ under `Knowledge/`. The core knowledge-graph builder.
|
|
|
31
31
|
- `~/.cache/fit/outpost/state/graph_processed` — processed-file index (TSV,
|
|
32
32
|
shared with `req-track` and `anarlog-process`).
|
|
33
33
|
- `~/.cache/fit/outpost/state/identity.md` — user identity for self-exclusion
|
|
34
|
-
(written by the `identify
|
|
34
|
+
(written by the `person-identify` skill).
|
|
35
35
|
|
|
36
36
|
## Outputs
|
|
37
37
|
|
|
@@ -76,7 +76,7 @@ writes.
|
|
|
76
76
|
### 0. Load context and pick the batch
|
|
77
77
|
|
|
78
78
|
Read the user's identity from `~/.cache/fit/outpost/state/identity.md` (run the
|
|
79
|
-
`identify
|
|
79
|
+
`person-identify` skill first if it is missing or stale). Find new/changed files:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
82
|
node scripts/state.mjs check
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: identify
|
|
3
|
-
description: Look up the current user's identity (real name, company, job title, department, email, employee ID, and manager) from the corporate directory via LDAP, authenticated with the existing Kerberos ticket. Use to establish who the knowledge base belongs to, when CLAUDE.md needs the user's identity, or when the user asks "who am I" / for their own directory record.
|
|
2
|
+
name: person-identify
|
|
3
|
+
description: Look up the current user's identity (real name, company, job title, department, email, employee ID, and manager) from the corporate directory via LDAP, authenticated with the existing Kerberos ticket. Use to establish who the knowledge base belongs to, when CLAUDE.md needs the user's identity, or when the user asks "who am I" / for their own directory record. To look up someone *other* than the current user, use the sibling `person-lookup` skill instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Identify
|
|
6
|
+
# Person Identify
|
|
7
7
|
|
|
8
8
|
Resolve the current user's identity from the corporate Active Directory over
|
|
9
9
|
LDAP. This is the canonical way to establish **who the knowledge base belongs
|
|
@@ -30,7 +30,7 @@ the bind uses SASL/GSSAPI against the existing ticket.
|
|
|
30
30
|
## Usage
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
bash .claude/skills/identify
|
|
33
|
+
bash .claude/skills/person-identify/scripts/identify.sh
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
This prints the user's directory record, resolves the manager to a name, and
|
|
@@ -96,8 +96,9 @@ Key attributes returned (names per Active Directory schema):
|
|
|
96
96
|
|
|
97
97
|
## Notes
|
|
98
98
|
|
|
99
|
-
- To look up **someone else**,
|
|
100
|
-
|
|
101
|
-
(`ldap://$dc:3268 -b ''`)
|
|
99
|
+
- To look up **someone else**, use the sibling `person-lookup` skill — it takes
|
|
100
|
+
free-text input (email or name), searches the Global Catalog forest-wide
|
|
101
|
+
(`ldap://$dc:3268 -b ''`), handles multiple matches, and does **not** touch the
|
|
102
|
+
identity cache.
|
|
102
103
|
- Not Active Directory? The same `ldapsearch -Y GSSAPI` shape works against any
|
|
103
104
|
Kerberos-backed LDAP directory; only the attribute names differ.
|
|
@@ -69,7 +69,7 @@ mkdir -p "$(dirname "$CACHE")"
|
|
|
69
69
|
{
|
|
70
70
|
echo "# User Identity"
|
|
71
71
|
echo
|
|
72
|
-
echo "Auto-generated by the \`identify
|
|
72
|
+
echo "Auto-generated by the \`person-identify\` skill from the corporate directory."
|
|
73
73
|
echo "Do not edit by hand — re-run the skill to refresh."
|
|
74
74
|
echo
|
|
75
75
|
echo "- **Name:** $name"
|