umbrella-context 0.1.37 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/adapters/umbrella-onboarding.js +3 -2
- package/dist/commands/connect.js +3 -2
- package/dist/commands/connectors.js +3 -0
- package/dist/commands/curate.d.ts +13 -1
- package/dist/commands/curate.js +146 -10
- package/dist/commands/fix.js +2 -10
- package/dist/commands/hub.d.ts +3 -1
- package/dist/commands/hub.js +218 -68
- package/dist/commands/interactive.js +21 -10
- package/dist/commands/locations.d.ts +6 -1
- package/dist/commands/locations.js +91 -5
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/pull.d.ts +6 -1
- package/dist/commands/pull.js +119 -3
- package/dist/commands/push.d.ts +6 -1
- package/dist/commands/push.js +147 -3
- package/dist/commands/restart.js +6 -1
- package/dist/commands/search.d.ts +5 -1
- package/dist/commands/search.js +857 -36
- package/dist/commands/session.js +0 -3
- package/dist/commands/setup.js +186 -26
- package/dist/commands/space.js +4 -3
- package/dist/commands/status.d.ts +16 -1
- package/dist/commands/status.js +111 -47
- package/dist/commands/tui.js +339 -107
- package/dist/config.d.ts +4 -0
- package/dist/config.js +6 -0
- package/dist/index.js +21 -3
- package/dist/repo-state.d.ts +115 -0
- package/dist/repo-state.js +195 -12
- package/package.json +2 -2
package/dist/commands/search.js
CHANGED
|
@@ -1,15 +1,794 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { configManager } from "../config.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { getProviderReadinessSummary } from "./providers.js";
|
|
4
|
+
import { addTaskReasoningContent, addTaskToolCall, appendTaskStreamingContent, buildLocalQueryFingerprint, completeTask, createTask, getQueryCacheEntry, getPendingMemories, getPulledMemories, markQueryCacheHit, patchTaskLifecycle, removeTask, recordSessionEvent, recordSessionQuery, setSessionPanel, storeQueryCacheEntry, summarizeLocalMemoryMatches, } from "../repo-state.js";
|
|
5
|
+
const DIRECT_RESPONSE_SCORE_THRESHOLD = 0.85;
|
|
6
|
+
const DIRECT_RESPONSE_HIGH_CONFIDENCE_THRESHOLD = 0.93;
|
|
7
|
+
const DIRECT_RESPONSE_MIN_GAP = 0.08;
|
|
8
|
+
const SYNTHESIS_CONTEXT_LIMIT = 6;
|
|
9
|
+
function printJson(payload) {
|
|
10
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
11
|
+
}
|
|
12
|
+
function trimMatchingQuotes(value) {
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
15
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
16
|
+
return trimmed.slice(1, -1).trim();
|
|
17
|
+
}
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
function normalizeQuery(query) {
|
|
21
|
+
return trimMatchingQuotes(query).toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
function stripQuestionPunctuation(query) {
|
|
24
|
+
return trimMatchingQuotes(query).replace(/[?!.]+$/g, "");
|
|
25
|
+
}
|
|
26
|
+
function extractTopicHint(query) {
|
|
27
|
+
const cleaned = stripQuestionPunctuation(query).trim();
|
|
28
|
+
if (!cleaned)
|
|
29
|
+
return "the topic";
|
|
30
|
+
const questionPrefixes = [
|
|
31
|
+
/^how is /i,
|
|
32
|
+
/^how are /i,
|
|
33
|
+
/^how does /i,
|
|
34
|
+
/^how do /i,
|
|
35
|
+
/^what is /i,
|
|
36
|
+
/^what are /i,
|
|
37
|
+
/^where is /i,
|
|
38
|
+
/^where are /i,
|
|
39
|
+
/^why is /i,
|
|
40
|
+
/^why are /i,
|
|
41
|
+
];
|
|
42
|
+
for (const prefix of questionPrefixes) {
|
|
43
|
+
if (prefix.test(cleaned)) {
|
|
44
|
+
return cleaned.replace(prefix, "").trim() || "the topic";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return cleaned;
|
|
48
|
+
}
|
|
49
|
+
function isVagueQuery(query) {
|
|
50
|
+
const normalized = normalizeQuery(query);
|
|
51
|
+
if (!normalized)
|
|
52
|
+
return true;
|
|
53
|
+
const words = normalized.split(/\s+/).filter(Boolean);
|
|
54
|
+
if (words.length <= 1)
|
|
55
|
+
return true;
|
|
56
|
+
const vaguePatterns = new Set([
|
|
57
|
+
"auth",
|
|
58
|
+
"authentication",
|
|
59
|
+
"rate limiting",
|
|
60
|
+
"rate limit",
|
|
61
|
+
"rate limits",
|
|
62
|
+
"show me code",
|
|
63
|
+
"bug",
|
|
64
|
+
"error",
|
|
65
|
+
"setup",
|
|
66
|
+
]);
|
|
67
|
+
return vaguePatterns.has(normalized);
|
|
68
|
+
}
|
|
69
|
+
function extractMeaningfulQueryWords(query) {
|
|
70
|
+
return normalizeQuery(query)
|
|
71
|
+
.replace(/[?!.:,;()[\]{}]/g, " ")
|
|
72
|
+
.split(/\s+/)
|
|
73
|
+
.map((word) => word.trim())
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.filter((word) => !new Set([
|
|
76
|
+
"a",
|
|
77
|
+
"an",
|
|
78
|
+
"and",
|
|
79
|
+
"are",
|
|
80
|
+
"do",
|
|
81
|
+
"does",
|
|
82
|
+
"how",
|
|
83
|
+
"implemented",
|
|
84
|
+
"is",
|
|
85
|
+
"the",
|
|
86
|
+
"what",
|
|
87
|
+
"where",
|
|
88
|
+
"why",
|
|
89
|
+
]).has(word));
|
|
90
|
+
}
|
|
91
|
+
function expandQueryWord(word) {
|
|
92
|
+
const synonymMap = {
|
|
93
|
+
auth: ["authentication", "signin", "sign-in", "jwt", "token", "cookie", "cookies"],
|
|
94
|
+
authentication: ["auth", "signin", "sign-in", "jwt", "token", "cookie", "cookies"],
|
|
95
|
+
rate: ["throttle", "limit", "limits", "limiting"],
|
|
96
|
+
limits: ["limit", "rate", "throttle", "limiting"],
|
|
97
|
+
};
|
|
98
|
+
return [word, ...(synonymMap[word] ?? [])];
|
|
99
|
+
}
|
|
100
|
+
function buildQueryCoverage(content, query) {
|
|
101
|
+
const queryWords = extractMeaningfulQueryWords(query);
|
|
102
|
+
if (queryWords.length === 0) {
|
|
103
|
+
return { ratio: 0, matchedWords: [], missingWords: [] };
|
|
104
|
+
}
|
|
105
|
+
const haystack = normalizeComparableContent(content);
|
|
106
|
+
const matchedWords = queryWords.filter((word) => {
|
|
107
|
+
const variants = expandQueryWord(word);
|
|
108
|
+
return variants.some((variant) => haystack.includes(normalizeComparableContent(variant)));
|
|
109
|
+
});
|
|
110
|
+
const uniqueMatchedWords = [...new Set(matchedWords)];
|
|
111
|
+
const missingWords = queryWords.filter((word) => !uniqueMatchedWords.includes(word));
|
|
112
|
+
return {
|
|
113
|
+
ratio: uniqueMatchedWords.length / queryWords.length,
|
|
114
|
+
matchedWords: uniqueMatchedWords,
|
|
115
|
+
missingWords,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function scoreLocalMatch(entry, query) {
|
|
119
|
+
const normalizedQuery = normalizeQuery(query);
|
|
120
|
+
const haystack = [entry.content, ...entry.tags, ...(entry.keywords ?? [])].join(" ").toLowerCase();
|
|
121
|
+
let score = 0;
|
|
122
|
+
if (haystack.includes(normalizedQuery))
|
|
123
|
+
score += 4;
|
|
124
|
+
const queryWords = extractMeaningfulQueryWords(query);
|
|
125
|
+
for (const word of queryWords) {
|
|
126
|
+
const variants = expandQueryWord(word);
|
|
127
|
+
if (variants.some((variant) => haystack.includes(variant))) {
|
|
128
|
+
score += 3;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return score;
|
|
132
|
+
}
|
|
133
|
+
function rankLocalMatches(entries, query) {
|
|
134
|
+
return [...entries]
|
|
135
|
+
.map((entry) => ({ entry, score: scoreLocalMatch(entry, query) }))
|
|
136
|
+
.sort((a, b) => b.score - a.score || b.entry.createdAt.localeCompare(a.entry.createdAt))
|
|
137
|
+
.map(({ entry, score }) => ({ entry, score }));
|
|
138
|
+
}
|
|
139
|
+
function buildHelpfulSuggestions(query) {
|
|
140
|
+
const normalized = normalizeQuery(query);
|
|
141
|
+
if (!normalized) {
|
|
142
|
+
return [
|
|
143
|
+
'Try a full question like "How is authentication implemented?"',
|
|
144
|
+
'Try something specific like "What rate limits do we enforce and where?"',
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
const words = normalized.split(/\s+/).filter(Boolean);
|
|
148
|
+
const alreadySpecific = words.length >= 4 && /[?]/.test(query);
|
|
149
|
+
if (alreadySpecific) {
|
|
150
|
+
return [
|
|
151
|
+
"Try pulling the latest shared context with `umbrella-context pull` and then query again.",
|
|
152
|
+
"If the answer only exists in your working tree, save it locally with `umbrella-context curate \"...\"` and then run `umbrella-context push`.",
|
|
153
|
+
"Try a nearby question that names a file, module, or subsystem you expect to contain the answer.",
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
if (normalized === "rate limits" || normalized === "rate limit" || normalized === "rate limiting") {
|
|
157
|
+
return [
|
|
158
|
+
'Try a fuller question, for example: "What are the API rate limits and where are they enforced?"',
|
|
159
|
+
'Try something implementation-focused, for example: "How is rate limiting implemented?"',
|
|
160
|
+
'If you already learned the answer locally, save it with `umbrella-context curate "..."` and then run `umbrella-context push`.',
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
if (normalized === "auth" || normalized === "authentication") {
|
|
164
|
+
return [
|
|
165
|
+
'Try a fuller question, for example: "How is authentication implemented?"',
|
|
166
|
+
'Try a configuration question, for example: "Where is authentication configured and enforced?"',
|
|
167
|
+
'If you already learned the answer locally, save it with `umbrella-context curate "..."` and then run `umbrella-context push`.',
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
const topic = extractTopicHint(query);
|
|
171
|
+
return [
|
|
172
|
+
`Try a fuller question, for example: "How is ${topic} implemented?"`,
|
|
173
|
+
`Try adding what you need, for example: "Where is ${topic} configured and enforced?"`,
|
|
174
|
+
'If you already learned the answer locally, save it with `umbrella-context curate "..."` and then run `umbrella-context push`.',
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
function normalizeLocalScore(score, query) {
|
|
178
|
+
const wordCount = normalizeQuery(query).split(/\s+/).filter(Boolean).length || 1;
|
|
179
|
+
const maxReasonableScore = Math.max(5, wordCount * 4);
|
|
180
|
+
return Math.min(score / maxReasonableScore, 1);
|
|
181
|
+
}
|
|
182
|
+
function normalizeServerScore(rawScore) {
|
|
183
|
+
if (typeof rawScore !== "number" || Number.isNaN(rawScore))
|
|
184
|
+
return 0;
|
|
185
|
+
return rawScore <= 1 ? rawScore : Math.min(rawScore / 10, 1);
|
|
186
|
+
}
|
|
187
|
+
function truncateSentence(value, maxLength = 260) {
|
|
188
|
+
const cleaned = value.replace(/\s+/g, " ").trim();
|
|
189
|
+
if (cleaned.length <= maxLength)
|
|
190
|
+
return cleaned;
|
|
191
|
+
return `${cleaned.slice(0, maxLength).trim()}...`;
|
|
192
|
+
}
|
|
193
|
+
function compactTimestamp(value) {
|
|
194
|
+
return new Date(value).toLocaleDateString();
|
|
195
|
+
}
|
|
196
|
+
function normalizeComparableContent(value) {
|
|
197
|
+
return value
|
|
198
|
+
.toLowerCase()
|
|
199
|
+
.replace(/[^\w\s]/g, " ")
|
|
200
|
+
.replace(/\s+/g, " ")
|
|
201
|
+
.trim();
|
|
202
|
+
}
|
|
203
|
+
function areNearDuplicateCandidates(a, b) {
|
|
204
|
+
const left = normalizeComparableContent(a.content);
|
|
205
|
+
const right = normalizeComparableContent(b.content);
|
|
206
|
+
if (!left || !right)
|
|
207
|
+
return false;
|
|
208
|
+
return left.includes(right) || right.includes(left);
|
|
209
|
+
}
|
|
210
|
+
function shareLocalSignals(left, right) {
|
|
211
|
+
const leftSignals = new Set([...(left.tags ?? []), ...(left.keywords ?? [])]
|
|
212
|
+
.map((value) => value.trim().toLowerCase())
|
|
213
|
+
.filter(Boolean));
|
|
214
|
+
const rightSignals = new Set([...(right.tags ?? []), ...(right.keywords ?? [])]
|
|
215
|
+
.map((value) => value.trim().toLowerCase())
|
|
216
|
+
.filter(Boolean));
|
|
217
|
+
let overlap = 0;
|
|
218
|
+
for (const signal of leftSignals) {
|
|
219
|
+
if (rightSignals.has(signal)) {
|
|
220
|
+
overlap += 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return overlap >= 1;
|
|
224
|
+
}
|
|
225
|
+
function buildDirectCandidates(query, localMatches, serverMatches) {
|
|
226
|
+
const localCandidates = localMatches.map(({ entry, score }) => ({
|
|
227
|
+
content: entry.content,
|
|
228
|
+
label: "local memory",
|
|
229
|
+
score: normalizeLocalScore(score, query),
|
|
230
|
+
source: entry.source ?? "local",
|
|
231
|
+
createdAt: entry.createdAt,
|
|
232
|
+
}));
|
|
233
|
+
const sharedCandidates = serverMatches.map((entry) => ({
|
|
234
|
+
content: typeof entry.content === "string" ? entry.content : "",
|
|
235
|
+
label: typeof entry.systemType === "string" ? entry.systemType : "shared memory",
|
|
236
|
+
score: normalizeServerScore(entry.score),
|
|
237
|
+
source: typeof entry.source === "string" ? entry.source : "server",
|
|
238
|
+
createdAt: typeof entry.createdAt === "string" ? entry.createdAt : new Date().toISOString(),
|
|
239
|
+
}));
|
|
240
|
+
return [...localCandidates, ...sharedCandidates]
|
|
241
|
+
.filter((entry) => entry.content.trim())
|
|
242
|
+
.sort((a, b) => b.score - a.score || b.createdAt.localeCompare(a.createdAt));
|
|
243
|
+
}
|
|
244
|
+
function hasStrongLocalDirectAnswer(localMatches) {
|
|
245
|
+
if (localMatches.length === 0)
|
|
246
|
+
return false;
|
|
247
|
+
const top = localMatches[0];
|
|
248
|
+
const second = localMatches[1];
|
|
249
|
+
if (top.score >= 3 && !second)
|
|
250
|
+
return true;
|
|
251
|
+
if (top.score >= 3 && second && areNearDuplicateCandidates({
|
|
252
|
+
content: top.entry.content,
|
|
253
|
+
label: "local memory",
|
|
254
|
+
score: top.score,
|
|
255
|
+
source: top.entry.source ?? "local",
|
|
256
|
+
createdAt: top.entry.createdAt,
|
|
257
|
+
}, {
|
|
258
|
+
content: second.entry.content,
|
|
259
|
+
label: "local memory",
|
|
260
|
+
score: second.score,
|
|
261
|
+
source: second.entry.source ?? "local",
|
|
262
|
+
createdAt: second.entry.createdAt,
|
|
263
|
+
})) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
if (top.score >= 3 && second && shareLocalSignals(top.entry, second.entry)) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
if (top.score >= 3 && second && top.score - second.score >= 1)
|
|
270
|
+
return true;
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
function buildDirectAnswer(query, localMatches, serverMatches) {
|
|
274
|
+
const candidates = buildDirectCandidates(query, localMatches, serverMatches);
|
|
275
|
+
if (candidates.length === 0)
|
|
276
|
+
return null;
|
|
277
|
+
const top = candidates[0];
|
|
278
|
+
const allowStrongLocalFallback = top.label === "local memory" && hasStrongLocalDirectAnswer(localMatches);
|
|
279
|
+
if (top.score < DIRECT_RESPONSE_SCORE_THRESHOLD && !allowStrongLocalFallback)
|
|
280
|
+
return null;
|
|
281
|
+
const second = candidates[1];
|
|
282
|
+
const dominant = allowStrongLocalFallback ||
|
|
283
|
+
!second ||
|
|
284
|
+
top.score >= DIRECT_RESPONSE_HIGH_CONFIDENCE_THRESHOLD ||
|
|
285
|
+
top.score - second.score >= DIRECT_RESPONSE_MIN_GAP ||
|
|
286
|
+
(areNearDuplicateCandidates(top, second) && top.content.length >= second.content.length);
|
|
287
|
+
if (!dominant)
|
|
288
|
+
return null;
|
|
289
|
+
const coverage = buildQueryCoverage(top.content, query);
|
|
290
|
+
const requiresBroaderCoverage = extractMeaningfulQueryWords(query).length >= 3;
|
|
291
|
+
const coverageThreshold = requiresBroaderCoverage ? 0.6 : 0.5;
|
|
292
|
+
if (coverage.ratio < coverageThreshold) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
summary: `Based on the strongest saved context, here is the best direct answer for "${query}".`,
|
|
297
|
+
details: truncateSentence(top.content),
|
|
298
|
+
sources: [`${top.source} (${top.label})`],
|
|
299
|
+
confidence: top.score >= DIRECT_RESPONSE_HIGH_CONFIDENCE_THRESHOLD ? "high" : "medium",
|
|
300
|
+
gaps: top.score >= DIRECT_RESPONSE_HIGH_CONFIDENCE_THRESHOLD
|
|
301
|
+
? "No major gaps were detected in the strongest saved context."
|
|
302
|
+
: "The saved context is good, but it may still be missing implementation details or edge cases.",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function buildSynthesisContext(localMatches, serverMatches) {
|
|
306
|
+
return [
|
|
307
|
+
...localMatches.slice(0, 3).map((entry, index) => ({
|
|
308
|
+
index: index + 1,
|
|
309
|
+
source: `local:${entry.source ?? "cli"}:${compactTimestamp(entry.createdAt)}`,
|
|
310
|
+
content: entry.content,
|
|
311
|
+
tags: entry.tags ?? [],
|
|
312
|
+
keywords: entry.keywords ?? [],
|
|
313
|
+
})),
|
|
314
|
+
...serverMatches.slice(0, 3).map((entry, index) => ({
|
|
315
|
+
index: localMatches.slice(0, 3).length + index + 1,
|
|
316
|
+
source: `shared:${typeof entry.source === "string" ? entry.source : "server"}:${typeof entry.createdAt === "string" ? compactTimestamp(entry.createdAt) : "unknown"}`,
|
|
317
|
+
content: typeof entry.content === "string" ? entry.content : "",
|
|
318
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
319
|
+
keywords: Array.isArray(entry.keywords) ? entry.keywords : [],
|
|
320
|
+
})),
|
|
321
|
+
]
|
|
322
|
+
.filter((entry) => entry.content.trim())
|
|
323
|
+
.slice(0, SYNTHESIS_CONTEXT_LIMIT);
|
|
324
|
+
}
|
|
325
|
+
function buildOpenAiStyleUrl(baseUrl, fallback) {
|
|
326
|
+
const trimmed = (baseUrl ?? fallback).replace(/\/+$/, "");
|
|
327
|
+
if (trimmed.endsWith("/v1")) {
|
|
328
|
+
return `${trimmed}/chat/completions`;
|
|
329
|
+
}
|
|
330
|
+
if (trimmed.endsWith("/chat/completions")) {
|
|
331
|
+
return trimmed;
|
|
332
|
+
}
|
|
333
|
+
return `${trimmed}/v1/chat/completions`;
|
|
334
|
+
}
|
|
335
|
+
function extractOpenAiStyleText(data) {
|
|
336
|
+
return (data?.choices?.[0]?.message?.content
|
|
337
|
+
?? data?.choices?.[0]?.text
|
|
338
|
+
?? null);
|
|
339
|
+
}
|
|
340
|
+
function extractAnthropicText(data) {
|
|
341
|
+
const blocks = Array.isArray(data?.content)
|
|
342
|
+
? data.content
|
|
343
|
+
: [];
|
|
344
|
+
return blocks
|
|
345
|
+
.filter((block) => block?.type === "text" && typeof block?.text === "string")
|
|
346
|
+
.map((block) => block.text)
|
|
347
|
+
.join("\n")
|
|
348
|
+
.trim();
|
|
349
|
+
}
|
|
350
|
+
function extractGoogleText(data) {
|
|
351
|
+
const parts = data?.candidates?.[0]?.content?.parts;
|
|
352
|
+
if (!Array.isArray(parts))
|
|
353
|
+
return null;
|
|
354
|
+
return parts
|
|
355
|
+
.map((part) => (typeof part?.text === "string" ? part.text : ""))
|
|
356
|
+
.join("\n")
|
|
357
|
+
.trim();
|
|
358
|
+
}
|
|
359
|
+
function inferAnswerGaps(details, query, fallback) {
|
|
360
|
+
const normalized = details.toLowerCase();
|
|
361
|
+
if (normalized.includes("not enough") ||
|
|
362
|
+
normalized.includes("missing") ||
|
|
363
|
+
normalized.includes("unclear") ||
|
|
364
|
+
normalized.includes("incomplete") ||
|
|
365
|
+
normalized.includes("do not know") ||
|
|
366
|
+
normalized.includes("unknown")) {
|
|
367
|
+
return truncateSentence(details, 220);
|
|
368
|
+
}
|
|
369
|
+
const meaningfulWords = extractMeaningfulQueryWords(query);
|
|
370
|
+
if (meaningfulWords.length >= 4) {
|
|
371
|
+
return fallback;
|
|
372
|
+
}
|
|
373
|
+
return "No major gaps were detected in the saved context used for this answer.";
|
|
374
|
+
}
|
|
375
|
+
function parseStructuredProviderAnswer(rawText) {
|
|
376
|
+
const trimmed = rawText.trim();
|
|
377
|
+
if (!trimmed)
|
|
378
|
+
return null;
|
|
379
|
+
try {
|
|
380
|
+
const parsed = JSON.parse(trimmed);
|
|
381
|
+
if (typeof parsed.summary !== "string" || typeof parsed.details !== "string") {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
summary: truncateSentence(parsed.summary.trim(), 240),
|
|
386
|
+
details: truncateSentence(parsed.details.trim(), 420),
|
|
387
|
+
sources: Array.isArray(parsed.sources)
|
|
388
|
+
? parsed.sources.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
389
|
+
: [],
|
|
390
|
+
gaps: typeof parsed.gaps === "string" && parsed.gaps.trim().length > 0
|
|
391
|
+
? truncateSentence(parsed.gaps.trim(), 240)
|
|
392
|
+
: "No major gaps were detected in the saved context used for this answer.",
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function synthesizeAnswerFromProvider(query, localMatches, serverMatches) {
|
|
400
|
+
const config = configManager.config;
|
|
401
|
+
if (!config?.activeProvider || !config.activeModel)
|
|
402
|
+
return null;
|
|
403
|
+
const provider = configManager.providers.find((entry) => entry.id === config.activeProvider) ?? null;
|
|
404
|
+
if (!provider?.apiKey)
|
|
405
|
+
return null;
|
|
406
|
+
const contextEntries = buildSynthesisContext(localMatches, serverMatches);
|
|
407
|
+
if (contextEntries.length === 0)
|
|
408
|
+
return null;
|
|
409
|
+
const userPrompt = [
|
|
410
|
+
`Question: ${query}`,
|
|
411
|
+
"",
|
|
412
|
+
"Saved context:",
|
|
413
|
+
...contextEntries.map((entry) => `[${entry.index}] ${entry.source}\nContent: ${entry.content}\nTags: ${entry.tags.join(", ") || "none"}\nKeywords: ${entry.keywords.join(", ") || "none"}`),
|
|
414
|
+
"",
|
|
415
|
+
"Answer using only the saved context above.",
|
|
416
|
+
"If the context is related but still incomplete, say exactly what is known and what is still missing.",
|
|
417
|
+
"Return only JSON with this exact shape:",
|
|
418
|
+
'{ "summary": "short answer", "details": "2 short paragraphs max", "sources": ["source label"], "gaps": "what is still missing" }',
|
|
419
|
+
"Do not add markdown fences or extra commentary.",
|
|
420
|
+
].join("\n");
|
|
421
|
+
let responseText = null;
|
|
422
|
+
if (provider.kind === "anthropic") {
|
|
423
|
+
const res = await fetch(provider.baseUrl?.replace(/\/+$/, "") || "https://api.anthropic.com/v1/messages", {
|
|
424
|
+
method: "POST",
|
|
425
|
+
headers: {
|
|
426
|
+
"content-type": "application/json",
|
|
427
|
+
"x-api-key": provider.apiKey,
|
|
428
|
+
"anthropic-version": "2023-06-01",
|
|
429
|
+
},
|
|
430
|
+
body: JSON.stringify({
|
|
431
|
+
model: config.activeModel,
|
|
432
|
+
max_tokens: 220,
|
|
433
|
+
system: "You answer engineering questions using only the provided saved context. Be concise and do not invent facts.",
|
|
434
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
435
|
+
}),
|
|
436
|
+
});
|
|
437
|
+
if (!res.ok)
|
|
438
|
+
return null;
|
|
439
|
+
responseText = extractAnthropicText(await res.json());
|
|
440
|
+
}
|
|
441
|
+
else if (provider.kind === "google") {
|
|
442
|
+
const model = encodeURIComponent(config.activeModel);
|
|
443
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(provider.apiKey)}`, {
|
|
444
|
+
method: "POST",
|
|
445
|
+
headers: { "content-type": "application/json" },
|
|
446
|
+
body: JSON.stringify({
|
|
447
|
+
contents: [{ role: "user", parts: [{ text: userPrompt }] }],
|
|
448
|
+
systemInstruction: {
|
|
449
|
+
parts: [
|
|
450
|
+
{
|
|
451
|
+
text: "You answer engineering questions using only the provided saved context. Be concise and do not invent facts.",
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
generationConfig: {
|
|
456
|
+
temperature: 0.2,
|
|
457
|
+
maxOutputTokens: 220,
|
|
458
|
+
},
|
|
459
|
+
}),
|
|
460
|
+
});
|
|
461
|
+
if (!res.ok)
|
|
462
|
+
return null;
|
|
463
|
+
responseText = extractGoogleText(await res.json());
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
const endpoint = buildOpenAiStyleUrl(provider.baseUrl, provider.kind === "openrouter" ? "https://openrouter.ai/api" : "https://api.openai.com");
|
|
467
|
+
const headers = {
|
|
468
|
+
"content-type": "application/json",
|
|
469
|
+
authorization: `Bearer ${provider.apiKey}`,
|
|
470
|
+
};
|
|
471
|
+
if (provider.kind === "openrouter") {
|
|
472
|
+
headers["HTTP-Referer"] = "https://github.com/buildingwithai/Umbrella";
|
|
473
|
+
headers["X-Title"] = "Umbrella Context CLI";
|
|
474
|
+
}
|
|
475
|
+
const res = await fetch(endpoint, {
|
|
476
|
+
method: "POST",
|
|
477
|
+
headers,
|
|
478
|
+
body: JSON.stringify({
|
|
479
|
+
model: config.activeModel,
|
|
480
|
+
temperature: 0.2,
|
|
481
|
+
max_tokens: 220,
|
|
482
|
+
messages: [
|
|
483
|
+
{
|
|
484
|
+
role: "system",
|
|
485
|
+
content: "You answer engineering questions using only the provided saved context. Be concise and do not invent facts.",
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
role: "user",
|
|
489
|
+
content: userPrompt,
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
if (!res.ok)
|
|
495
|
+
return null;
|
|
496
|
+
responseText = extractOpenAiStyleText(await res.json());
|
|
497
|
+
}
|
|
498
|
+
const responseTextValue = responseText ?? "";
|
|
499
|
+
const structured = parseStructuredProviderAnswer(responseTextValue);
|
|
500
|
+
const details = truncateSentence(responseTextValue, 400);
|
|
501
|
+
if (!structured && !details)
|
|
502
|
+
return null;
|
|
503
|
+
return {
|
|
504
|
+
summary: structured?.summary
|
|
505
|
+
?? `This answer was synthesized from the strongest saved context for "${query}".`,
|
|
506
|
+
details: structured?.details ?? details,
|
|
507
|
+
sources: structured?.sources.length
|
|
508
|
+
? structured.sources
|
|
509
|
+
: contextEntries.map((entry) => entry.source),
|
|
510
|
+
confidence: "medium",
|
|
511
|
+
gaps: structured?.gaps
|
|
512
|
+
?? inferAnswerGaps(details, query, "The saved context is related, but it may still be missing exact implementation details or the final policy decision."),
|
|
513
|
+
provider: provider.name,
|
|
514
|
+
model: config.activeModel,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function printAnswerSections(answer, opts = {}) {
|
|
518
|
+
if (opts.heading) {
|
|
519
|
+
console.log(chalk.cyan(` ${opts.heading}:\n`));
|
|
520
|
+
}
|
|
521
|
+
console.log(chalk.cyan(" Summary:\n"));
|
|
522
|
+
console.log(` ${answer.summary}`);
|
|
523
|
+
console.log("");
|
|
524
|
+
console.log(chalk.cyan(" Details:\n"));
|
|
525
|
+
console.log(` ${answer.details}`);
|
|
526
|
+
console.log("");
|
|
527
|
+
console.log(chalk.cyan(" Sources:\n"));
|
|
528
|
+
if (answer.sources.length === 0) {
|
|
529
|
+
console.log(" None");
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
answer.sources.forEach((source, index) => {
|
|
533
|
+
console.log(chalk.gray(` ${index + 1}. ${source}`));
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
console.log("");
|
|
537
|
+
console.log(chalk.cyan(" Gaps:\n"));
|
|
538
|
+
console.log(` ${answer.gaps ?? "No major gaps were detected in the saved context used for this answer."}`);
|
|
539
|
+
console.log("");
|
|
540
|
+
if (answer.provider || answer.model) {
|
|
541
|
+
console.log(chalk.gray(` Confidence: ${answer.confidence}${answer.provider ? ` | Provider: ${answer.provider}` : ""}${answer.model ? ` | Model: ${answer.model}` : ""}`));
|
|
542
|
+
console.log("");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
console.log(chalk.gray(` Confidence: ${answer.confidence}`));
|
|
546
|
+
console.log("");
|
|
547
|
+
}
|
|
548
|
+
function printMatchMeta(result) {
|
|
549
|
+
const meta = [];
|
|
550
|
+
if ("accessLevel" in result && result.accessLevel) {
|
|
551
|
+
meta.push(`Access: ${result.accessLevel}`);
|
|
552
|
+
}
|
|
553
|
+
if ("category" in result && result.category) {
|
|
554
|
+
meta.push(`Category: ${result.category}`);
|
|
555
|
+
}
|
|
556
|
+
if ("tags" in result && Array.isArray(result.tags) && result.tags.length > 0) {
|
|
557
|
+
meta.push(`Tags: ${result.tags.slice(0, 4).join(", ")}`);
|
|
558
|
+
}
|
|
559
|
+
if ("keywords" in result && Array.isArray(result.keywords) && result.keywords.length > 0) {
|
|
560
|
+
meta.push(`Keywords: ${result.keywords.slice(0, 4).join(", ")}`);
|
|
561
|
+
}
|
|
562
|
+
return meta;
|
|
563
|
+
}
|
|
564
|
+
function printSupportingMatches(localMatches, serverMatches) {
|
|
565
|
+
const support = [
|
|
566
|
+
...localMatches.slice(0, 2).map((result) => ({
|
|
567
|
+
kind: "local",
|
|
568
|
+
content: result.content,
|
|
569
|
+
source: `Saved ${compactTimestamp(result.createdAt)}`,
|
|
570
|
+
meta: printMatchMeta(result),
|
|
571
|
+
})),
|
|
572
|
+
...serverMatches.slice(0, 2).map((result) => ({
|
|
573
|
+
kind: typeof result.systemType === "string" ? result.systemType : "shared",
|
|
574
|
+
content: typeof result.content === "string" ? result.content : "",
|
|
575
|
+
source: typeof result.createdAt === "string"
|
|
576
|
+
? `Shared ${compactTimestamp(result.createdAt)}`
|
|
577
|
+
: "Shared memory",
|
|
578
|
+
meta: printMatchMeta(result),
|
|
579
|
+
})),
|
|
580
|
+
].filter((result) => result.content.trim());
|
|
581
|
+
if (support.length === 0)
|
|
582
|
+
return;
|
|
583
|
+
console.log(chalk.cyan(" Supporting context:\n"));
|
|
584
|
+
support.forEach((result, index) => {
|
|
585
|
+
console.log(chalk.cyan(` ${index + 1}. [${result.kind}] ${truncateSentence(result.content, 140)}`));
|
|
586
|
+
console.log(chalk.gray(` ${result.source}`));
|
|
587
|
+
if (result.meta.length > 0) {
|
|
588
|
+
console.log(chalk.gray(` ${result.meta.join(" | ")}`));
|
|
589
|
+
}
|
|
590
|
+
console.log("");
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
function printPartialMatches(localMatches, serverMatches) {
|
|
594
|
+
const support = [
|
|
595
|
+
...localMatches.slice(0, 3).map((result) => ({
|
|
596
|
+
kind: "local",
|
|
597
|
+
content: result.content,
|
|
598
|
+
source: `Saved ${compactTimestamp(result.createdAt)}`,
|
|
599
|
+
meta: printMatchMeta(result),
|
|
600
|
+
})),
|
|
601
|
+
...serverMatches.slice(0, 3).map((result) => ({
|
|
602
|
+
kind: typeof result.systemType === "string" ? result.systemType : "shared",
|
|
603
|
+
content: typeof result.content === "string" ? result.content : "",
|
|
604
|
+
source: typeof result.createdAt === "string"
|
|
605
|
+
? `Shared ${compactTimestamp(result.createdAt)}`
|
|
606
|
+
: "Shared memory",
|
|
607
|
+
meta: printMatchMeta(result),
|
|
608
|
+
})),
|
|
609
|
+
].filter((result) => result.content.trim());
|
|
610
|
+
if (support.length === 0)
|
|
611
|
+
return;
|
|
612
|
+
console.log(chalk.cyan(" Top context:\n"));
|
|
613
|
+
support.forEach((result, index) => {
|
|
614
|
+
console.log(chalk.cyan(` ${index + 1}. [${result.kind}] ${truncateSentence(result.content, 140)}`));
|
|
615
|
+
console.log(chalk.gray(` ${result.source}`));
|
|
616
|
+
if (result.meta.length > 0) {
|
|
617
|
+
console.log(chalk.gray(` ${result.meta.join(" | ")}`));
|
|
618
|
+
}
|
|
619
|
+
console.log("");
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function printTextResults(payload, cached = false) {
|
|
623
|
+
if (payload.total === 0) {
|
|
624
|
+
console.log(chalk.bold(`\n Context answer for ${payload.companyName} / ${payload.spaceName}\n`));
|
|
625
|
+
console.log(chalk.cyan(" Summary:\n"));
|
|
626
|
+
console.log(` ${cached ? "No cached context matched this question yet." : "No matching context was found for this question yet."}`);
|
|
627
|
+
console.log("");
|
|
628
|
+
console.log(chalk.cyan(" Details:\n"));
|
|
629
|
+
console.log(` I checked both your local .um workspace and the shared server memory for "${payload.query}", but neither one had a strong saved answer.`);
|
|
630
|
+
console.log("");
|
|
631
|
+
console.log(chalk.cyan(" Sources:\n"));
|
|
632
|
+
console.log(" None");
|
|
633
|
+
console.log("");
|
|
634
|
+
console.log(chalk.cyan(" Next step:\n"));
|
|
635
|
+
payload.suggestions.forEach((suggestion, index) => {
|
|
636
|
+
console.log(chalk.gray(` ${index + 1}. ${suggestion}`));
|
|
637
|
+
});
|
|
638
|
+
console.log("");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const cacheBanner = cached ? chalk.gray(" (cached)") : "";
|
|
642
|
+
console.log(chalk.bold(`\n Context answer for ${payload.companyName} / ${payload.spaceName}${cacheBanner}\n`));
|
|
643
|
+
if (payload.directAnswer) {
|
|
644
|
+
printAnswerSections(payload.directAnswer, { heading: "Direct answer" });
|
|
645
|
+
printSupportingMatches(payload.localMatches, payload.serverMatches);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (payload.synthesizedAnswer) {
|
|
649
|
+
printAnswerSections(payload.synthesizedAnswer, { heading: "Synthesized answer" });
|
|
650
|
+
printPartialMatches(payload.localMatches, payload.serverMatches);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
console.log(chalk.cyan(" Summary:\n"));
|
|
654
|
+
console.log(` I found ${payload.total} related context entr${payload.total === 1 ? "y" : "ies"}, but there is not one strong enough match to give a single direct answer yet.`);
|
|
655
|
+
console.log("");
|
|
656
|
+
console.log(chalk.cyan(" Details:\n"));
|
|
657
|
+
console.log(` There ${payload.total === 1 ? "is" : "are"} ${payload.localMatches.length} local and ${payload.serverMatches.length} shared match${payload.total === 1 ? "" : "es"} for "${payload.query}". The best next move is to review the strongest saved context below or ask a more specific question.`);
|
|
658
|
+
console.log("");
|
|
659
|
+
printPartialMatches(payload.localMatches, payload.serverMatches);
|
|
660
|
+
console.log(chalk.cyan(" Sources:\n"));
|
|
661
|
+
console.log(` Local .um: ${payload.localMatches.length} | Shared server memory: ${payload.serverMatches.length}`);
|
|
662
|
+
console.log("");
|
|
663
|
+
console.log(chalk.cyan(" Next step:\n"));
|
|
664
|
+
buildHelpfulSuggestions(payload.query).forEach((suggestion, index) => {
|
|
665
|
+
console.log(chalk.gray(` ${index + 1}. ${suggestion}`));
|
|
666
|
+
});
|
|
667
|
+
if (payload.providerRequiredForSynthesis) {
|
|
668
|
+
console.log(chalk.yellow(`\n Provider note: ${payload.providerReady
|
|
669
|
+
? "A provider is connected, so this question is ready for a richer synthesized answer in a later pass."
|
|
670
|
+
: 'Connect a provider with "umbrella-context providers setup" if you want the CLI to turn these partial matches into one cleaner answer.'}`));
|
|
671
|
+
}
|
|
672
|
+
console.log("");
|
|
673
|
+
}
|
|
674
|
+
function printSearchPayload(format, payload, cached = false) {
|
|
675
|
+
if (format === "json") {
|
|
676
|
+
printJson({
|
|
677
|
+
ok: true,
|
|
678
|
+
action: "query",
|
|
679
|
+
cached,
|
|
680
|
+
...payload,
|
|
681
|
+
});
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
printTextResults(payload, cached);
|
|
685
|
+
}
|
|
686
|
+
async function enrichCachedPayload(query, payload, providerReady) {
|
|
687
|
+
if (payload.total === 0) {
|
|
688
|
+
return payload;
|
|
689
|
+
}
|
|
690
|
+
const rankedLocalMatches = rankLocalMatches(payload.localMatches, query);
|
|
691
|
+
const directAnswer = buildDirectAnswer(query, rankedLocalMatches, payload.serverMatches);
|
|
692
|
+
const synthesizedAnswer = !directAnswer && providerReady
|
|
693
|
+
? (payload.synthesizedAnswer
|
|
694
|
+
?? await synthesizeAnswerFromProvider(query, payload.localMatches, payload.serverMatches))
|
|
695
|
+
: null;
|
|
696
|
+
const nextProviderRequiredForSynthesis = !directAnswer && payload.total > 0;
|
|
697
|
+
const nextSuggestions = !directAnswer && nextProviderRequiredForSynthesis && !providerReady
|
|
698
|
+
? Array.from(new Set([
|
|
699
|
+
...buildHelpfulSuggestions(query),
|
|
700
|
+
'Run "umbrella-context providers setup" to connect a provider and model for richer synthesized answers.',
|
|
701
|
+
]))
|
|
702
|
+
: payload.suggestions.filter((suggestion) => suggestion !==
|
|
703
|
+
'Run "umbrella-context providers setup" to connect a provider and model for richer synthesized answers.');
|
|
704
|
+
const changed = JSON.stringify(payload.directAnswer ?? null) !== JSON.stringify(directAnswer ?? null) ||
|
|
705
|
+
JSON.stringify(payload.synthesizedAnswer ?? null) !== JSON.stringify(synthesizedAnswer ?? null) ||
|
|
706
|
+
payload.providerReady !== providerReady ||
|
|
707
|
+
payload.providerRequiredForSynthesis !== nextProviderRequiredForSynthesis ||
|
|
708
|
+
JSON.stringify(payload.suggestions) !== JSON.stringify(nextSuggestions);
|
|
709
|
+
if (!changed) {
|
|
710
|
+
return payload;
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
...payload,
|
|
714
|
+
providerReady,
|
|
715
|
+
directAnswer,
|
|
716
|
+
synthesizedAnswer,
|
|
717
|
+
providerRequiredForSynthesis: nextProviderRequiredForSynthesis,
|
|
718
|
+
suggestions: nextSuggestions,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
export async function searchCommandAction(query, opts = {}) {
|
|
5
722
|
const config = configManager.config;
|
|
723
|
+
const format = opts.format === "json" ? "json" : "text";
|
|
724
|
+
const providerReadiness = getProviderReadinessSummary();
|
|
725
|
+
const providerReady = providerReadiness.providerReady && providerReadiness.modelReady;
|
|
6
726
|
if (!config) {
|
|
727
|
+
if (format === "json") {
|
|
728
|
+
printJson({ ok: false, action: "query", error: "Not configured. Run: umbrella-context setup" });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
7
731
|
console.log(chalk.red("Not configured. Run: umbrella-context setup"));
|
|
8
732
|
return;
|
|
9
733
|
}
|
|
734
|
+
if (!query?.trim()) {
|
|
735
|
+
if (format === "json") {
|
|
736
|
+
printJson({
|
|
737
|
+
ok: false,
|
|
738
|
+
action: "query",
|
|
739
|
+
error: 'Query argument is required. Example: umbrella-context query "How does auth work?"',
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
console.log(chalk.red('\n Query argument is required.'));
|
|
744
|
+
console.log(chalk.gray(' Usage: umbrella-context query "How does auth work?"'));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (isVagueQuery(query)) {
|
|
748
|
+
const suggestions = buildHelpfulSuggestions(query);
|
|
749
|
+
if (format === "json") {
|
|
750
|
+
printJson({
|
|
751
|
+
ok: false,
|
|
752
|
+
action: "query",
|
|
753
|
+
error: "The query is too vague. Ask a fuller natural-language question.",
|
|
754
|
+
suggestions,
|
|
755
|
+
});
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
console.log(chalk.yellow('\n That question is too vague.'));
|
|
759
|
+
console.log(chalk.gray(" Ask a fuller natural-language question so Context can search more usefully."));
|
|
760
|
+
suggestions.forEach((suggestion) => {
|
|
761
|
+
console.log(chalk.gray(` - ${suggestion}`));
|
|
762
|
+
});
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
10
765
|
try {
|
|
11
766
|
await setSessionPanel("query", query);
|
|
12
767
|
await recordSessionQuery(query);
|
|
768
|
+
const fingerprint = await buildLocalQueryFingerprint();
|
|
769
|
+
const cached = await getQueryCacheEntry(query, fingerprint);
|
|
770
|
+
if (cached) {
|
|
771
|
+
const enrichedPayload = await enrichCachedPayload(query, cached.payload, providerReady);
|
|
772
|
+
if (enrichedPayload !== cached.payload) {
|
|
773
|
+
await storeQueryCacheEntry({
|
|
774
|
+
query,
|
|
775
|
+
normalizedQuery: normalizeQuery(query),
|
|
776
|
+
fingerprint,
|
|
777
|
+
payload: enrichedPayload,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
await markQueryCacheHit(cached.id);
|
|
781
|
+
await recordSessionEvent({
|
|
782
|
+
kind: "query",
|
|
783
|
+
title: `Answered cached query for "${query}"`,
|
|
784
|
+
detail: `Reused a cached answer for ${enrichedPayload.companyName} / ${enrichedPayload.spaceName}.`,
|
|
785
|
+
panel: "query",
|
|
786
|
+
focus: query,
|
|
787
|
+
status: "success",
|
|
788
|
+
});
|
|
789
|
+
printSearchPayload(format, enrichedPayload, true);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
13
792
|
const task = await createTask({
|
|
14
793
|
kind: "query",
|
|
15
794
|
title: `Query: ${query}`,
|
|
@@ -18,7 +797,8 @@ export async function searchCommandAction(query) {
|
|
|
18
797
|
panel: "query",
|
|
19
798
|
focus: query,
|
|
20
799
|
});
|
|
21
|
-
const
|
|
800
|
+
const rankedLocalMatches = rankLocalMatches(summarizeLocalMemoryMatches([...(await getPendingMemories()), ...(await getPulledMemories())], query), query);
|
|
801
|
+
const localMatches = rankedLocalMatches.map(({ entry }) => entry);
|
|
22
802
|
await addTaskReasoningContent(task.id, `Checked local repo context first and found ${localMatches.length} candidate match${localMatches.length === 1 ? "" : "es"}.`);
|
|
23
803
|
await addTaskToolCall(task.id, {
|
|
24
804
|
toolName: "server.memory.search",
|
|
@@ -43,6 +823,10 @@ export async function searchCommandAction(query) {
|
|
|
43
823
|
error: err.error,
|
|
44
824
|
});
|
|
45
825
|
await completeTask(task.id, "failed", err.error || "Search failed.");
|
|
826
|
+
if (format === "json") {
|
|
827
|
+
printJson({ ok: false, action: "query", error: err.error || "Search failed." });
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
46
830
|
console.log(chalk.red(`\n Failed: ${err.error}`));
|
|
47
831
|
return;
|
|
48
832
|
}
|
|
@@ -55,20 +839,56 @@ export async function searchCommandAction(query) {
|
|
|
55
839
|
});
|
|
56
840
|
await appendTaskStreamingContent(task.id, `server=${data.results.length};`);
|
|
57
841
|
await addTaskReasoningContent(task.id, `Shared search returned ${data.results.length} match${data.results.length === 1 ? "" : "es"}.`);
|
|
842
|
+
const payload = {
|
|
843
|
+
companyName: config.companyName,
|
|
844
|
+
spaceName: config.projectName,
|
|
845
|
+
query,
|
|
846
|
+
localMatches,
|
|
847
|
+
serverMatches: data.results,
|
|
848
|
+
total: localMatches.length + data.results.length,
|
|
849
|
+
suggestions: [],
|
|
850
|
+
providerReady,
|
|
851
|
+
providerRequiredForSynthesis: false,
|
|
852
|
+
directAnswer: buildDirectAnswer(query, rankedLocalMatches, data.results),
|
|
853
|
+
synthesizedAnswer: null,
|
|
854
|
+
};
|
|
58
855
|
if (data.results.length === 0 && localMatches.length === 0) {
|
|
59
|
-
await
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
856
|
+
await removeTask(task.id);
|
|
857
|
+
const suggestions = buildHelpfulSuggestions(query);
|
|
858
|
+
payload.suggestions = suggestions;
|
|
859
|
+
await storeQueryCacheEntry({
|
|
860
|
+
query,
|
|
861
|
+
normalizedQuery: normalizeQuery(query),
|
|
862
|
+
fingerprint,
|
|
863
|
+
payload,
|
|
67
864
|
});
|
|
68
|
-
|
|
865
|
+
if (format === "json") {
|
|
866
|
+
printSearchPayload(format, payload);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
printSearchPayload(format, payload);
|
|
69
870
|
return;
|
|
70
871
|
}
|
|
872
|
+
if (!payload.directAnswer && payload.total > 0) {
|
|
873
|
+
payload.providerRequiredForSynthesis = true;
|
|
874
|
+
if (!providerReady) {
|
|
875
|
+
payload.suggestions = [
|
|
876
|
+
...buildHelpfulSuggestions(query),
|
|
877
|
+
'Run "umbrella-context providers setup" to connect a provider and model for richer synthesized answers.',
|
|
878
|
+
];
|
|
879
|
+
await addTaskReasoningContent(task.id, "Partial context exists, but there is no provider/model ready for a richer synthesized answer yet.");
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
payload.synthesizedAnswer = await synthesizeAnswerFromProvider(query, localMatches, data.results);
|
|
883
|
+
await addTaskReasoningContent(task.id, payload.synthesizedAnswer
|
|
884
|
+
? "Partial context existed, and the connected provider synthesized one cleaner answer from the saved matches."
|
|
885
|
+
: "Partial context exists and a provider is ready, so this query can support richer synthesis in a later pass.");
|
|
886
|
+
}
|
|
887
|
+
}
|
|
71
888
|
await completeTask(task.id, "completed", `Found ${localMatches.length} local match${localMatches.length === 1 ? "" : "es"} and ${data.results.length} shared match${data.results.length === 1 ? "" : "es"}.`);
|
|
889
|
+
if (payload.directAnswer) {
|
|
890
|
+
await addTaskReasoningContent(task.id, `A direct answer was possible because one match clearly dominated the others.`);
|
|
891
|
+
}
|
|
72
892
|
await recordSessionEvent({
|
|
73
893
|
kind: "query",
|
|
74
894
|
title: `Searched Context for "${query}"`,
|
|
@@ -77,29 +897,13 @@ export async function searchCommandAction(query) {
|
|
|
77
897
|
focus: query,
|
|
78
898
|
status: "success",
|
|
79
899
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
console.log(` ${result.content}`);
|
|
86
|
-
if (result.tags?.length)
|
|
87
|
-
console.log(chalk.gray(` Tags: ${result.tags.join(", ")}`));
|
|
88
|
-
console.log(chalk.gray(` Saved: ${new Date(result.createdAt).toLocaleString()}`));
|
|
89
|
-
console.log("");
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
if (data.results.length > 0) {
|
|
93
|
-
console.log(chalk.cyan(" Server matches:\n"));
|
|
94
|
-
}
|
|
95
|
-
data.results.forEach((result, index) => {
|
|
96
|
-
console.log(chalk.cyan(` ${index + 1}. [${result.systemType}]`));
|
|
97
|
-
console.log(` ${result.content}`);
|
|
98
|
-
if (result.tags?.length)
|
|
99
|
-
console.log(chalk.gray(` Tags: ${result.tags.join(", ")}`));
|
|
100
|
-
console.log(chalk.gray(` Source: ${result.source} | ${new Date(result.createdAt).toLocaleDateString()}`));
|
|
101
|
-
console.log("");
|
|
900
|
+
await storeQueryCacheEntry({
|
|
901
|
+
query,
|
|
902
|
+
normalizedQuery: normalizeQuery(query),
|
|
903
|
+
fingerprint,
|
|
904
|
+
payload,
|
|
102
905
|
});
|
|
906
|
+
printSearchPayload(format, payload);
|
|
103
907
|
}
|
|
104
908
|
catch (err) {
|
|
105
909
|
const failedTask = await createTask({
|
|
@@ -111,11 +915,28 @@ export async function searchCommandAction(query) {
|
|
|
111
915
|
focus: query,
|
|
112
916
|
});
|
|
113
917
|
await completeTask(failedTask.id, "failed", err.message);
|
|
918
|
+
if (format === "json") {
|
|
919
|
+
printJson({ ok: false, action: "query", error: err.message });
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
114
922
|
console.log(chalk.red(`\n Error: ${err.message}`));
|
|
115
923
|
}
|
|
116
924
|
}
|
|
117
925
|
export function searchCommand(cli) {
|
|
118
|
-
cli
|
|
119
|
-
|
|
926
|
+
cli
|
|
927
|
+
.command("search <query>", `Query and retrieve information from saved context.
|
|
928
|
+
|
|
929
|
+
Good:
|
|
930
|
+
- "How is authentication implemented?"
|
|
931
|
+
- "What are the API rate limits and where are they enforced?"
|
|
932
|
+
Bad:
|
|
933
|
+
- "auth"
|
|
934
|
+
- "authentication"
|
|
935
|
+
- "show me code"`)
|
|
936
|
+
.option("--format <format>", "Output format (text or json)")
|
|
937
|
+
.example('search "How is authentication implemented?"')
|
|
938
|
+
.example('search "How does auth work?" --format json')
|
|
939
|
+
.action(async (query, opts) => {
|
|
940
|
+
await searchCommandAction(query, opts);
|
|
120
941
|
});
|
|
121
942
|
}
|