@cleocode/cleo 2026.5.86 → 2026.5.87
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/assets/viewer/index.html +2 -0
- package/assets/viewer/styles.css +64 -0
- package/assets/viewer/viewer.js +97 -0
- package/dist/cli/index.js +51 -7
- package/dist/cli/index.js.map +2 -2
- package/package.json +11 -11
package/assets/viewer/index.html
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
<header class="topbar">
|
|
11
11
|
<h1>CLEO Docs</h1>
|
|
12
12
|
<input id="filter" type="search" placeholder="Filter by slug, title, type..." autocomplete="off" />
|
|
13
|
+
<input id="search" type="search" placeholder="Search content (ranked)..." autocomplete="off" aria-label="Search docs by content" />
|
|
13
14
|
</header>
|
|
14
15
|
<main class="layout">
|
|
15
16
|
<aside id="sidebar">
|
|
17
|
+
<div id="search-results" aria-live="polite" hidden></div>
|
|
16
18
|
<div id="doc-list" aria-live="polite">Loading docs...</div>
|
|
17
19
|
</aside>
|
|
18
20
|
<section id="content">
|
package/assets/viewer/styles.css
CHANGED
|
@@ -130,3 +130,67 @@ html, body {
|
|
|
130
130
|
|
|
131
131
|
.hint { color: var(--text-muted); }
|
|
132
132
|
.error { color: #ff7f7f; }
|
|
133
|
+
|
|
134
|
+
/* T9647 — search results UI */
|
|
135
|
+
#search {
|
|
136
|
+
flex: 1;
|
|
137
|
+
max-width: 360px;
|
|
138
|
+
background: var(--surface-2);
|
|
139
|
+
border: 1px solid var(--border);
|
|
140
|
+
color: var(--text);
|
|
141
|
+
padding: 0.45rem 0.75rem;
|
|
142
|
+
border-radius: 6px;
|
|
143
|
+
font: inherit;
|
|
144
|
+
}
|
|
145
|
+
#search:focus { outline: 2px solid var(--accent); outline-offset: 0; }
|
|
146
|
+
|
|
147
|
+
#search-results {
|
|
148
|
+
padding: 0.25rem 0;
|
|
149
|
+
border-bottom: 1px solid var(--border);
|
|
150
|
+
}
|
|
151
|
+
#search-results .result {
|
|
152
|
+
display: block;
|
|
153
|
+
padding: 0.55rem 1rem;
|
|
154
|
+
color: var(--text);
|
|
155
|
+
text-decoration: none;
|
|
156
|
+
border-left: 3px solid transparent;
|
|
157
|
+
}
|
|
158
|
+
#search-results .result:hover {
|
|
159
|
+
background: var(--surface-2);
|
|
160
|
+
border-left-color: var(--accent);
|
|
161
|
+
}
|
|
162
|
+
#search-results .result-head {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: baseline;
|
|
165
|
+
gap: 0.5rem;
|
|
166
|
+
font-size: 0.9rem;
|
|
167
|
+
}
|
|
168
|
+
#search-results .result-name {
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
color: var(--accent);
|
|
171
|
+
flex: 1;
|
|
172
|
+
word-break: break-word;
|
|
173
|
+
}
|
|
174
|
+
#search-results .result-type {
|
|
175
|
+
font-size: 0.7rem;
|
|
176
|
+
color: var(--text-muted);
|
|
177
|
+
text-transform: uppercase;
|
|
178
|
+
letter-spacing: 0.03em;
|
|
179
|
+
}
|
|
180
|
+
#search-results .result-score {
|
|
181
|
+
font-size: 0.75rem;
|
|
182
|
+
color: var(--text-muted);
|
|
183
|
+
font-variant-numeric: tabular-nums;
|
|
184
|
+
}
|
|
185
|
+
#search-results .result-snippet {
|
|
186
|
+
color: var(--text-muted);
|
|
187
|
+
font-size: 0.8rem;
|
|
188
|
+
line-height: 1.4;
|
|
189
|
+
margin-top: 0.2rem;
|
|
190
|
+
}
|
|
191
|
+
#search-results mark {
|
|
192
|
+
background: rgba(106, 167, 255, 0.25);
|
|
193
|
+
color: inherit;
|
|
194
|
+
padding: 0 2px;
|
|
195
|
+
border-radius: 2px;
|
|
196
|
+
}
|
package/assets/viewer/viewer.js
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
const $list = document.getElementById('doc-list');
|
|
7
7
|
const $render = document.getElementById('doc-render');
|
|
8
8
|
const $filter = document.getElementById('filter');
|
|
9
|
+
const $search = document.getElementById('search');
|
|
10
|
+
const $searchResults = document.getElementById('search-results');
|
|
9
11
|
|
|
10
12
|
const state = {
|
|
11
13
|
docs: [],
|
|
12
14
|
activeSlug: null,
|
|
15
|
+
searchSeq: 0,
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
function escapeHtml(str) {
|
|
@@ -287,6 +290,100 @@ function slugFromPath(pathname) {
|
|
|
287
290
|
|
|
288
291
|
$filter.addEventListener('input', (e) => applyFilter(e.target.value));
|
|
289
292
|
|
|
293
|
+
// ── Search (T9647) — debounced /api/search with ranked results ──────────────
|
|
294
|
+
|
|
295
|
+
function debounce(fn, ms) {
|
|
296
|
+
let timer = null;
|
|
297
|
+
return function debounced(...args) {
|
|
298
|
+
if (timer !== null) clearTimeout(timer);
|
|
299
|
+
timer = setTimeout(() => {
|
|
300
|
+
timer = null;
|
|
301
|
+
fn.apply(this, args);
|
|
302
|
+
}, ms);
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function highlightMatches(snippet, query) {
|
|
307
|
+
if (!query) return escapeHtml(snippet);
|
|
308
|
+
const terms = query
|
|
309
|
+
.toLowerCase()
|
|
310
|
+
.split(/\s+/)
|
|
311
|
+
.filter((t) => t.length >= 2);
|
|
312
|
+
if (terms.length === 0) return escapeHtml(snippet);
|
|
313
|
+
// Build a regex that matches any term, case-insensitive. Escape regex metachars.
|
|
314
|
+
const pattern = terms
|
|
315
|
+
.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
316
|
+
.join('|');
|
|
317
|
+
const re = new RegExp(`(${pattern})`, 'gi');
|
|
318
|
+
const escaped = escapeHtml(snippet);
|
|
319
|
+
return escaped.replace(re, '<mark>$1</mark>');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function renderSearchResults(query, payload) {
|
|
323
|
+
if (!payload || !payload.hits || payload.hits.length === 0) {
|
|
324
|
+
$searchResults.innerHTML =
|
|
325
|
+
`<p class="hint">No results for <code>${escapeHtml(query)}</code>.</p>`;
|
|
326
|
+
$searchResults.hidden = false;
|
|
327
|
+
$list.hidden = true;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const html = payload.hits
|
|
331
|
+
.map((h) => {
|
|
332
|
+
const slug = escapeHtml(h.slug || h.id);
|
|
333
|
+
const name = escapeHtml(h.name || h.slug || h.id);
|
|
334
|
+
const type = h.type ? `<span class="result-type">${escapeHtml(h.type)}</span>` : '';
|
|
335
|
+
const score = typeof h.score === 'number' ? h.score.toFixed(3) : '—';
|
|
336
|
+
const snippet = highlightMatches(h.snippet || '', query);
|
|
337
|
+
return [
|
|
338
|
+
`<a href="/docs/${slug}" data-slug="${slug}" class="result">`,
|
|
339
|
+
`<div class="result-head"><span class="result-name">${name}</span>${type}<span class="result-score">${score}</span></div>`,
|
|
340
|
+
`<div class="result-snippet">${snippet}</div>`,
|
|
341
|
+
`</a>`,
|
|
342
|
+
].join('');
|
|
343
|
+
})
|
|
344
|
+
.join('');
|
|
345
|
+
$searchResults.innerHTML = html;
|
|
346
|
+
$searchResults.hidden = false;
|
|
347
|
+
$list.hidden = true;
|
|
348
|
+
$searchResults.querySelectorAll('a.result').forEach((a) => {
|
|
349
|
+
a.addEventListener('click', (e) => {
|
|
350
|
+
e.preventDefault();
|
|
351
|
+
const slug = a.getAttribute('data-slug');
|
|
352
|
+
navigate(slug);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function clearSearchResults() {
|
|
358
|
+
$searchResults.hidden = true;
|
|
359
|
+
$searchResults.innerHTML = '';
|
|
360
|
+
$list.hidden = false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function runSearch(query) {
|
|
364
|
+
const q = query.trim();
|
|
365
|
+
if (q.length === 0) {
|
|
366
|
+
clearSearchResults();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const seq = ++state.searchSeq;
|
|
370
|
+
try {
|
|
371
|
+
const url = `/api/search?q=${encodeURIComponent(q)}&limit=20`;
|
|
372
|
+
const data = await fetchJson(url);
|
|
373
|
+
// Drop stale responses if a newer query has fired.
|
|
374
|
+
if (seq !== state.searchSeq) return;
|
|
375
|
+
renderSearchResults(q, data);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (seq !== state.searchSeq) return;
|
|
378
|
+
$searchResults.innerHTML = `<p class="error">${escapeHtml(String(err.message || err))}</p>`;
|
|
379
|
+
$searchResults.hidden = false;
|
|
380
|
+
$list.hidden = true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const debouncedSearch = debounce(runSearch, 300);
|
|
385
|
+
$search.addEventListener('input', (e) => debouncedSearch(e.target.value));
|
|
386
|
+
|
|
290
387
|
(async function init() {
|
|
291
388
|
try {
|
|
292
389
|
const data = await fetchJson('/api/docs');
|
package/dist/cli/index.js
CHANGED
|
@@ -41701,7 +41701,8 @@ import { dirname as dirname8, join as join16, normalize, resolve as resolve5 } f
|
|
|
41701
41701
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
41702
41702
|
import {
|
|
41703
41703
|
createAttachmentStore as createAttachmentStore2,
|
|
41704
|
-
getProjectRoot as getProjectRoot27
|
|
41704
|
+
getProjectRoot as getProjectRoot27,
|
|
41705
|
+
searchAllProjectDocs
|
|
41705
41706
|
} from "@cleocode/core/internal";
|
|
41706
41707
|
function getViewerAssetsDir() {
|
|
41707
41708
|
const thisFile = fileURLToPath5(import.meta.url);
|
|
@@ -41810,6 +41811,39 @@ function buildViewerHandler(opts = {}) {
|
|
|
41810
41811
|
send(res, 200, "application/json", lafsSuccessJson({ docs }));
|
|
41811
41812
|
return;
|
|
41812
41813
|
}
|
|
41814
|
+
if (pathname === "/api/search") {
|
|
41815
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
41816
|
+
const q = (url.searchParams.get("q") ?? "").trim();
|
|
41817
|
+
const limitRaw = url.searchParams.get("limit");
|
|
41818
|
+
const type2 = url.searchParams.get("type") ?? void 0;
|
|
41819
|
+
if (q.length === 0) {
|
|
41820
|
+
send(
|
|
41821
|
+
res,
|
|
41822
|
+
400,
|
|
41823
|
+
"application/json",
|
|
41824
|
+
lafsErrorJson(
|
|
41825
|
+
"E_VALIDATION",
|
|
41826
|
+
"query parameter `q` is required",
|
|
41827
|
+
"append ?q=<query> to /api/search, e.g. '/api/search?q=release'"
|
|
41828
|
+
)
|
|
41829
|
+
);
|
|
41830
|
+
return;
|
|
41831
|
+
}
|
|
41832
|
+
const limit = limitRaw ? Math.max(1, Math.min(50, Number.parseInt(limitRaw, 10))) : 10;
|
|
41833
|
+
try {
|
|
41834
|
+
const result = await searchAllProjectDocs(q, {
|
|
41835
|
+
projectRoot,
|
|
41836
|
+
limit: Number.isFinite(limit) ? limit : 10,
|
|
41837
|
+
type: type2 && type2.length > 0 ? type2 : void 0
|
|
41838
|
+
});
|
|
41839
|
+
send(res, 200, "application/json", lafsSuccessJson(result));
|
|
41840
|
+
} catch (err) {
|
|
41841
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
41842
|
+
const code = err instanceof Error && err.code ? err.code : "E_SEARCH_FAILED";
|
|
41843
|
+
send(res, 500, "application/json", lafsErrorJson(code, msg));
|
|
41844
|
+
}
|
|
41845
|
+
return;
|
|
41846
|
+
}
|
|
41813
41847
|
const apiDocMatch = pathname.match(/^\/api\/docs\/([^/]+)$/);
|
|
41814
41848
|
if (apiDocMatch) {
|
|
41815
41849
|
const slug = decodeURIComponent(apiDocMatch[1]);
|
|
@@ -41905,7 +41939,7 @@ import { spawn } from "node:child_process";
|
|
|
41905
41939
|
import { open as fsOpen } from "node:fs/promises";
|
|
41906
41940
|
import { join as join17 } from "node:path";
|
|
41907
41941
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
41908
|
-
import { getCleoHome as getCleoHome3, getProjectRoot as getProjectRoot28 } from "@cleocode/core
|
|
41942
|
+
import { getCleoHome as getCleoHome3, getProjectRoot as getProjectRoot28 } from "@cleocode/core";
|
|
41909
41943
|
function getCleoBinPath() {
|
|
41910
41944
|
const thisFile = fileURLToPath6(import.meta.url);
|
|
41911
41945
|
return join17(thisFile, "..", "..", "index.js");
|
|
@@ -42402,6 +42436,7 @@ import {
|
|
|
42402
42436
|
readJson,
|
|
42403
42437
|
recordPublication,
|
|
42404
42438
|
runDocsImport,
|
|
42439
|
+
searchAllProjectDocs as searchAllProjectDocs2,
|
|
42405
42440
|
searchDocs as searchDocs2,
|
|
42406
42441
|
statusDocs,
|
|
42407
42442
|
syncFromGit
|
|
@@ -42765,7 +42800,7 @@ var init_docs3 = __esm({
|
|
|
42765
42800
|
searchCommand = defineCommand({
|
|
42766
42801
|
meta: {
|
|
42767
42802
|
name: "search",
|
|
42768
|
-
description: "Search attachments by semantic similarity using llmtxt/similarity.rankBySimilarity.
|
|
42803
|
+
description: "Search attachments by semantic similarity using llmtxt/similarity.rankBySimilarity. Without --owner, ranks every published doc in the project by content (T9647). Pass --owner to scope to a specific entity (T###, ses_*, O-*) and rank by blob name."
|
|
42769
42804
|
},
|
|
42770
42805
|
args: {
|
|
42771
42806
|
query: {
|
|
@@ -42775,7 +42810,11 @@ var init_docs3 = __esm({
|
|
|
42775
42810
|
},
|
|
42776
42811
|
owner: {
|
|
42777
42812
|
type: "string",
|
|
42778
|
-
description: "Scope search to a specific owner entity ID"
|
|
42813
|
+
description: "Scope search to a specific owner entity ID (legacy name-only ranking)"
|
|
42814
|
+
},
|
|
42815
|
+
type: {
|
|
42816
|
+
type: "string",
|
|
42817
|
+
description: "Filter project-wide search by taxonomy type: spec|adr|research|handoff|note|llm-readme"
|
|
42779
42818
|
},
|
|
42780
42819
|
limit: {
|
|
42781
42820
|
type: "string",
|
|
@@ -42788,10 +42827,15 @@ var init_docs3 = __esm({
|
|
|
42788
42827
|
},
|
|
42789
42828
|
async run({ args }) {
|
|
42790
42829
|
const projectRoot = getProjectRoot29();
|
|
42830
|
+
const limit = args.limit ? Number.parseInt(String(args.limit), 10) : 10;
|
|
42791
42831
|
try {
|
|
42792
|
-
const result = await searchDocs2(String(args.query), {
|
|
42793
|
-
ownerId: args.owner
|
|
42794
|
-
limit
|
|
42832
|
+
const result = args.owner ? await searchDocs2(String(args.query), {
|
|
42833
|
+
ownerId: String(args.owner),
|
|
42834
|
+
limit,
|
|
42835
|
+
projectRoot
|
|
42836
|
+
}) : await searchAllProjectDocs2(String(args.query), {
|
|
42837
|
+
limit,
|
|
42838
|
+
type: args.type ? String(args.type) : void 0,
|
|
42795
42839
|
projectRoot
|
|
42796
42840
|
});
|
|
42797
42841
|
cliOutput(result, { command: "docs search", operation: "docs.search" });
|