bikky 0.4.2 ā 0.4.3
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 +6 -4
- package/dist/config.d.ts +11 -1
- package/dist/config.js +88 -20
- package/dist/daemon/capture-policy.d.ts +0 -1
- package/dist/daemon/capture-policy.js +0 -1
- package/dist/daemon/consolidation.d.ts +2 -1
- package/dist/daemon/consolidation.js +28 -11
- package/dist/daemon/entity-typing.js +10 -0
- package/dist/daemon/episode-summary.d.ts +4 -0
- package/dist/daemon/episode-summary.js +39 -8
- package/dist/daemon/extraction.d.ts +1 -1
- package/dist/daemon/extraction.js +52 -17
- package/dist/daemon/qdrant.d.ts +32 -10
- package/dist/daemon/qdrant.js +177 -60
- package/dist/daemon/relations.d.ts +3 -3
- package/dist/daemon/relations.js +27 -15
- package/dist/daemon/session-index.d.ts +5 -0
- package/dist/daemon/session-index.js +36 -9
- package/dist/daemon/session-summary.d.ts +3 -0
- package/dist/daemon/session-summary.js +48 -15
- package/dist/daemon/staleness.js +2 -2
- package/dist/daemon/transcript-sources.js +3 -2
- package/dist/daemon/watcher.js +2 -0
- package/dist/daemon/workstream-summary.d.ts +4 -0
- package/dist/daemon/workstream-summary.js +58 -16
- package/dist/install.d.ts +11 -0
- package/dist/install.js +38 -0
- package/dist/llm/embedding/index.js +2 -1
- package/dist/llm/embedding/providers/openai.js +8 -2
- package/dist/llm/embedding/providers/portkey.js +9 -2
- package/dist/llm/inference/index.js +2 -1
- package/dist/llm/util.d.ts +12 -0
- package/dist/llm/util.js +18 -0
- package/dist/mcp/helpers.d.ts +5 -0
- package/dist/mcp/helpers.js +27 -3
- package/dist/mcp/taxonomy.js +12 -1
- package/dist/mcp/tools.js +161 -57
- package/dist/mcp/types.d.ts +12 -0
- package/dist/package-verifier.d.ts +19 -0
- package/dist/package-verifier.js +83 -0
- package/dist/provenance/origin.d.ts +57 -0
- package/dist/provenance/origin.js +254 -0
- package/docs/config/fully-hosted.md +33 -13
- package/docs/config/hosted-models.md +33 -13
- package/docs/configuration.md +18 -0
- package/package.json +2 -2
|
@@ -8,6 +8,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
8
8
|
import { CAPTURE_POLICY_VERSION, DEFAULT_CAPTURE_CONTEXT, PROMPT_VERSIONS } from "./capture-policy.js";
|
|
9
9
|
import * as qdrant from "./qdrant.js";
|
|
10
10
|
import { combineRedactions, redactStorageText, } from "../privacy/redaction.js";
|
|
11
|
+
import { buildOperationOrigin } from "../provenance/origin.js";
|
|
11
12
|
const contentHash = (text) => createHash("sha256").update(`session-index:${text}`).digest("hex");
|
|
12
13
|
export const buildSessionIndexDraft = (input) => {
|
|
13
14
|
const episodeIds = input.episodeResults
|
|
@@ -48,6 +49,16 @@ export const buildSessionIndexPayload = (input) => {
|
|
|
48
49
|
const redactedEntities = input.draft.entities.map((entity) => redactStorageText(entity, input.redactionOptions));
|
|
49
50
|
const redaction = combineRedactions([redactedContent, ...redactedEntities]);
|
|
50
51
|
const existingPayload = input.existing?.payload ?? {};
|
|
52
|
+
const operationOrigin = input.origin ?? buildOperationOrigin({
|
|
53
|
+
interface: "daemon",
|
|
54
|
+
action: input.existing ? "update" : "create",
|
|
55
|
+
subsystem: "session_index",
|
|
56
|
+
config: input.config,
|
|
57
|
+
metadata: {
|
|
58
|
+
session_id: input.sessionId,
|
|
59
|
+
event_count: input.eventCount,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
51
62
|
const payload = {
|
|
52
63
|
...existingPayload,
|
|
53
64
|
content: redactedContent.text,
|
|
@@ -57,9 +68,9 @@ export const buildSessionIndexPayload = (input) => {
|
|
|
57
68
|
memory_subtype: "session_index",
|
|
58
69
|
layer: "episode",
|
|
59
70
|
...(input.scope.workspaceId ? { workspace_id: input.scope.workspaceId } : {}),
|
|
60
|
-
|
|
71
|
+
origin: existingPayload.origin ?? operationOrigin,
|
|
72
|
+
...(input.existing ? { last_operation_origin: operationOrigin } : {}),
|
|
61
73
|
entities: redactedEntities.map((entity) => entity.text.toLowerCase()),
|
|
62
|
-
source: "system",
|
|
63
74
|
confidence: 1.0,
|
|
64
75
|
importance: input.draft.importance,
|
|
65
76
|
content_hash: contentHash(redactedContent.text),
|
|
@@ -93,24 +104,38 @@ export const buildSessionIndexPayload = (input) => {
|
|
|
93
104
|
}
|
|
94
105
|
return { payload, redaction };
|
|
95
106
|
};
|
|
96
|
-
const findExistingSessionIndex = async (sessionId, scope) => {
|
|
97
|
-
const
|
|
107
|
+
const findExistingSessionIndex = async (sessionId, scope, destination) => {
|
|
108
|
+
const collection = qdrant.collectionForDestination(destination);
|
|
109
|
+
const result = await qdrant.qdrantRequest("POST", `/collections/${collection}/points/scroll`, {
|
|
98
110
|
filter: buildSessionIndexFilter(sessionId, scope),
|
|
99
111
|
limit: 1,
|
|
100
112
|
with_payload: true,
|
|
101
|
-
});
|
|
113
|
+
}, destination);
|
|
102
114
|
return result.result?.points?.[0] ?? null;
|
|
103
115
|
};
|
|
104
116
|
export const updateSessionIndex = async (input) => {
|
|
105
117
|
if (input.episodeResults.length === 0) {
|
|
106
118
|
return { action: "skipped", reason: "no_episode_results" };
|
|
107
119
|
}
|
|
108
|
-
const existing = await findExistingSessionIndex(input.sessionId, input.scope);
|
|
109
120
|
const draft = buildSessionIndexDraft({
|
|
110
121
|
sessionId: input.sessionId,
|
|
111
122
|
eventCount: input.eventCount,
|
|
112
123
|
episodeResults: input.episodeResults,
|
|
113
124
|
});
|
|
125
|
+
const destination = input.destination
|
|
126
|
+
?? input.episodeResults.find((result) => result.destination)?.destination
|
|
127
|
+
?? qdrant.resolveDestination({
|
|
128
|
+
content: draft.content,
|
|
129
|
+
entities: draft.entities,
|
|
130
|
+
metadata: {
|
|
131
|
+
session_id: input.sessionId,
|
|
132
|
+
memory_subtype: "session_index",
|
|
133
|
+
kind: "summary",
|
|
134
|
+
origin_interface: "daemon",
|
|
135
|
+
origin_agent_type: "daemon",
|
|
136
|
+
},
|
|
137
|
+
}).name;
|
|
138
|
+
const existing = await findExistingSessionIndex(input.sessionId, input.scope, destination);
|
|
114
139
|
const now = new Date().toISOString();
|
|
115
140
|
const { payload } = buildSessionIndexPayload({
|
|
116
141
|
draft,
|
|
@@ -119,6 +144,7 @@ export const updateSessionIndex = async (input) => {
|
|
|
119
144
|
now,
|
|
120
145
|
existing,
|
|
121
146
|
eventCount: input.eventCount,
|
|
147
|
+
config: input.config,
|
|
122
148
|
redactionOptions: {
|
|
123
149
|
enabled: true,
|
|
124
150
|
redactPii: false,
|
|
@@ -126,9 +152,10 @@ export const updateSessionIndex = async (input) => {
|
|
|
126
152
|
});
|
|
127
153
|
const vector = await qdrant.embed(String(payload.content));
|
|
128
154
|
const factId = existing?.id ?? randomUUID();
|
|
129
|
-
|
|
155
|
+
const collection = qdrant.collectionForDestination(destination);
|
|
156
|
+
await qdrant.qdrantRequest("PUT", `/collections/${collection}/points`, {
|
|
130
157
|
points: [{ id: factId, vector, payload }],
|
|
131
|
-
});
|
|
132
|
-
return { action: existing ? "updated" : "stored", factId };
|
|
158
|
+
}, destination);
|
|
159
|
+
return { action: existing ? "updated" : "stored", factId, destination };
|
|
133
160
|
};
|
|
134
161
|
//# sourceMappingURL=session-index.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BikkyConfig } from "../config.js";
|
|
2
2
|
import type { QdrantPayload } from "./qdrant.js";
|
|
3
3
|
import { type RedactionSummary } from "../privacy/redaction.js";
|
|
4
|
+
import { type OperationOrigin } from "../provenance/origin.js";
|
|
4
5
|
export interface WorkspaceScope {
|
|
5
6
|
workspaceId?: string;
|
|
6
7
|
actorId?: string;
|
|
@@ -52,6 +53,8 @@ export declare const buildSessionSummaryPayload: (input: {
|
|
|
52
53
|
enabled: boolean;
|
|
53
54
|
redactPii: boolean;
|
|
54
55
|
};
|
|
56
|
+
config?: BikkyConfig;
|
|
57
|
+
origin?: OperationOrigin;
|
|
55
58
|
}) => SessionSummaryPayloadResult;
|
|
56
59
|
export declare const updateSessionSummary: (input: {
|
|
57
60
|
sessionId: string;
|
|
@@ -13,7 +13,7 @@ import { buildSessionIndexFilter, updateSessionIndex } from "./session-index.js"
|
|
|
13
13
|
import { updateWorkstreamSummaries } from "./workstream-summary.js";
|
|
14
14
|
import * as qdrant from "./qdrant.js";
|
|
15
15
|
import { combineRedactions, redactStorageText, } from "../privacy/redaction.js";
|
|
16
|
-
import {
|
|
16
|
+
import { buildOperationOrigin } from "../provenance/origin.js";
|
|
17
17
|
const DEFAULT_SUMMARY_IMPORTANCE = 0.8;
|
|
18
18
|
const contentHash = (text) => createHash("sha256").update(`summary:${text}`).digest("hex");
|
|
19
19
|
const stripJsonFence = (raw) => raw.trim().replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "").trim();
|
|
@@ -92,6 +92,16 @@ export const buildSessionSummaryPayload = (input) => {
|
|
|
92
92
|
...redactedEntities,
|
|
93
93
|
]);
|
|
94
94
|
const existingPayload = input.existing?.payload ?? {};
|
|
95
|
+
const operationOrigin = input.origin ?? buildOperationOrigin({
|
|
96
|
+
interface: "daemon",
|
|
97
|
+
action: input.existing ? "update" : "create",
|
|
98
|
+
subsystem: "session_summary",
|
|
99
|
+
config: input.config,
|
|
100
|
+
metadata: {
|
|
101
|
+
session_id: input.sessionId,
|
|
102
|
+
event_count: input.eventCount,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
95
105
|
const payload = {
|
|
96
106
|
...existingPayload,
|
|
97
107
|
content: redactedContent.text,
|
|
@@ -101,9 +111,9 @@ export const buildSessionSummaryPayload = (input) => {
|
|
|
101
111
|
memory_subtype: "session_index",
|
|
102
112
|
layer: "episode",
|
|
103
113
|
...(input.scope.workspaceId ? { workspace_id: input.scope.workspaceId } : {}),
|
|
104
|
-
|
|
114
|
+
origin: existingPayload.origin ?? operationOrigin,
|
|
115
|
+
...(input.existing ? { last_operation_origin: operationOrigin } : {}),
|
|
105
116
|
entities: redactedEntities.map((entity) => entity.text.toLowerCase()),
|
|
106
|
-
source: "system",
|
|
107
117
|
confidence: 1.0,
|
|
108
118
|
importance: input.draft.importance,
|
|
109
119
|
content_hash: contentHash(redactedContent.text),
|
|
@@ -144,8 +154,7 @@ export const updateSessionSummary = async (input) => {
|
|
|
144
154
|
return { action: "skipped", reason: "qdrant_not_ready" };
|
|
145
155
|
}
|
|
146
156
|
const config = input.config ?? loadConfig();
|
|
147
|
-
const
|
|
148
|
-
const scope = actor.actor_id ? { actorId: actor.actor_id } : {};
|
|
157
|
+
const scope = {};
|
|
149
158
|
const segments = segmentTranscriptIntoEpisodes({
|
|
150
159
|
sessionId: input.sessionId,
|
|
151
160
|
transcript: input.transcript,
|
|
@@ -164,18 +173,42 @@ export const updateSessionSummary = async (input) => {
|
|
|
164
173
|
if (episodeResults.length === 0) {
|
|
165
174
|
return { action: "skipped", reason: "no_episode_summaries" };
|
|
166
175
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
176
|
+
let indexResult = null;
|
|
177
|
+
const episodeResultsByDestination = new Map();
|
|
178
|
+
for (const result of episodeResults) {
|
|
179
|
+
const destination = result.destination ?? qdrant.resolveDestination({
|
|
180
|
+
content: result.workstreamKey ?? result.episodeId ?? input.sessionId,
|
|
181
|
+
entities: [result.workstreamKey, result.episodeId].filter((value) => Boolean(value)),
|
|
182
|
+
metadata: {
|
|
183
|
+
session_id: input.sessionId,
|
|
184
|
+
...(result.episodeId ? { episode_id: result.episodeId } : {}),
|
|
185
|
+
...(result.workstreamKey ? { workstream_key: result.workstreamKey } : {}),
|
|
186
|
+
kind: "summary",
|
|
187
|
+
memory_subtype: "session_index",
|
|
188
|
+
origin_interface: "daemon",
|
|
189
|
+
origin_agent_type: "daemon",
|
|
190
|
+
},
|
|
191
|
+
}).name;
|
|
192
|
+
const bucket = episodeResultsByDestination.get(destination) ?? [];
|
|
193
|
+
bucket.push(result);
|
|
194
|
+
episodeResultsByDestination.set(destination, bucket);
|
|
195
|
+
}
|
|
196
|
+
for (const [destination, destinationEpisodeResults] of episodeResultsByDestination) {
|
|
197
|
+
const result = await updateSessionIndex({
|
|
198
|
+
sessionId: input.sessionId,
|
|
199
|
+
eventCount: input.eventCount,
|
|
200
|
+
episodeResults: destinationEpisodeResults,
|
|
201
|
+
scope,
|
|
202
|
+
config,
|
|
203
|
+
destination,
|
|
204
|
+
});
|
|
205
|
+
indexResult ??= result;
|
|
206
|
+
}
|
|
174
207
|
await updateWorkstreamSummaries({ episodeResults, scope, config });
|
|
175
208
|
return {
|
|
176
|
-
action: indexResult
|
|
177
|
-
factId: indexResult
|
|
178
|
-
reason: indexResult
|
|
209
|
+
action: indexResult?.action ?? "skipped",
|
|
210
|
+
factId: indexResult?.factId,
|
|
211
|
+
reason: indexResult?.reason,
|
|
179
212
|
};
|
|
180
213
|
};
|
|
181
214
|
//# sourceMappingURL=session-summary.js.map
|
package/dist/daemon/staleness.js
CHANGED
|
@@ -11,7 +11,7 @@ let logFn = console.log;
|
|
|
11
11
|
let lastStaleIds = "";
|
|
12
12
|
const defaultDeps = {
|
|
13
13
|
isReady: qdrantMod.isReady,
|
|
14
|
-
scrollFacts: qdrantMod.
|
|
14
|
+
scrollFacts: qdrantMod.scrollFactsAcrossDestinations,
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
17
|
* Scan for stale facts via direct Qdrant scroll query.
|
|
@@ -37,7 +37,7 @@ export const scanStaleFacts = async (config, deps = defaultDeps) => {
|
|
|
37
37
|
}, limit);
|
|
38
38
|
if (staleFacts.length > 0) {
|
|
39
39
|
// Deduplicate: only log if the set of stale fact IDs has changed
|
|
40
|
-
const currentIds = staleFacts.map((f) => f.id).sort().join(",");
|
|
40
|
+
const currentIds = staleFacts.map((f) => `${f.destination ?? "default"}:${f.id}`).sort().join(",");
|
|
41
41
|
if (currentIds === lastStaleIds) {
|
|
42
42
|
logFn("DEBUG", `Staleness scan: same ${staleFacts.length} fact(s) still stale, skipping duplicate log`);
|
|
43
43
|
return;
|
|
@@ -166,11 +166,12 @@ export const parseClaudeTranscriptLine = (line) => {
|
|
|
166
166
|
};
|
|
167
167
|
export const readNewTranscriptEvents = async (eventsPath, byteOffset, source) => {
|
|
168
168
|
const fileStat = await stat(eventsPath);
|
|
169
|
-
if (fileStat.size
|
|
169
|
+
if (fileStat.size === byteOffset) {
|
|
170
170
|
return { events: [], newOffset: byteOffset, totalLines: 0 };
|
|
171
171
|
}
|
|
172
|
+
const startOffset = fileStat.size < byteOffset ? 0 : byteOffset;
|
|
172
173
|
const buf = await readFile(eventsPath);
|
|
173
|
-
const newContent = buf.subarray(
|
|
174
|
+
const newContent = buf.subarray(startOffset).toString("utf-8");
|
|
174
175
|
const events = [];
|
|
175
176
|
let totalLines = 0;
|
|
176
177
|
for (const line of newContent.split("\n")) {
|
package/dist/daemon/watcher.js
CHANGED
|
@@ -6,6 +6,8 @@ import path from "node:path";
|
|
|
6
6
|
import { loadConfig } from "../config.js";
|
|
7
7
|
export function discoverSessions() {
|
|
8
8
|
const cfg = loadConfig();
|
|
9
|
+
if (!cfg.watchers.copilot.enabled)
|
|
10
|
+
return [];
|
|
9
11
|
const baseDir = cfg.watchers.copilot.path;
|
|
10
12
|
if (!fs.existsSync(baseDir))
|
|
11
13
|
return [];
|
|
@@ -2,6 +2,7 @@ import type { BikkyConfig } from "../config.js";
|
|
|
2
2
|
import type { EpisodeSummaryWriteResult } from "./episode-summary.js";
|
|
3
3
|
import type { QdrantPayload } from "./qdrant.js";
|
|
4
4
|
import { type RedactionSummary } from "../privacy/redaction.js";
|
|
5
|
+
import { type OperationOrigin } from "../provenance/origin.js";
|
|
5
6
|
export { buildWorkstreamSummaryMessages } from "../prompts/index.js";
|
|
6
7
|
export interface WorkspaceScope {
|
|
7
8
|
workspaceId?: string;
|
|
@@ -23,6 +24,7 @@ export interface WorkstreamSummaryPayloadResult {
|
|
|
23
24
|
export interface WorkstreamUpdateResult {
|
|
24
25
|
action: "stored" | "updated" | "skipped";
|
|
25
26
|
factId?: string;
|
|
27
|
+
destination?: string;
|
|
26
28
|
workstreamKey?: string;
|
|
27
29
|
reason?: string;
|
|
28
30
|
}
|
|
@@ -45,6 +47,8 @@ export declare const buildWorkstreamSummaryPayload: (input: {
|
|
|
45
47
|
enabled: boolean;
|
|
46
48
|
redactPii: boolean;
|
|
47
49
|
};
|
|
50
|
+
config?: BikkyConfig;
|
|
51
|
+
origin?: OperationOrigin;
|
|
48
52
|
}) => WorkstreamSummaryPayloadResult;
|
|
49
53
|
export declare const updateWorkstreamSummaries: (input: {
|
|
50
54
|
episodeResults: EpisodeSummaryWriteResult[];
|
|
@@ -11,6 +11,7 @@ import { workstreamSummaryPrompt } from "../prompts/index.js";
|
|
|
11
11
|
import { CAPTURE_POLICY_VERSION, CAPTURE_TRIGGERS, DEFAULT_CAPTURE_CONTEXT, PROMPT_VERSIONS, } from "./capture-policy.js";
|
|
12
12
|
import * as qdrant from "./qdrant.js";
|
|
13
13
|
import { combineRedactions, redactStorageText, } from "../privacy/redaction.js";
|
|
14
|
+
import { buildOperationOrigin } from "../provenance/origin.js";
|
|
14
15
|
export { buildWorkstreamSummaryMessages } from "../prompts/index.js";
|
|
15
16
|
const contentHash = (text) => createHash("sha256").update(`workstream:${text}`).digest("hex");
|
|
16
17
|
const stripJsonFence = (raw) => raw.trim().replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "").trim();
|
|
@@ -39,6 +40,20 @@ export const groupEpisodeResultsByWorkstream = (episodeResults) => {
|
|
|
39
40
|
}
|
|
40
41
|
return grouped;
|
|
41
42
|
};
|
|
43
|
+
const groupEpisodeResultsByWorkstreamDestination = (episodeResults) => {
|
|
44
|
+
const grouped = new Map();
|
|
45
|
+
for (const result of episodeResults) {
|
|
46
|
+
const workstreamKey = result.workstreamKey?.trim();
|
|
47
|
+
if (!workstreamKey || !result.episodeId)
|
|
48
|
+
continue;
|
|
49
|
+
const destination = result.destination?.trim();
|
|
50
|
+
const groupKey = `${destination ?? ""}::${workstreamKey}`;
|
|
51
|
+
const existing = grouped.get(groupKey) ?? { workstreamKey, destination, results: [] };
|
|
52
|
+
existing.results.push(result);
|
|
53
|
+
grouped.set(groupKey, existing);
|
|
54
|
+
}
|
|
55
|
+
return grouped;
|
|
56
|
+
};
|
|
42
57
|
export const parseWorkstreamSummaryDraft = (raw) => {
|
|
43
58
|
const parsed = JSON.parse(stripJsonFence(raw));
|
|
44
59
|
const contentValue = parsed.content ?? parsed.summary;
|
|
@@ -109,6 +124,17 @@ export const buildWorkstreamSummaryPayload = (input) => {
|
|
|
109
124
|
...redactedEntities,
|
|
110
125
|
]);
|
|
111
126
|
const existingPayload = input.existing?.payload ?? {};
|
|
127
|
+
const operationOrigin = input.origin ?? buildOperationOrigin({
|
|
128
|
+
interface: "daemon",
|
|
129
|
+
action: input.existing ? "update" : "create",
|
|
130
|
+
subsystem: "workstream_summary",
|
|
131
|
+
config: input.config,
|
|
132
|
+
metadata: {
|
|
133
|
+
workstream_key: input.workstreamKey,
|
|
134
|
+
source_episode_count: input.sourceEpisodeIds.length,
|
|
135
|
+
...(input.repo ? { repo: input.repo } : {}),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
112
138
|
const payload = {
|
|
113
139
|
...existingPayload,
|
|
114
140
|
content: redactedContent.text,
|
|
@@ -118,10 +144,10 @@ export const buildWorkstreamSummaryPayload = (input) => {
|
|
|
118
144
|
memory_subtype: "workstream",
|
|
119
145
|
layer: "workstream",
|
|
120
146
|
...(input.scope.workspaceId ? { workspace_id: input.scope.workspaceId } : {}),
|
|
121
|
-
...(input.scope.actorId ? { actor_id: input.scope.actorId } : {}),
|
|
122
147
|
...(input.repo ? { repo: input.repo } : {}),
|
|
148
|
+
origin: existingPayload.origin ?? operationOrigin,
|
|
149
|
+
...(input.existing ? { last_operation_origin: operationOrigin } : {}),
|
|
123
150
|
entities: redactedEntities.map((entity) => entity.text.toLowerCase()),
|
|
124
|
-
source: "system",
|
|
125
151
|
confidence: 1.0,
|
|
126
152
|
importance: input.draft.importance,
|
|
127
153
|
content_hash: contentHash(redactedContent.text),
|
|
@@ -156,20 +182,22 @@ export const buildWorkstreamSummaryPayload = (input) => {
|
|
|
156
182
|
}
|
|
157
183
|
return { payload, redaction };
|
|
158
184
|
};
|
|
159
|
-
const findExistingWorkstreamSummary = async (workstreamKey, scope, repo) => {
|
|
160
|
-
const
|
|
185
|
+
const findExistingWorkstreamSummary = async (workstreamKey, scope, repo, destination) => {
|
|
186
|
+
const collection = qdrant.collectionForDestination(destination);
|
|
187
|
+
const result = await qdrant.qdrantRequest("POST", `/collections/${collection}/points/scroll`, {
|
|
161
188
|
filter: buildWorkstreamSummaryFilter(workstreamKey, scope, repo),
|
|
162
189
|
limit: 1,
|
|
163
190
|
with_payload: true,
|
|
164
|
-
});
|
|
191
|
+
}, destination);
|
|
165
192
|
return result.result?.points?.[0] ?? null;
|
|
166
193
|
};
|
|
167
|
-
const loadEpisodeSummaries = async (workstreamKey, scope) => {
|
|
168
|
-
const
|
|
194
|
+
const loadEpisodeSummaries = async (workstreamKey, scope, destination) => {
|
|
195
|
+
const collection = qdrant.collectionForDestination(destination);
|
|
196
|
+
const result = await qdrant.qdrantRequest("POST", `/collections/${collection}/points/scroll`, {
|
|
169
197
|
filter: buildWorkstreamEpisodeFilter(workstreamKey, scope),
|
|
170
198
|
limit: CAPTURE_TRIGGERS.workstreamSummary.maxEpisodesPerUpdate,
|
|
171
199
|
with_payload: true,
|
|
172
|
-
});
|
|
200
|
+
}, destination);
|
|
173
201
|
return result.result?.points ?? [];
|
|
174
202
|
};
|
|
175
203
|
const summarizeWorkstream = async (input) => {
|
|
@@ -183,20 +211,32 @@ const summarizeWorkstream = async (input) => {
|
|
|
183
211
|
return parseWorkstreamSummaryDraft(result);
|
|
184
212
|
};
|
|
185
213
|
export const updateWorkstreamSummaries = async (input) => {
|
|
186
|
-
const grouped =
|
|
214
|
+
const grouped = groupEpisodeResultsByWorkstreamDestination(input.episodeResults);
|
|
187
215
|
if (grouped.size === 0)
|
|
188
216
|
return [{ action: "skipped", reason: "no_workstream_keys" }];
|
|
189
217
|
const results = [];
|
|
190
|
-
for (const
|
|
191
|
-
const
|
|
218
|
+
for (const { workstreamKey, destination: groupDestination } of grouped.values()) {
|
|
219
|
+
const destination = groupDestination
|
|
220
|
+
?? qdrant.resolveDestination({
|
|
221
|
+
content: workstreamKey,
|
|
222
|
+
entities: [workstreamKey],
|
|
223
|
+
metadata: {
|
|
224
|
+
workstream_key: workstreamKey,
|
|
225
|
+
memory_subtype: "workstream",
|
|
226
|
+
kind: "summary",
|
|
227
|
+
origin_interface: "daemon",
|
|
228
|
+
origin_agent_type: "daemon",
|
|
229
|
+
},
|
|
230
|
+
}).name;
|
|
231
|
+
const episodes = await loadEpisodeSummaries(workstreamKey, input.scope, destination);
|
|
192
232
|
const episodeSummaries = episodes
|
|
193
233
|
.map((episode) => episode.payload?.content)
|
|
194
234
|
.filter((content) => Boolean(content?.trim()));
|
|
195
235
|
if (episodeSummaries.length < CAPTURE_TRIGGERS.workstreamSummary.minEpisodeCount) {
|
|
196
|
-
results.push({ action: "skipped", reason: "not_enough_episodes", workstreamKey });
|
|
236
|
+
results.push({ action: "skipped", reason: "not_enough_episodes", workstreamKey, destination });
|
|
197
237
|
continue;
|
|
198
238
|
}
|
|
199
|
-
const existing = await findExistingWorkstreamSummary(workstreamKey, input.scope, null);
|
|
239
|
+
const existing = await findExistingWorkstreamSummary(workstreamKey, input.scope, null, destination);
|
|
200
240
|
const draft = await summarizeWorkstream({
|
|
201
241
|
workstreamKey,
|
|
202
242
|
existingSummary: existing?.payload?.content ?? null,
|
|
@@ -211,6 +251,7 @@ export const updateWorkstreamSummaries = async (input) => {
|
|
|
211
251
|
existing,
|
|
212
252
|
sourceEpisodeIds: episodes.map((episode) => episode.payload?.episode_id ?? episode.id).filter(Boolean),
|
|
213
253
|
repo: null,
|
|
254
|
+
config: input.config,
|
|
214
255
|
redactionOptions: {
|
|
215
256
|
enabled: true,
|
|
216
257
|
redactPii: false,
|
|
@@ -218,10 +259,11 @@ export const updateWorkstreamSummaries = async (input) => {
|
|
|
218
259
|
});
|
|
219
260
|
const vector = await qdrant.embed(String(payload.content));
|
|
220
261
|
const factId = existing?.id ?? randomUUID();
|
|
221
|
-
|
|
262
|
+
const collection = qdrant.collectionForDestination(destination);
|
|
263
|
+
await qdrant.qdrantRequest("PUT", `/collections/${collection}/points`, {
|
|
222
264
|
points: [{ id: factId, vector, payload }],
|
|
223
|
-
});
|
|
224
|
-
results.push({ action: existing ? "updated" : "stored", factId, workstreamKey });
|
|
265
|
+
}, destination);
|
|
266
|
+
results.push({ action: existing ? "updated" : "stored", factId, workstreamKey, destination });
|
|
225
267
|
}
|
|
226
268
|
return results;
|
|
227
269
|
};
|
package/dist/install.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Write MCP config entries for Copilot and/or Claude Code.
|
|
3
3
|
*/
|
|
4
|
+
import { type OriginIdentity } from "./provenance/origin.js";
|
|
4
5
|
export interface InstallOptions {
|
|
5
6
|
homeDir?: string;
|
|
6
7
|
/**
|
|
@@ -8,6 +9,16 @@ export interface InstallOptions {
|
|
|
8
9
|
* user config file directly.
|
|
9
10
|
*/
|
|
10
11
|
claudeCommand?: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Defaults to true. Set to false in tests or advanced flows that only want
|
|
14
|
+
* MCP config files and do not want ~/.bikky/config.json touched.
|
|
15
|
+
*/
|
|
16
|
+
provisionIdentity?: boolean;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
hostname?: string;
|
|
20
|
+
shellUsername?: string | null;
|
|
11
21
|
}
|
|
22
|
+
export declare function provisionUserIdentityConfig(options?: InstallOptions): OriginIdentity | null;
|
|
12
23
|
export declare function writeInstallConfig(options?: InstallOptions): Promise<void>;
|
|
13
24
|
//# sourceMappingURL=install.d.ts.map
|
package/dist/install.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { inferUserIdentity } from "./provenance/origin.js";
|
|
8
9
|
const SERVER_NAME = "bikky";
|
|
9
10
|
function readJsonConfig(filePath) {
|
|
10
11
|
if (!fs.existsSync(filePath))
|
|
@@ -57,6 +58,40 @@ function writeClaudeCodeUserConfig(homeDir, entry) {
|
|
|
57
58
|
writeJsonConfig(claudeConfigPath, config);
|
|
58
59
|
console.log(`ā
Written to ${claudeConfigPath}`);
|
|
59
60
|
}
|
|
61
|
+
function bikkyConfigPath(homeDir, explicitHomeDir, env = process.env) {
|
|
62
|
+
const bikkyDir = explicitHomeDir ? path.join(homeDir, ".bikky") : env.BIKKY_HOME ?? path.join(homeDir, ".bikky");
|
|
63
|
+
return path.join(bikkyDir, "config.json");
|
|
64
|
+
}
|
|
65
|
+
function hasConfiguredValue(value) {
|
|
66
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
67
|
+
}
|
|
68
|
+
export function provisionUserIdentityConfig(options = {}) {
|
|
69
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
70
|
+
const configPath = bikkyConfigPath(homeDir, options.homeDir !== undefined, options.env);
|
|
71
|
+
const config = readJsonConfig(configPath);
|
|
72
|
+
const existingIdentity = config.identity && typeof config.identity === "object" && !Array.isArray(config.identity)
|
|
73
|
+
? config.identity
|
|
74
|
+
: {};
|
|
75
|
+
const hasUserId = hasConfiguredValue(existingIdentity.user_id);
|
|
76
|
+
const hasUserName = hasConfiguredValue(existingIdentity.user_name);
|
|
77
|
+
if (hasUserId && hasUserName)
|
|
78
|
+
return null;
|
|
79
|
+
const inferred = inferUserIdentity({
|
|
80
|
+
env: options.env,
|
|
81
|
+
cwd: options.cwd,
|
|
82
|
+
hostname: options.hostname,
|
|
83
|
+
shellUsername: options.shellUsername,
|
|
84
|
+
});
|
|
85
|
+
const nextIdentity = { ...existingIdentity };
|
|
86
|
+
if (!hasUserId)
|
|
87
|
+
nextIdentity.user_id = inferred.id;
|
|
88
|
+
if (!hasUserName)
|
|
89
|
+
nextIdentity.user_name = inferred.name;
|
|
90
|
+
config.identity = nextIdentity;
|
|
91
|
+
writeJsonConfig(configPath, config);
|
|
92
|
+
console.log(`ā
Provisioned bikky user identity in ${configPath}`);
|
|
93
|
+
return inferred;
|
|
94
|
+
}
|
|
60
95
|
export async function writeInstallConfig(options = {}) {
|
|
61
96
|
const homeDir = options.homeDir ?? os.homedir();
|
|
62
97
|
const entry = {
|
|
@@ -72,6 +107,9 @@ export async function writeInstallConfig(options = {}) {
|
|
|
72
107
|
if (!registeredWithClaudeCli) {
|
|
73
108
|
writeClaudeCodeUserConfig(homeDir, entry);
|
|
74
109
|
}
|
|
110
|
+
if (options.provisionIdentity !== false) {
|
|
111
|
+
provisionUserIdentityConfig(options);
|
|
112
|
+
}
|
|
75
113
|
console.log("\nš§ bikky is now registered. Restart your editor to activate.");
|
|
76
114
|
}
|
|
77
115
|
//# sourceMappingURL=install.js.map
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import "./providers/index.js";
|
|
20
20
|
import { getEmbeddingProvider } from "./registry.js";
|
|
21
21
|
import { EmbeddingDimensionMismatchError } from "../errors.js";
|
|
22
|
+
import { firstNonEmptyString } from "../util.js";
|
|
22
23
|
export { registerEmbeddingProvider, getEmbeddingProvider, listEmbeddingProviders, } from "./registry.js";
|
|
23
24
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
24
25
|
const DEFAULT_RETRIES = 2;
|
|
@@ -32,7 +33,7 @@ export function initEmbedding(input) {
|
|
|
32
33
|
provider: provider.name,
|
|
33
34
|
model: input.model ?? provider.defaults.model,
|
|
34
35
|
dimensions: input.dimensions ?? provider.defaults.dimensions,
|
|
35
|
-
baseUrl: (input.baseUrl
|
|
36
|
+
baseUrl: (firstNonEmptyString(input.baseUrl, provider.defaults.baseUrl) ?? "").replace(/\/+$/, ""),
|
|
36
37
|
apiKey: input.apiKey ?? null,
|
|
37
38
|
extra: input.extra ?? {},
|
|
38
39
|
timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
@@ -10,12 +10,18 @@ export const openaiEmbeddingProvider = {
|
|
|
10
10
|
browserCompatible: true,
|
|
11
11
|
defaults: {
|
|
12
12
|
model: "text-embedding-3-small",
|
|
13
|
-
dimensions:
|
|
13
|
+
dimensions: 1024,
|
|
14
14
|
baseUrl: "https://api.openai.com",
|
|
15
15
|
},
|
|
16
16
|
async embed(text, cfg) {
|
|
17
17
|
if (!cfg.apiKey)
|
|
18
18
|
throw new Error("Embedding failed [openai]: api key not configured");
|
|
19
|
+
const body = { model: cfg.model, input: text };
|
|
20
|
+
// The `dimensions` parameter (Matryoshka truncation) is only supported by
|
|
21
|
+
// the text-embedding-3-* family. Older models (e.g. ada-002) reject it.
|
|
22
|
+
if (cfg.dimensions && /text-embedding-3/.test(cfg.model)) {
|
|
23
|
+
body.dimensions = cfg.dimensions;
|
|
24
|
+
}
|
|
19
25
|
const resp = await resilientFetch({
|
|
20
26
|
url: `${cfg.baseUrl}/v1/embeddings`,
|
|
21
27
|
init: {
|
|
@@ -24,7 +30,7 @@ export const openaiEmbeddingProvider = {
|
|
|
24
30
|
"Content-Type": "application/json",
|
|
25
31
|
Authorization: `Bearer ${cfg.apiKey}`,
|
|
26
32
|
},
|
|
27
|
-
body: JSON.stringify(
|
|
33
|
+
body: JSON.stringify(body),
|
|
28
34
|
},
|
|
29
35
|
timeoutMs: cfg.timeoutMs,
|
|
30
36
|
retries: cfg.retries,
|
|
@@ -19,7 +19,7 @@ export const portkeyEmbeddingProvider = {
|
|
|
19
19
|
browserCompatible: true,
|
|
20
20
|
defaults: {
|
|
21
21
|
model: "@openai/text-embedding-3-small",
|
|
22
|
-
dimensions:
|
|
22
|
+
dimensions: 1024,
|
|
23
23
|
baseUrl: "https://api.portkey.ai",
|
|
24
24
|
},
|
|
25
25
|
async embed(text, cfg) {
|
|
@@ -33,12 +33,19 @@ export const portkeyEmbeddingProvider = {
|
|
|
33
33
|
headers["x-portkey-virtual-key"] = cfg.extra.virtual_key;
|
|
34
34
|
if (cfg.extra.config_id)
|
|
35
35
|
headers["x-portkey-config"] = cfg.extra.config_id;
|
|
36
|
+
const body = { model: cfg.model, input: text };
|
|
37
|
+
// The `dimensions` parameter (Matryoshka truncation) is supported by the
|
|
38
|
+
// OpenAI text-embedding-3-* family. Forward it when the model name matches
|
|
39
|
+
// so users get the configured vector size instead of the provider native.
|
|
40
|
+
if (cfg.dimensions && /text-embedding-3/.test(cfg.model)) {
|
|
41
|
+
body.dimensions = cfg.dimensions;
|
|
42
|
+
}
|
|
36
43
|
const resp = await resilientFetch({
|
|
37
44
|
url: `${cfg.baseUrl}/v1/embeddings`,
|
|
38
45
|
init: {
|
|
39
46
|
method: "POST",
|
|
40
47
|
headers,
|
|
41
|
-
body: JSON.stringify(
|
|
48
|
+
body: JSON.stringify(body),
|
|
42
49
|
},
|
|
43
50
|
timeoutMs: cfg.timeoutMs,
|
|
44
51
|
retries: cfg.retries,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import "./providers/index.js";
|
|
14
14
|
import { getInferenceProvider, listInferenceProviders, registerInferenceProvider, } from "./registry.js";
|
|
15
|
+
import { firstNonEmptyString } from "../util.js";
|
|
15
16
|
import { estimateTokens, writeTelemetry } from "../telemetry.js";
|
|
16
17
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
17
18
|
const DEFAULT_RETRIES = 2;
|
|
@@ -27,7 +28,7 @@ export function initLLM(opts) {
|
|
|
27
28
|
resolved = {
|
|
28
29
|
provider: provider.name,
|
|
29
30
|
model: opts.config.model ?? provider.defaults.model,
|
|
30
|
-
baseUrl: (opts.config.baseUrl
|
|
31
|
+
baseUrl: (firstNonEmptyString(opts.config.baseUrl, provider.defaults.baseUrl) ?? "").replace(/\/+$/, ""),
|
|
31
32
|
apiKey: opts.config.apiKey ?? null,
|
|
32
33
|
fallback: opts.config.fallback ?? null,
|
|
33
34
|
extra: opts.config.extra ?? {},
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny helpers shared by the embedding and inference layers.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Returns the first argument that is a non-empty trimmed string, or undefined.
|
|
6
|
+
*
|
|
7
|
+
* Useful when a value may come from a config layer that normalises absent
|
|
8
|
+
* fields to `""` instead of leaving them undefined ā `??` alone won't fall
|
|
9
|
+
* through `""`, but this will. See issue #131.
|
|
10
|
+
*/
|
|
11
|
+
export declare function firstNonEmptyString(...candidates: Array<string | undefined | null>): string | undefined;
|
|
12
|
+
//# sourceMappingURL=util.d.ts.map
|
package/dist/llm/util.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny helpers shared by the embedding and inference layers.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Returns the first argument that is a non-empty trimmed string, or undefined.
|
|
6
|
+
*
|
|
7
|
+
* Useful when a value may come from a config layer that normalises absent
|
|
8
|
+
* fields to `""` instead of leaving them undefined ā `??` alone won't fall
|
|
9
|
+
* through `""`, but this will. See issue #131.
|
|
10
|
+
*/
|
|
11
|
+
export function firstNonEmptyString(...candidates) {
|
|
12
|
+
for (const c of candidates) {
|
|
13
|
+
if (typeof c === "string" && c.trim().length > 0)
|
|
14
|
+
return c;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=util.js.map
|