wendkeep 0.72.0 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -3
- package/README.en.md +28 -11
- package/README.md +28 -11
- package/docs/en/commands/changes-and-verification.md +10 -5
- package/docs/en/commands/maintenance-and-diagnostics.md +17 -9
- package/docs/en/commands/observer.md +18 -12
- package/docs/en/commands/operating-profiles.md +28 -3
- package/docs/en/commands/sessions-and-import.md +4 -4
- package/docs/pt-BR/commands/changes-and-verification.md +10 -5
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -5
- package/docs/pt-BR/commands/observer.md +18 -12
- package/docs/pt-BR/commands/operating-profiles.md +28 -3
- package/docs/pt-BR/commands/sessions-and-import.md +4 -4
- package/hooks/brain-inject.mjs +6 -6
- package/hooks/change-context.mjs +11 -0
- package/hooks/change-core.mjs +53 -21
- package/hooks/change-warn.mjs +2 -0
- package/hooks/harness-doctor.mjs +13 -5
- package/hooks/understand-inject.mjs +1 -1
- package/hooks/vault-health.mjs +2 -2
- package/package.json +5 -4
- package/packages/cli/src/index.mjs +12 -2
- package/packages/integrations/src/host-hooks.mjs +1 -1
- package/packages/vault/src/memory-store.mjs +17 -4
- package/src/change.mjs +10 -4
- package/src/delivery.mjs +303 -0
- package/src/doctor.mjs +47 -10
- package/src/init.mjs +2 -2
- package/src/observer-auth.mjs +10 -0
- package/src/observer-memory-publish.mjs +13 -8
- package/src/observer-privacy.mjs +23 -0
- package/src/observer-publish.mjs +10 -7
- package/src/observer-server.mjs +51 -0
- package/src/observer-sql-publish.mjs +72 -31
- package/src/observer-sql-store.mjs +33 -2
- package/src/observer.mjs +10 -3
- package/src/release-changelog.mjs +1 -1
- package/src/release-provenance.mjs +115 -0
- package/src/skills-seed.mjs +25 -9
- package/src/sync-defs.mjs +5 -2
- package/src/sync.mjs +2 -2
- package/src/taxonomy.mjs +1 -1
- package/src/vault-readme.mjs +2 -2
- package/src/work-kind.mjs +62 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
|
|
3
|
+
const TRANSCRIPT_PATH_KEY = /^(?:transcript_path|agent_transcript_path|transcriptPath|agentTranscriptPath)$/i;
|
|
4
|
+
const TRANSCRIPT_PATH_LINE = /^(\s*["']?(?:transcript_path|agent_transcript_path|transcriptPath|agentTranscriptPath)["']?\s*:\s*)(["']?)(.*?)(\2)(\s*,?\s*)$/gmi;
|
|
5
|
+
|
|
6
|
+
function sourceLabel(value) {
|
|
7
|
+
return basename(String(value || '').replaceAll('\\', '/'));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function sanitizeObserverContent(content) {
|
|
11
|
+
return String(content || '').replace(TRANSCRIPT_PATH_LINE, (_line, prefix, quote, value, _closing, suffix) => (
|
|
12
|
+
`${prefix}${quote}${sourceLabel(value)}${quote}${suffix}`
|
|
13
|
+
));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function sanitizeObserverMetadata(value) {
|
|
17
|
+
if (Array.isArray(value)) return value.map(sanitizeObserverMetadata);
|
|
18
|
+
if (!value || typeof value !== 'object') return value;
|
|
19
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
20
|
+
key,
|
|
21
|
+
TRANSCRIPT_PATH_KEY.test(key) ? sourceLabel(item) : sanitizeObserverMetadata(item),
|
|
22
|
+
]));
|
|
23
|
+
}
|
package/src/observer-publish.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSyn
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { buildProjectSnapshot } from './observer-snapshot.mjs';
|
|
4
4
|
import { publishObserverSql } from './observer-sql-publish.mjs';
|
|
5
|
+
import { observerAuthHeaders, resolveObserverToken } from './observer-auth.mjs';
|
|
5
6
|
|
|
6
7
|
const OUTBOX_REL = join('.brain', 'observer-outbox');
|
|
7
8
|
const REQUEST_TIMEOUT_MS = 500;
|
|
@@ -46,15 +47,15 @@ function removeOutbox(vaultBase, eventId) {
|
|
|
46
47
|
if (existsSync(path)) unlinkSync(path);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
async function postSnapshot(url, event) {
|
|
50
|
+
async function postSnapshot(url, event, token) {
|
|
50
51
|
const controller = new AbortController();
|
|
51
52
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
52
53
|
try {
|
|
53
54
|
const response = await fetch(`${String(url).replace(/\/$/, '')}/v1/projects/${encodeURIComponent(event.project_id)}/snapshot`, {
|
|
54
55
|
method: 'POST',
|
|
55
|
-
headers: {
|
|
56
|
+
headers: observerAuthHeaders(token, {
|
|
56
57
|
'content-type': 'application/json',
|
|
57
|
-
},
|
|
58
|
+
}),
|
|
58
59
|
body: JSON.stringify(event),
|
|
59
60
|
signal: controller.signal,
|
|
60
61
|
});
|
|
@@ -70,14 +71,14 @@ async function postSnapshot(url, event) {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
export async function retryObserverOutbox({ vaultBase, url } = {}) {
|
|
74
|
+
export async function retryObserverOutbox({ vaultBase, url, token = process.env.WENDKEEP_OBSERVER_TOKEN || '' } = {}) {
|
|
74
75
|
if (!url) return { attempted: 0, confirmed: 0, pending: listOutbox(vaultBase).length };
|
|
75
76
|
let attempted = 0;
|
|
76
77
|
let confirmed = 0;
|
|
77
78
|
for (const event of listOutbox(vaultBase)) {
|
|
78
79
|
attempted += 1;
|
|
79
80
|
try {
|
|
80
|
-
await postSnapshot(url, event);
|
|
81
|
+
await postSnapshot(url, event, token);
|
|
81
82
|
removeOutbox(vaultBase, event.event_id);
|
|
82
83
|
confirmed += 1;
|
|
83
84
|
} catch { /* preserve the event for a later retry */ }
|
|
@@ -91,6 +92,7 @@ export async function publishObserverSnapshot({
|
|
|
91
92
|
url = process.env.WENDKEEP_OBSERVER_URL || '',
|
|
92
93
|
now = new Date(),
|
|
93
94
|
input = {},
|
|
95
|
+
token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
|
|
94
96
|
} = {}) {
|
|
95
97
|
try {
|
|
96
98
|
const event = buildProjectSnapshot({ vaultBase, projectRoot, now });
|
|
@@ -100,15 +102,16 @@ export async function publishObserverSnapshot({
|
|
|
100
102
|
url,
|
|
101
103
|
input,
|
|
102
104
|
now,
|
|
105
|
+
token,
|
|
103
106
|
});
|
|
104
107
|
if (!url) return { ok: sql.ok, skipped: true, queued: sql.queued, hookExitCode: 0, event_id: event.event_id, sql };
|
|
105
108
|
|
|
106
|
-
await retryObserverOutbox({ vaultBase, url });
|
|
109
|
+
await retryObserverOutbox({ vaultBase, url, token });
|
|
107
110
|
// SQL is the live authority. Keep the legacy-shaped `memory` field for
|
|
108
111
|
// older integrations while reporting the real SQL publication separately.
|
|
109
112
|
const memory = { ok: sql.ok, queued: sql.queued, changed: sql.changed, pending: sql.pending, authority: 'sqlite' };
|
|
110
113
|
try {
|
|
111
|
-
const response = await postSnapshot(url, event);
|
|
114
|
+
const response = await postSnapshot(url, event, resolveObserverToken(token));
|
|
112
115
|
return {
|
|
113
116
|
ok: true,
|
|
114
117
|
queued: false,
|
package/src/observer-server.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
3
|
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { gunzipSync } from 'node:zlib';
|
|
@@ -52,6 +53,39 @@ function loopbackOnly(host) {
|
|
|
52
53
|
return LOOPBACK_HOSTS.has(String(host || '').toLowerCase());
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
function safeTokenEqual(actual, expected) {
|
|
57
|
+
if (!actual || !expected) return false;
|
|
58
|
+
const left = createHash('sha256').update(String(actual)).digest();
|
|
59
|
+
const right = createHash('sha256').update(String(expected)).digest();
|
|
60
|
+
return timingSafeEqual(left, right);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function bearerToken(req) {
|
|
64
|
+
const match = String(req.headers.authorization || '').match(/^Bearer\s+(.+)$/i);
|
|
65
|
+
return match?.[1] || '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function requestHostname(value) {
|
|
69
|
+
try { return new URL(`http://${String(value || '')}`).hostname.toLowerCase(); }
|
|
70
|
+
catch { return ''; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateAuthority(req, { loopback }) {
|
|
74
|
+
const hostname = requestHostname(req.headers.host);
|
|
75
|
+
if (!hostname || (loopback && !LOOPBACK_HOSTS.has(hostname))) {
|
|
76
|
+
return { ok: false, status: 421, code: 'invalid_host', message: 'Host não corresponde ao binding do Observer.' };
|
|
77
|
+
}
|
|
78
|
+
const origin = String(req.headers.origin || '');
|
|
79
|
+
if (origin) {
|
|
80
|
+
let originHostname = '';
|
|
81
|
+
try { originHostname = new URL(origin).hostname.toLowerCase(); } catch { /* invalid below */ }
|
|
82
|
+
if (!originHostname || originHostname !== hostname) {
|
|
83
|
+
return { ok: false, status: 403, code: 'invalid_origin', message: 'Origin não corresponde ao Host do Observer.' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { ok: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
55
89
|
function json(res, status, body) {
|
|
56
90
|
const content = JSON.stringify(body);
|
|
57
91
|
res.writeHead(status, {
|
|
@@ -184,10 +218,16 @@ export async function startObserverServer({
|
|
|
184
218
|
port = 8787,
|
|
185
219
|
dataDir,
|
|
186
220
|
allowNonLoopback = false,
|
|
221
|
+
token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
|
|
187
222
|
} = {}) {
|
|
188
223
|
if (!loopbackOnly(host) && !allowNonLoopback) {
|
|
189
224
|
throw new Error(`Observer HTTP aceita somente host loopback; recebido: ${host}`);
|
|
190
225
|
}
|
|
226
|
+
if (!loopbackOnly(host) && !token) {
|
|
227
|
+
const error = new Error('Observer non-loopback exige --token ou WENDKEEP_OBSERVER_TOKEN.');
|
|
228
|
+
error.code = 'WENDKEEP_OBSERVER_TOKEN_REQUIRED';
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
191
231
|
if (!dataDir) throw new Error('dataDir é obrigatório.');
|
|
192
232
|
const sqlDb = ensureObserverDatabase(dataDir);
|
|
193
233
|
const databaseMigration = migrateObserverDatabase(sqlDb);
|
|
@@ -204,6 +244,17 @@ export async function startObserverServer({
|
|
|
204
244
|
const server = createServer(async (req, res) => {
|
|
205
245
|
try {
|
|
206
246
|
const pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
|
|
247
|
+
const authority = validateAuthority(req, { loopback: loopbackOnly(host) });
|
|
248
|
+
if (!authority.ok) {
|
|
249
|
+
errorResponse(res, authority.status, authority.code, authority.message);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const authenticated = safeTokenEqual(bearerToken(req), token);
|
|
253
|
+
const mutating = !['GET', 'HEAD', 'OPTIONS'].includes(String(req.method || '').toUpperCase());
|
|
254
|
+
if ((mutating || !loopbackOnly(host)) && !authenticated) {
|
|
255
|
+
errorResponse(res, 401, 'observer_auth_required', 'Bearer token válido é obrigatório para esta operação.');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
207
258
|
if (req.method === 'GET' && pathname === '/healthz') {
|
|
208
259
|
json(res, 200, {
|
|
209
260
|
ok: true,
|
|
@@ -13,6 +13,8 @@ import { gzipSync } from 'node:zlib';
|
|
|
13
13
|
import { parseTranscriptContent } from '../packages/integrations/src/transcripts.mjs';
|
|
14
14
|
import { parseSessionCost, } from './cost.mjs';
|
|
15
15
|
import { buildSessionIdentityMap, listMigrationDocuments, parseFrontmatter, sessionEvents } from './observer-sql-migrate.mjs';
|
|
16
|
+
import { observerAuthHeaders } from './observer-auth.mjs';
|
|
17
|
+
import { sanitizeObserverContent, sanitizeObserverMetadata } from './observer-privacy.mjs';
|
|
16
18
|
|
|
17
19
|
export const SQL_OUTBOX_REL = '.brain/observer-sql-outbox';
|
|
18
20
|
export const SQL_STATE_REL = '.brain/observer-sql-state.json';
|
|
@@ -20,6 +22,28 @@ const SQL_SCHEMA_VERSION = 1;
|
|
|
20
22
|
export const SQL_EVENT_BATCH_SIZE = 64;
|
|
21
23
|
export const SQL_EVENT_BATCH_BYTES = 8 * 1024 * 1024;
|
|
22
24
|
const REQUEST_TIMEOUT_MS = 15000;
|
|
25
|
+
const MAX_REQUEST_TIMEOUT_MS = 120000;
|
|
26
|
+
const REQUEST_TIMEOUT_BYTES_STEP = 1024 * 1024;
|
|
27
|
+
const CAPTURE_LEVELS = new Set(['metadata', 'messages', 'full-transcript']);
|
|
28
|
+
|
|
29
|
+
export function observerSqlRequestTimeoutMs(rawBytes) {
|
|
30
|
+
const size = Math.max(0, Number(rawBytes) || 0);
|
|
31
|
+
const oversizedBytes = Math.max(0, size - SQL_EVENT_BATCH_BYTES);
|
|
32
|
+
return Math.min(
|
|
33
|
+
MAX_REQUEST_TIMEOUT_MS,
|
|
34
|
+
REQUEST_TIMEOUT_MS + Math.ceil(oversizedBytes / REQUEST_TIMEOUT_BYTES_STEP) * 1000,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeObserverCaptureLevel(value = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata') {
|
|
39
|
+
const level = String(value || 'metadata').trim().toLowerCase();
|
|
40
|
+
if (!CAPTURE_LEVELS.has(level)) {
|
|
41
|
+
const error = new Error(`Nível de captura inválido: ${level}. Use metadata, messages ou full-transcript.`);
|
|
42
|
+
error.code = 'WENDKEEP_OBSERVER_CAPTURE_LEVEL_INVALID';
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
return level;
|
|
46
|
+
}
|
|
23
47
|
|
|
24
48
|
function text(value, fallback = '') { return String(value ?? fallback); }
|
|
25
49
|
function hash(value) { return createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value)).digest('hex'); }
|
|
@@ -73,7 +97,9 @@ function tokenPayload(usage = {}) {
|
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
function documentEvent({ projectId, logicalPath, content, metadata, revision, occurredAt }) {
|
|
76
|
-
const
|
|
100
|
+
const safeContent = sanitizeObserverContent(content);
|
|
101
|
+
const safeMetadata = sanitizeObserverMetadata(metadata);
|
|
102
|
+
const contentHash = hash(safeContent);
|
|
77
103
|
return {
|
|
78
104
|
schema_version: 1,
|
|
79
105
|
event_id: eventId('document', projectId, `${logicalPath}:${revision}:${contentHash}`),
|
|
@@ -89,11 +115,11 @@ function documentEvent({ projectId, logicalPath, content, metadata, revision, oc
|
|
|
89
115
|
: logicalPath.startsWith('07-Specs/') ? 'spec'
|
|
90
116
|
: logicalPath.startsWith('08-Mudanças/') ? 'change' : 'memory',
|
|
91
117
|
title: basename(logicalPath).replace(/\.md$/i, ''),
|
|
92
|
-
content,
|
|
118
|
+
content: safeContent,
|
|
93
119
|
content_hash: contentHash,
|
|
94
120
|
revision,
|
|
95
|
-
metadata,
|
|
96
|
-
source_session_id: text(
|
|
121
|
+
metadata: safeMetadata,
|
|
122
|
+
source_session_id: text(safeMetadata?.session_id),
|
|
97
123
|
},
|
|
98
124
|
};
|
|
99
125
|
}
|
|
@@ -123,7 +149,7 @@ function agentEvent({ projectId, sessionId, agentId, parentAgentId = '', role =
|
|
|
123
149
|
};
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
function transcriptCalls({ projectId, sessionId, agentId, role, provider, modelFallback, transcriptId, content, occurredAt }) {
|
|
152
|
+
function transcriptCalls({ projectId, sessionId, agentId, role, provider, modelFallback, transcriptId, content, occurredAt, includeMessages }) {
|
|
127
153
|
let parsed;
|
|
128
154
|
try { parsed = parseTranscriptContent(content); } catch { return []; }
|
|
129
155
|
return (parsed.turns || []).flatMap((turn, index) => {
|
|
@@ -154,8 +180,8 @@ function transcriptCalls({ projectId, sessionId, agentId, role, provider, modelF
|
|
|
154
180
|
cost_usd: 0,
|
|
155
181
|
cost_status: 'unknown',
|
|
156
182
|
transcript_id: transcriptId,
|
|
157
|
-
prompt_text: prompt,
|
|
158
|
-
response_text: response,
|
|
183
|
+
prompt_text: includeMessages ? prompt : '',
|
|
184
|
+
response_text: includeMessages ? response : '',
|
|
159
185
|
status: turn.status === 'aborted' ? 'aborted' : 'complete',
|
|
160
186
|
metadata: { tools: turn.tools || [], source: 'transcript-parser' },
|
|
161
187
|
},
|
|
@@ -180,7 +206,7 @@ function sourceCandidates({ vaultBase, logicalPath, fm, input }) {
|
|
|
180
206
|
return candidates;
|
|
181
207
|
}
|
|
182
208
|
|
|
183
|
-
function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider, model, source, now }) {
|
|
209
|
+
function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider, model, source, now, captureLevel }) {
|
|
184
210
|
const content = readFileSync(source.path, 'utf8');
|
|
185
211
|
const agentId = source.role === 'subagent'
|
|
186
212
|
? `${projectId}:${sessionId}:subagent:${hash(source.path).slice(0, 16)}`
|
|
@@ -189,7 +215,7 @@ function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider,
|
|
|
189
215
|
? agentEvent({ projectId, sessionId, agentId, parentAgentId: mainAgentId, role: 'subagent', provider, model, input: source.agentInput, occurredAt: now })
|
|
190
216
|
: null;
|
|
191
217
|
const fingerprint = hash(content);
|
|
192
|
-
const transcript = {
|
|
218
|
+
const transcript = captureLevel === 'full-transcript' ? {
|
|
193
219
|
schema_version: 1,
|
|
194
220
|
event_id: eventId('transcript', projectId, `${source.transcriptId}:${fingerprint}`),
|
|
195
221
|
kind: 'transcript.upsert',
|
|
@@ -202,10 +228,21 @@ function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider,
|
|
|
202
228
|
coverage: 'complete',
|
|
203
229
|
content,
|
|
204
230
|
source: 'hook-transcript',
|
|
205
|
-
metadata: {
|
|
231
|
+
metadata: { source_label: basename(source.path), source_hash: hash(normalizePath(source.path)) },
|
|
206
232
|
},
|
|
207
|
-
};
|
|
208
|
-
const calls = transcriptCalls({
|
|
233
|
+
} : null;
|
|
234
|
+
const calls = transcriptCalls({
|
|
235
|
+
projectId,
|
|
236
|
+
sessionId,
|
|
237
|
+
agentId,
|
|
238
|
+
role: source.role,
|
|
239
|
+
provider,
|
|
240
|
+
modelFallback: model,
|
|
241
|
+
transcriptId: source.transcriptId,
|
|
242
|
+
content,
|
|
243
|
+
occurredAt: now,
|
|
244
|
+
includeMessages: captureLevel !== 'metadata',
|
|
245
|
+
});
|
|
209
246
|
return { events: [agent, transcript, ...calls].filter(Boolean), fingerprint, transcriptId: source.transcriptId };
|
|
210
247
|
}
|
|
211
248
|
|
|
@@ -218,9 +255,10 @@ function dedupeEvents(events) {
|
|
|
218
255
|
});
|
|
219
256
|
}
|
|
220
257
|
|
|
221
|
-
export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, now = new Date(), state = readState(vaultBase), remoteDocuments = {} } = {}) {
|
|
258
|
+
export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, now = new Date(), state = readState(vaultBase), remoteDocuments = {}, captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata' } = {}) {
|
|
222
259
|
if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
|
|
223
260
|
const occurredAt = isoNow(now);
|
|
261
|
+
const resolvedCaptureLevel = normalizeObserverCaptureLevel(captureLevel);
|
|
224
262
|
const nextState = { schema_version: SQL_SCHEMA_VERSION, files: { ...(state.files || {}) }, transcripts: { ...(state.transcripts || {}) } };
|
|
225
263
|
const events = [];
|
|
226
264
|
const sessionContexts = [];
|
|
@@ -235,7 +273,7 @@ export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, n
|
|
|
235
273
|
let changed = 0;
|
|
236
274
|
for (const file of files) {
|
|
237
275
|
const content = readFileSync(file.absolute, 'utf8');
|
|
238
|
-
const contentHash = hash(content);
|
|
276
|
+
const contentHash = hash(sanitizeObserverContent(content));
|
|
239
277
|
const previous = state.files?.[file.logicalPath];
|
|
240
278
|
const remote = remoteDocuments?.[file.logicalPath];
|
|
241
279
|
const baseRevision = Math.max(Number(previous?.revision || 0), Number(remote?.revision || 0));
|
|
@@ -263,9 +301,10 @@ export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, n
|
|
|
263
301
|
for (const source of sources) {
|
|
264
302
|
const content = readFileSync(source.path, 'utf8');
|
|
265
303
|
const fingerprint = hash(content);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
304
|
+
const previousTranscript = state.transcripts?.[source.transcriptId];
|
|
305
|
+
if (previousTranscript?.content_hash === fingerprint && previousTranscript?.coverage === resolvedCaptureLevel) continue;
|
|
306
|
+
const complete = completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider, model, source, now: occurredAt, captureLevel: resolvedCaptureLevel });
|
|
307
|
+
nextState.transcripts[source.transcriptId] = { content_hash: complete.fingerprint, coverage: resolvedCaptureLevel };
|
|
269
308
|
const summaryId = complete.transcriptId;
|
|
270
309
|
for (const event of complete.events) {
|
|
271
310
|
if (event.kind === 'agent.upsert' && event.payload.agent_id !== mainAgentId) events.push(event);
|
|
@@ -297,14 +336,15 @@ export function listSqlOutbox(vaultBase) {
|
|
|
297
336
|
});
|
|
298
337
|
}
|
|
299
338
|
|
|
300
|
-
async function postSqlChunk({ url, projectId, events, fetchImpl = globalThis.fetch }) {
|
|
339
|
+
async function postSqlChunk({ url, projectId, events, fetchImpl = globalThis.fetch, token = '' }) {
|
|
301
340
|
const controller = new AbortController();
|
|
302
|
-
const
|
|
303
|
-
const
|
|
341
|
+
const rawBody = Buffer.from(JSON.stringify({ events }), 'utf8');
|
|
342
|
+
const timer = setTimeout(() => controller.abort(), observerSqlRequestTimeoutMs(rawBody.byteLength));
|
|
343
|
+
const wireBody = gzipSync(rawBody);
|
|
304
344
|
try {
|
|
305
345
|
const response = await fetchImpl(`${String(url).replace(/\/$/, '')}/v1/projects/${encodeURIComponent(projectId)}/ingest`, {
|
|
306
346
|
method: 'POST',
|
|
307
|
-
headers: { 'content-type': 'application/json', 'content-encoding': 'gzip', accept: 'application/json' },
|
|
347
|
+
headers: observerAuthHeaders(token, { 'content-type': 'application/json', 'content-encoding': 'gzip', accept: 'application/json' }),
|
|
308
348
|
body: wireBody,
|
|
309
349
|
signal: controller.signal,
|
|
310
350
|
});
|
|
@@ -314,11 +354,11 @@ async function postSqlChunk({ url, projectId, events, fetchImpl = globalThis.fet
|
|
|
314
354
|
} finally { clearTimeout(timer); }
|
|
315
355
|
}
|
|
316
356
|
|
|
317
|
-
async function readRemoteDocuments({ url, projectId, fetchImpl = globalThis.fetch }) {
|
|
357
|
+
async function readRemoteDocuments({ url, projectId, fetchImpl = globalThis.fetch, token = '' }) {
|
|
318
358
|
if (!url) return {};
|
|
319
359
|
try {
|
|
320
360
|
const response = await fetchImpl(`${String(url).replace(/\/$/, '')}/v1/projects/${encodeURIComponent(projectId)}/memory/tree`, {
|
|
321
|
-
headers: { accept: 'application/json' },
|
|
361
|
+
headers: observerAuthHeaders(token, { accept: 'application/json' }),
|
|
322
362
|
});
|
|
323
363
|
if (!response.ok) return {};
|
|
324
364
|
const body = await response.json().catch(() => ({}));
|
|
@@ -328,7 +368,7 @@ async function readRemoteDocuments({ url, projectId, fetchImpl = globalThis.fetc
|
|
|
328
368
|
}
|
|
329
369
|
}
|
|
330
370
|
|
|
331
|
-
async function postSqlBatch({ url, projectId, events, fetchImpl = globalThis.fetch }) {
|
|
371
|
+
async function postSqlBatch({ url, projectId, events, fetchImpl = globalThis.fetch, token = '' }) {
|
|
332
372
|
const aggregate = { accepted: 0, rejected: 0, conflicts: 0, stale: 0, duplicates: 0 };
|
|
333
373
|
let chunk = [];
|
|
334
374
|
let chunkBytes = 0;
|
|
@@ -339,6 +379,7 @@ async function postSqlBatch({ url, projectId, events, fetchImpl = globalThis.fet
|
|
|
339
379
|
projectId,
|
|
340
380
|
events: items,
|
|
341
381
|
fetchImpl,
|
|
382
|
+
token,
|
|
342
383
|
});
|
|
343
384
|
for (const key of Object.keys(aggregate)) aggregate[key] += Number(response?.[key]) || 0;
|
|
344
385
|
};
|
|
@@ -358,7 +399,7 @@ async function postSqlBatch({ url, projectId, events, fetchImpl = globalThis.fet
|
|
|
358
399
|
return aggregate;
|
|
359
400
|
}
|
|
360
401
|
|
|
361
|
-
export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl = globalThis.fetch } = {}) {
|
|
402
|
+
export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '' } = {}) {
|
|
362
403
|
const pending = listSqlOutbox(vaultBase);
|
|
363
404
|
if (!url) return { attempted: 0, confirmed: 0, pending: pending.length };
|
|
364
405
|
let attempted = 0;
|
|
@@ -366,7 +407,7 @@ export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchI
|
|
|
366
407
|
for (const batch of pending) {
|
|
367
408
|
attempted += 1;
|
|
368
409
|
try {
|
|
369
|
-
await postSqlBatch({ url, projectId, events: batch.events, fetchImpl });
|
|
410
|
+
await postSqlBatch({ url, projectId, events: batch.events, fetchImpl, token });
|
|
370
411
|
unlinkSync(batch.path);
|
|
371
412
|
confirmed += 1;
|
|
372
413
|
} catch { break; }
|
|
@@ -374,14 +415,14 @@ export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchI
|
|
|
374
415
|
return { attempted, confirmed, pending: listSqlOutbox(vaultBase).length };
|
|
375
416
|
}
|
|
376
417
|
|
|
377
|
-
export async function publishObserverSql({ vaultBase, projectId, url = process.env.WENDKEEP_OBSERVER_URL || '', input = {}, now = new Date(), fetchImpl = globalThis.fetch } = {}) {
|
|
418
|
+
export async function publishObserverSql({ vaultBase, projectId, url = process.env.WENDKEEP_OBSERVER_URL || '', input = {}, now = new Date(), fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '', captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata' } = {}) {
|
|
378
419
|
if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
|
|
379
|
-
const replay = await retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl });
|
|
420
|
+
const replay = await retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl, token });
|
|
380
421
|
const state = readState(vaultBase);
|
|
381
422
|
const remoteDocuments = Object.keys(state.files || {}).length === 0
|
|
382
|
-
? await readRemoteDocuments({ url, projectId, fetchImpl })
|
|
423
|
+
? await readRemoteDocuments({ url, projectId, fetchImpl, token })
|
|
383
424
|
: {};
|
|
384
|
-
const batch = buildObserverSqlEventBatch({ vaultBase, projectId, input, now, state, remoteDocuments });
|
|
425
|
+
const batch = buildObserverSqlEventBatch({ vaultBase, projectId, input, now, state, remoteDocuments, captureLevel });
|
|
385
426
|
atomicJson(statePath(vaultBase), batch.nextState);
|
|
386
427
|
if (!batch.events.length) return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay };
|
|
387
428
|
if (!url) {
|
|
@@ -389,7 +430,7 @@ export async function publishObserverSql({ vaultBase, projectId, url = process.e
|
|
|
389
430
|
return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay, hookExitCode: 0 };
|
|
390
431
|
}
|
|
391
432
|
try {
|
|
392
|
-
const response = await postSqlBatch({ url, projectId, events: batch.events, fetchImpl });
|
|
433
|
+
const response = await postSqlBatch({ url, projectId, events: batch.events, fetchImpl, token });
|
|
393
434
|
return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay, response };
|
|
394
435
|
} catch (error) {
|
|
395
436
|
queueOutbox(vaultBase, { schema_version: SQL_SCHEMA_VERSION, project_id: projectId, events: batch.events });
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
import { join, basename, dirname } from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { DatabaseSync } from 'node:sqlite';
|
|
6
6
|
import { decodeTranscript, encodeTranscript } from './observer-transcript-store.mjs';
|
|
7
7
|
|
|
8
8
|
export const OBSERVER_SQL_FILE = 'observer.sqlite';
|
|
@@ -16,6 +16,36 @@ const EVENT_KINDS = new Set([
|
|
|
16
16
|
'usage.rollup', 'llm_call', 'transcript.upsert',
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
|
+
const OBSERVER_SQL_MINIMUM_NODE = '22.13.0';
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
let DatabaseSync;
|
|
22
|
+
|
|
23
|
+
export function observerSqlRuntimeSupport(version = process.versions.node) {
|
|
24
|
+
const current = String(version || '0.0.0');
|
|
25
|
+
const [major = 0, minor = 0] = current.split('.').map((part) => Number(part) || 0);
|
|
26
|
+
return {
|
|
27
|
+
supported: major > 22 || (major === 22 && minor >= 13),
|
|
28
|
+
minimum: OBSERVER_SQL_MINIMUM_NODE,
|
|
29
|
+
current,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function observerSqlRuntimeError(support = observerSqlRuntimeSupport()) {
|
|
34
|
+
const error = new Error(`Observer SQL requer Node.js >= ${support.minimum}; atual: ${support.current}. O Keep Core continua compatível com Node.js >= 18.`);
|
|
35
|
+
error.code = 'WENDKEEP_OBSERVER_NODE_UNSUPPORTED';
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function observerDatabaseSync() {
|
|
40
|
+
const support = observerSqlRuntimeSupport();
|
|
41
|
+
if (!support.supported) throw observerSqlRuntimeError(support);
|
|
42
|
+
if (!DatabaseSync) {
|
|
43
|
+
try { ({ DatabaseSync } = require('node:sqlite')); }
|
|
44
|
+
catch { throw observerSqlRuntimeError(support); }
|
|
45
|
+
}
|
|
46
|
+
return DatabaseSync;
|
|
47
|
+
}
|
|
48
|
+
|
|
19
49
|
function now() { return new Date().toISOString(); }
|
|
20
50
|
|
|
21
51
|
function text(value, fallback = '') { return String(value ?? fallback); }
|
|
@@ -63,7 +93,8 @@ function migrationFiles() {
|
|
|
63
93
|
export function openObserverDatabase(dataDir) {
|
|
64
94
|
if (!dataDir) throw new Error('dataDir é obrigatório.');
|
|
65
95
|
mkdirSync(dataDir, { recursive: true });
|
|
66
|
-
const
|
|
96
|
+
const SqliteDatabase = observerDatabaseSync();
|
|
97
|
+
const db = new SqliteDatabase(join(dataDir, OBSERVER_SQL_FILE));
|
|
67
98
|
db.exec('PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;');
|
|
68
99
|
return db;
|
|
69
100
|
}
|
package/src/observer.mjs
CHANGED
|
@@ -7,15 +7,17 @@ import { publishObserverSql } from './observer-sql-publish.mjs';
|
|
|
7
7
|
import { startObserverServer } from './observer-server.mjs';
|
|
8
8
|
import { ensureObserverDatabase, migrateObserverDatabase, listSqlProjects, OBSERVER_SQL_FILE, OBSERVER_SQL_SCHEMA_VERSION } from './observer-sql-store.mjs';
|
|
9
9
|
import { resolveProjectVault } from '../packages/vault/src/project-vault.mjs';
|
|
10
|
+
import { observerAuthHeaders, resolveObserverToken } from './observer-auth.mjs';
|
|
10
11
|
|
|
11
12
|
export const OBSERVER_HELP = `wendkeep observer — Observer local multi-projeto
|
|
12
13
|
|
|
13
14
|
Uso:
|
|
14
15
|
wendkeep observer serve [--data-dir P] [--host 127.0.0.1] [--port 8787]
|
|
15
|
-
[--allow-non-loopback]
|
|
16
|
+
[--allow-non-loopback] [--token TOKEN]
|
|
16
17
|
wendkeep observer register --project P [--vault V] [--data-dir D] [--json]
|
|
17
18
|
wendkeep observer publish --project P [--vault V] [--data-dir D] [--json]
|
|
18
|
-
wendkeep observer memory import --project P [--vault V] [--url U] [--
|
|
19
|
+
wendkeep observer memory import --project P [--vault V] [--url U] [--token TOKEN]
|
|
20
|
+
[--capture-level metadata|messages|full-transcript] [--json]
|
|
19
21
|
wendkeep observer status [--data-dir D] [--json]
|
|
20
22
|
|
|
21
23
|
O Observer local pode manter snapshots operacionais e uma cópia completa da memória em volume
|
|
@@ -79,6 +81,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
79
81
|
return 0;
|
|
80
82
|
}
|
|
81
83
|
const dir = dataDir(argv);
|
|
84
|
+
const token = resolveObserverToken(optionValue(argv, '--token'));
|
|
82
85
|
|
|
83
86
|
if (sub === 'status') {
|
|
84
87
|
print({ ...summary(readObserverIndex(dir)), database: databaseSummary(dir) }, asJson, write);
|
|
@@ -92,6 +95,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
92
95
|
host,
|
|
93
96
|
port: Number(optionValue(argv, '--port') || 8787),
|
|
94
97
|
allowNonLoopback: argv.includes('--allow-non-loopback'),
|
|
98
|
+
token,
|
|
95
99
|
});
|
|
96
100
|
const address = server.address();
|
|
97
101
|
process.stdout.write(`wendkeep observer listening: http://${address.address}:${address.port}\n`);
|
|
@@ -106,7 +110,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
106
110
|
const snapshot = buildProjectSnapshot({ vaultBase: vault, projectRoot: root });
|
|
107
111
|
const url = optionValue(argv, '--url') || process.env.WENDKEEP_OBSERVER_URL || '';
|
|
108
112
|
if (!url) throw new Error('observer memory import: --url ou WENDKEEP_OBSERVER_URL é obrigatório.');
|
|
109
|
-
const headers = { 'content-type': 'application/json', accept: 'application/json' };
|
|
113
|
+
const headers = observerAuthHeaders(token, { 'content-type': 'application/json', accept: 'application/json' });
|
|
110
114
|
const registration = await fetch(
|
|
111
115
|
String(url).replace(/\/$/, '') + '/v1/projects/' + encodeURIComponent(snapshot.project_id),
|
|
112
116
|
{
|
|
@@ -124,11 +128,14 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
124
128
|
vaultBase: vault,
|
|
125
129
|
projectId: snapshot.project_id,
|
|
126
130
|
url,
|
|
131
|
+
token,
|
|
132
|
+
captureLevel: optionValue(argv, '--capture-level') || process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata',
|
|
127
133
|
});
|
|
128
134
|
const parity = await compareMemoryParity({
|
|
129
135
|
vaultBase: vault,
|
|
130
136
|
projectId: snapshot.project_id,
|
|
131
137
|
url,
|
|
138
|
+
token,
|
|
132
139
|
});
|
|
133
140
|
const result = {
|
|
134
141
|
ok: sql.ok && parity.missing === 0 && parity.mismatched === 0,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure helper: extract a single version's release notes from a Keep-a-Changelog
|
|
2
|
-
// file. Reused by scripts/release.mjs and .github/workflows/
|
|
2
|
+
// file. Reused by scripts/release.mjs and .github/workflows/auto-tag.yml so the
|
|
3
3
|
// GitHub Release body always matches the committed CHANGELOG.
|
|
4
4
|
|
|
5
5
|
const HEADER_RE = /^##\s*\[([^\]]+)\]\s*[—–-]\s*(.+?)\s*$/;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { cpSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const DEPENDENCY_FIELDS = Object.freeze([
|
|
7
|
+
'dependencies',
|
|
8
|
+
'devDependencies',
|
|
9
|
+
'optionalDependencies',
|
|
10
|
+
'peerDependencies',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export function parsePackIntegrity(raw) {
|
|
14
|
+
const text = String(raw || '');
|
|
15
|
+
const start = text.indexOf('[');
|
|
16
|
+
const end = text.lastIndexOf(']');
|
|
17
|
+
if (start < 0 || end < start) return '';
|
|
18
|
+
try {
|
|
19
|
+
return String(JSON.parse(text.slice(start, end + 1))[0]?.integrity || '');
|
|
20
|
+
} catch {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function packIntegrityInIsolatedCopy(root, { execute = execFileSync } = {}) {
|
|
26
|
+
const tempRoot = mkdtempSync(join(tmpdir(), 'wendkeep-release-pack-'));
|
|
27
|
+
const packageRoot = join(tempRoot, 'package');
|
|
28
|
+
const ignored = new Set(['.git', 'node_modules']);
|
|
29
|
+
try {
|
|
30
|
+
const binding = JSON.parse(readFileSync(join(root, '.wendkeep.json'), 'utf8'));
|
|
31
|
+
const vault = String(binding.vault || '');
|
|
32
|
+
if (vault && !vault.includes('/') && !vault.includes('\\')) ignored.add(vault);
|
|
33
|
+
} catch { /* unbound package: nothing else to exclude */ }
|
|
34
|
+
try {
|
|
35
|
+
cpSync(root, packageRoot, {
|
|
36
|
+
recursive: true,
|
|
37
|
+
filter(source) {
|
|
38
|
+
if (source === root) return true;
|
|
39
|
+
const relativeName = basename(source);
|
|
40
|
+
return !ignored.has(relativeName);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
44
|
+
const raw = execute(command, ['pack', '--dry-run', '--json'], {
|
|
45
|
+
cwd: packageRoot,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
});
|
|
49
|
+
return parsePackIntegrity(raw);
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function packageHasSelfDependency(pkg = {}) {
|
|
56
|
+
const name = String(pkg.name || '');
|
|
57
|
+
if (!name) return false;
|
|
58
|
+
return DEPENDENCY_FIELDS.some((field) => Object.hasOwn(pkg[field] || {}, name));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function evaluateReleaseProvenance({
|
|
62
|
+
name,
|
|
63
|
+
version,
|
|
64
|
+
headCommit,
|
|
65
|
+
tagCommit = '',
|
|
66
|
+
publishedIntegrity = '',
|
|
67
|
+
localIntegrity = '',
|
|
68
|
+
requirePublished = false,
|
|
69
|
+
} = {}) {
|
|
70
|
+
const tag = `v${version}`;
|
|
71
|
+
if (tagCommit && tagCommit !== headCommit) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
code: 'tag_commit_mismatch',
|
|
75
|
+
message: `${tag} aponta para ${tagCommit.slice(0, 7)}, não para ${headCommit.slice(0, 7)}. Bump a versão antes de alterar a árvore publicada.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (publishedIntegrity && !tagCommit) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
code: 'published_tag_missing',
|
|
82
|
+
message: `${name}@${version} está publicado, mas ${tag} não comprova o commit correspondente.`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (requirePublished && !publishedIntegrity) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
code: 'published_artifact_missing',
|
|
89
|
+
message: `${name}@${version} ainda não possui integridade consultável no npm.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (publishedIntegrity && localIntegrity && publishedIntegrity !== localIntegrity) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
code: 'tarball_integrity_mismatch',
|
|
96
|
+
message: `o tarball de ${tag} diverge do artefato publicado no npm.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (publishedIntegrity && !localIntegrity) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
code: 'local_integrity_missing',
|
|
103
|
+
message: `não foi possível calcular a integridade do tarball de ${tag}.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
ok: true,
|
|
108
|
+
code: publishedIntegrity ? 'verified' : 'release_candidate',
|
|
109
|
+
name,
|
|
110
|
+
version,
|
|
111
|
+
tag,
|
|
112
|
+
commit: headCommit,
|
|
113
|
+
integrity: publishedIntegrity || localIntegrity || '',
|
|
114
|
+
};
|
|
115
|
+
}
|