brainclaw 1.8.0 → 1.9.1
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 +592 -505
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +138 -13
- package/dist/commands/add-step.js +1 -1
- package/dist/commands/bootstrap.js +2 -26
- package/dist/commands/check-security-mcp.js +50 -33
- package/dist/commands/check-security.js +86 -43
- package/dist/commands/claim.js +22 -21
- package/dist/commands/confirm.js +26 -0
- package/dist/commands/context-diff.js +1 -1
- package/dist/commands/dispatch-watch.js +142 -0
- package/dist/commands/doctor.js +113 -2
- package/dist/commands/estimation-report.js +115 -16
- package/dist/commands/harvest.js +286 -23
- package/dist/commands/hooks.js +73 -73
- package/dist/commands/init.js +124 -22
- package/dist/commands/install-hooks.js +78 -78
- package/dist/commands/loops-handlers.js +4 -0
- package/dist/commands/mcp-read-handlers.js +253 -41
- package/dist/commands/mcp.js +664 -102
- package/dist/commands/memory.js +21 -17
- package/dist/commands/migrate.js +81 -17
- package/dist/commands/prune.js +78 -4
- package/dist/commands/reflect.js +26 -20
- package/dist/commands/register-agent.js +57 -1
- package/dist/commands/repair.js +20 -0
- package/dist/commands/session-end.js +15 -6
- package/dist/commands/session-start.js +18 -1
- package/dist/commands/setup-security.js +39 -18
- package/dist/commands/setup.js +26 -27
- package/dist/commands/stale.js +16 -2
- package/dist/commands/switch.js +26 -5
- package/dist/commands/uninstall.js +126 -34
- package/dist/commands/update-step.js +6 -0
- package/dist/commands/version.js +1 -1
- package/dist/commands/worktree.js +60 -0
- package/dist/core/actions.js +12 -3
- package/dist/core/agent-capability.js +30 -17
- package/dist/core/agent-files.js +963 -666
- package/dist/core/agent-integrations.js +0 -3
- package/dist/core/agent-inventory.js +67 -0
- package/dist/core/agent-registry.js +163 -29
- package/dist/core/agentrun-reconciler.js +33 -2
- package/dist/core/agentruns.js +7 -1
- package/dist/core/ai-agent-detection.js +31 -44
- package/dist/core/archival.js +15 -9
- package/dist/core/assignment-reconciler.js +56 -0
- package/dist/core/assignment-sweeper.js +127 -4
- package/dist/core/assignments.js +69 -11
- package/dist/core/bootstrap.js +233 -67
- package/dist/core/brainclaw-version.js +22 -0
- package/dist/core/candidates.js +21 -1
- package/dist/core/claims.js +313 -150
- package/dist/core/codev-prompts.js +38 -38
- package/dist/core/config.js +6 -1
- package/dist/core/context-diff.js +148 -20
- package/dist/core/context.js +129 -8
- package/dist/core/coordination.js +22 -3
- package/dist/core/default-profiles/doctor.yaml +11 -11
- package/dist/core/default-profiles/janitor.yaml +11 -11
- package/dist/core/default-profiles/onboarder.yaml +11 -11
- package/dist/core/default-profiles/reviewer.yaml +13 -13
- package/dist/core/dispatch-status.js +79 -5
- package/dist/core/dispatcher.js +65 -12
- package/dist/core/entity-operations.js +74 -27
- package/dist/core/entity-registry.js +31 -5
- package/dist/core/event-log.js +138 -21
- package/dist/core/events/checkpoint.js +258 -0
- package/dist/core/events/genesis.js +220 -0
- package/dist/core/events/journal.js +507 -0
- package/dist/core/events/materialize.js +126 -0
- package/dist/core/events/registry-post-image.js +110 -0
- package/dist/core/events/verify.js +109 -0
- package/dist/core/execution-adapters.js +23 -0
- package/dist/core/execution.js +1 -1
- package/dist/core/facade-schema.js +38 -0
- package/dist/core/gc-semantic.js +130 -5
- package/dist/core/handoff-snapshot.js +68 -0
- package/dist/core/ids.js +19 -8
- package/dist/core/instruction-templates.js +34 -115
- package/dist/core/io.js +39 -3
- package/dist/core/json-store.js +10 -1
- package/dist/core/lock.js +153 -28
- package/dist/core/loops/bootstrap-acquire.js +25 -1
- package/dist/core/loops/facade-schema.js +2 -0
- package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
- package/dist/core/loops/index.js +1 -0
- package/dist/core/loops/presets/bootstrap.js +7 -0
- package/dist/core/loops/store.js +17 -0
- package/dist/core/loops/verbs.js +24 -2
- package/dist/core/markdown.js +8 -76
- package/dist/core/mcp-command-resolution.js +245 -0
- package/dist/core/memory-compactor.js +5 -3
- package/dist/core/memory-lifecycle.js +282 -0
- package/dist/core/merge-risk.js +150 -0
- package/dist/core/messaging.js +10 -3
- package/dist/core/migration.js +11 -1
- package/dist/core/observer-mode.js +26 -0
- package/dist/core/operations/memory-mutation.js +90 -65
- package/dist/core/operations/plan.js +27 -1
- package/dist/core/protocol-skills.js +210 -0
- package/dist/core/reflection-safety.js +6 -7
- package/dist/core/reputation.js +84 -2
- package/dist/core/runtime-signals.js +72 -10
- package/dist/core/runtime.js +84 -1
- package/dist/core/schema.js +114 -0
- package/dist/core/search.js +19 -2
- package/dist/core/security-detectors.js +125 -0
- package/dist/core/security-extract.js +189 -0
- package/dist/core/security-guard.js +217 -139
- package/dist/core/security-packages.js +121 -0
- package/dist/core/security-scoring.js +76 -9
- package/dist/core/security.js +34 -2
- package/dist/core/sequence.js +11 -2
- package/dist/core/setup-flow.js +141 -13
- package/dist/core/spawn-check.js +16 -2
- package/dist/core/staleness.js +73 -2
- package/dist/core/state.js +250 -54
- package/dist/core/store-resolution.js +45 -12
- package/dist/core/worktree.js +90 -26
- package/dist/facts.js +8 -8
- package/dist/facts.json +7 -7
- package/docs/PROTOCOL.md +223 -0
- package/docs/adapters/openclaw.md +43 -43
- package/docs/architecture/project-refs.md +328 -328
- package/docs/cli.md +2097 -2096
- package/docs/concepts/coordination.md +52 -52
- package/docs/concepts/coordinator-runbook.md +129 -0
- package/docs/concepts/dispatch-lifecycle.md +245 -245
- package/docs/concepts/event-log-store.md +928 -0
- package/docs/concepts/ideation-loop.md +317 -317
- package/docs/concepts/loop-engine.md +520 -511
- package/docs/concepts/mcp-governance.md +268 -268
- package/docs/concepts/memory.md +89 -88
- package/docs/concepts/multi-agent-workflows.md +167 -167
- package/docs/concepts/observer-protocol.md +361 -0
- package/docs/concepts/parallel-merge-protocol.md +71 -0
- package/docs/concepts/plans-and-claims.md +217 -174
- package/docs/concepts/project-md-convention.md +35 -35
- package/docs/concepts/runtime-notes.md +38 -38
- package/docs/concepts/skills.md +78 -0
- package/docs/concepts/troubleshooting.md +254 -254
- package/docs/concepts/workspace-bootstrapping.md +142 -81
- package/docs/context-format-changelog.md +35 -35
- package/docs/context-format.md +48 -48
- package/docs/index.md +65 -65
- package/docs/integrations/agents.md +162 -162
- package/docs/integrations/claude-code.md +23 -23
- package/docs/integrations/cline.md +87 -88
- package/docs/integrations/codex.md +2 -2
- package/docs/integrations/continue.md +60 -60
- package/docs/integrations/copilot.md +82 -80
- package/docs/integrations/cursor.md +23 -23
- package/docs/integrations/kilocode.md +72 -72
- package/docs/integrations/mcp.md +377 -377
- package/docs/integrations/mistral-vibe.md +122 -122
- package/docs/integrations/openclaw.md +99 -98
- package/docs/integrations/opencode.md +84 -84
- package/docs/integrations/overview.md +122 -122
- package/docs/integrations/roo.md +74 -74
- package/docs/integrations/windsurf.md +83 -83
- package/docs/mcp-schema-changelog.md +360 -329
- package/docs/playbooks/integration/index.md +121 -121
- package/docs/playbooks/orchestration.md +37 -0
- package/docs/playbooks/productivity/index.md +99 -99
- package/docs/playbooks/team/index.md +117 -117
- package/docs/product/agent-first-model.md +184 -184
- package/docs/product/entity-model-audit.md +462 -462
- package/docs/product/positioning.md +86 -86
- package/docs/quickstart-existing-project.md +107 -107
- package/docs/quickstart.md +148 -147
- package/docs/release-maintenance.md +79 -79
- package/docs/reputation.md +52 -52
- package/docs/review.md +45 -45
- package/docs/security.md +212 -53
- package/docs/server-operations.md +118 -118
- package/docs/storage.md +110 -108
- package/package.json +86 -69
|
@@ -46,6 +46,18 @@ export function getRuntimeSignalPath(root, assignmentId, signal) {
|
|
|
46
46
|
export function getRuntimeLogPath(root, assignmentId, stream) {
|
|
47
47
|
return path.join(runtimeDir(root), 'log', `${assignmentId}.${stream}.log`);
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Worktree-local heartbeat path (sprint 1.5). The project-root signal path is
|
|
51
|
+
* NOT writable from inside many worker sandboxes (Claude Code restricts writes
|
|
52
|
+
* to its working directories; codex workspace-write roots exclude the project) —
|
|
53
|
+
* observed live: the generated brief demanded a heartbeat the worker could not
|
|
54
|
+
* write. The worktree root is the one location every dispatched worker can
|
|
55
|
+
* write, so briefs point step-0 here, and every heartbeat reader checks BOTH
|
|
56
|
+
* locations.
|
|
57
|
+
*/
|
|
58
|
+
export function getWorktreeHeartbeatPath(worktreePath, assignmentId) {
|
|
59
|
+
return path.join(worktreePath, `.brainclaw-heartbeat-${assignmentId}`);
|
|
60
|
+
}
|
|
49
61
|
/** Ensure the ack / signal / log directories exist (best-effort, recursive). */
|
|
50
62
|
export function ensureRuntimeDirs(root) {
|
|
51
63
|
const base = runtimeDir(root);
|
|
@@ -61,13 +73,7 @@ export function signalExists(root, assignmentId, signal) {
|
|
|
61
73
|
return false;
|
|
62
74
|
}
|
|
63
75
|
}
|
|
64
|
-
|
|
65
|
-
* Read the heartbeat sentinel. The body is expected to be
|
|
66
|
-
* `work_loop_reached{run_id,nonce}` JSON, but a bare `touch` (empty file) still
|
|
67
|
-
* counts as a heartbeat — the mtime alone is a valid life-sign.
|
|
68
|
-
*/
|
|
69
|
-
export function readHeartbeat(root, assignmentId) {
|
|
70
|
-
const p = getRuntimeSignalPath(root, assignmentId, 'heartbeat');
|
|
76
|
+
function readHeartbeatFile(p) {
|
|
71
77
|
try {
|
|
72
78
|
const stat = fs.statSync(p);
|
|
73
79
|
const info = { exists: true, mtimeMs: stat.mtimeMs };
|
|
@@ -88,12 +94,68 @@ export function readHeartbeat(root, assignmentId) {
|
|
|
88
94
|
return { exists: false };
|
|
89
95
|
}
|
|
90
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Read the heartbeat sentinel. The body is expected to be
|
|
99
|
+
* `work_loop_reached{run_id,nonce}` JSON, but a bare `touch` (empty file) still
|
|
100
|
+
* counts as a heartbeat — the mtime alone is a valid life-sign.
|
|
101
|
+
*
|
|
102
|
+
* Checks the project-root signal path AND (when `worktreePath` is given) the
|
|
103
|
+
* worktree-local heartbeat — sandboxed workers can only write the latter. When
|
|
104
|
+
* both exist, the freshest mtime wins.
|
|
105
|
+
*/
|
|
106
|
+
export function readHeartbeat(root, assignmentId, worktreePath) {
|
|
107
|
+
const projectInfo = readHeartbeatFile(getRuntimeSignalPath(root, assignmentId, 'heartbeat'));
|
|
108
|
+
const worktreeInfo = worktreePath
|
|
109
|
+
? readHeartbeatFile(getWorktreeHeartbeatPath(worktreePath, assignmentId))
|
|
110
|
+
: { exists: false };
|
|
111
|
+
if (!projectInfo.exists)
|
|
112
|
+
return worktreeInfo;
|
|
113
|
+
if (!worktreeInfo.exists)
|
|
114
|
+
return projectInfo;
|
|
115
|
+
return (worktreeInfo.mtimeMs ?? 0) > (projectInfo.mtimeMs ?? 0) ? worktreeInfo : projectInfo;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* can_c39f0961 — CP850 high-byte table (0x80–0xFF). Windows-native console
|
|
119
|
+
* tools write redirected stdout/stderr in the OEM codepage (cp850 on western
|
|
120
|
+
* locales), which read as UTF-8 produces U+FFFD mojibake in captured logs.
|
|
121
|
+
* WHATWG TextDecoder does not cover ibm850, so a 128-entry table keeps the
|
|
122
|
+
* fallback decode dependency-free.
|
|
123
|
+
*/
|
|
124
|
+
const CP850_HIGH = 'ÇüéâäàåçêëèïîìÄÅ' +
|
|
125
|
+
'ÉæÆôöòûùÿÖÜø£Ø×ƒ' +
|
|
126
|
+
'áíóúñѪº¿®¬½¼¡«»' +
|
|
127
|
+
'░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐' +
|
|
128
|
+
'└┴┬├─┼ãÃ╚╔╩╦╠═╬¤' +
|
|
129
|
+
'ðÐÊËÈıÍÎÏ┘┌█▄¦Ì▀' +
|
|
130
|
+
'ÓßÔÒõÕµþÞÚÛÙýݯ´' +
|
|
131
|
+
'±‗¾¶§÷¸°¨·¹³²■ ';
|
|
132
|
+
/**
|
|
133
|
+
* Decode a captured-log buffer: UTF-8 first, falling back to cp850 when the
|
|
134
|
+
* UTF-8 decode shows replacement characters (the OEM-output signature).
|
|
135
|
+
*/
|
|
136
|
+
export function decodeOemAwareBuffer(buf) {
|
|
137
|
+
const utf8 = buf.toString('utf-8');
|
|
138
|
+
if (!utf8.includes('�'))
|
|
139
|
+
return utf8;
|
|
140
|
+
let out = '';
|
|
141
|
+
for (const byte of buf) {
|
|
142
|
+
out += byte < 0x80 ? String.fromCharCode(byte) : CP850_HIGH[byte - 0x80];
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
91
146
|
/** Read the tail of a captured stream log (for failed_silent diagnostics). */
|
|
92
147
|
export function readLogTail(root, assignmentId, stream, maxBytes = 2000) {
|
|
93
148
|
try {
|
|
94
149
|
const p = getRuntimeLogPath(root, assignmentId, stream);
|
|
95
|
-
const
|
|
96
|
-
|
|
150
|
+
const buf = fs.readFileSync(p);
|
|
151
|
+
let slice = buf.length > maxBytes ? buf.subarray(buf.length - maxBytes) : buf;
|
|
152
|
+
// A byte-offset tail can start mid-UTF-8-sequence; dropping leading
|
|
153
|
+
// continuation bytes avoids a false U+FFFD that would trigger the cp850
|
|
154
|
+
// fallback on genuine UTF-8 content.
|
|
155
|
+
while (slice.length > 0 && slice[0] >= 0x80 && slice[0] <= 0xbf) {
|
|
156
|
+
slice = slice.subarray(1);
|
|
157
|
+
}
|
|
158
|
+
return decodeOemAwareBuffer(slice);
|
|
97
159
|
}
|
|
98
160
|
catch {
|
|
99
161
|
return '';
|
|
@@ -152,7 +214,7 @@ export function latestWorktreeFileMtimeMs(worktreePath, maxDepth = 4) {
|
|
|
152
214
|
* file in its worktree. Lets the reconciler / dispatch_status distinguish
|
|
153
215
|
* "no heartbeat BUT fs active" (working — e.g. codex streaming to stderr, or
|
|
154
216
|
* claude -p editing files) from "no heartbeat AND fs inert" (genuinely stalled),
|
|
155
|
-
* fixing the false-`stalled` verdict (debrief
|
|
217
|
+
* fixing the false-`stalled` verdict (field debrief P1#1). Returns undefined
|
|
156
218
|
* when nothing is observable.
|
|
157
219
|
*/
|
|
158
220
|
export function latestActivityMs(root, assignmentId, worktreePath) {
|
package/dist/core/runtime.js
CHANGED
|
@@ -8,6 +8,7 @@ import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
|
8
8
|
import { RuntimeNoteSchema } from './schema.js';
|
|
9
9
|
import { commitMemoryChange } from './memory-git.js';
|
|
10
10
|
import { appendEvent } from './event-log.js';
|
|
11
|
+
import { emitRegistryPostImage, emitRegistryTombstone, registryFaultPoint } from './events/registry-post-image.js';
|
|
11
12
|
function sharedRuntimeDir(cwd, mode = 'read') {
|
|
12
13
|
return resolveEntityDir('runtime', cwd ?? process.cwd(), mode);
|
|
13
14
|
}
|
|
@@ -46,7 +47,16 @@ export function saveRuntimeNote(note, cwd) {
|
|
|
46
47
|
const filepath = visibility === 'shared'
|
|
47
48
|
? path.join(sharedAgentDir(note.agent, cwd, 'write'), `${note.id}.json`)
|
|
48
49
|
: path.join(hostAgentDir(visibility, hostId, note.agent, cwd, 'write'), `${note.id}.json`);
|
|
49
|
-
|
|
50
|
+
const parsed = RuntimeNoteSchema.parse(persistedNote);
|
|
51
|
+
// pln#568 (I2): journal the post-image BEFORE the projection write — but
|
|
52
|
+
// SHARED notes only. Private/machine-visibility notes must not leak their
|
|
53
|
+
// payload into the shared journal (the observer's board shows shared notes).
|
|
54
|
+
if (visibility === 'shared') {
|
|
55
|
+
const created = !fs.existsSync(filepath);
|
|
56
|
+
emitRegistryPostImage('runtime_note', parsed, { created, agent: note.agent, agent_id: note.agent_id, session_id: note.session_id, cwd });
|
|
57
|
+
registryFaultPoint('after_registry_journal');
|
|
58
|
+
}
|
|
59
|
+
saveVersionedJsonFile('runtime_note', filepath, parsed);
|
|
50
60
|
appendEvent({ action: 'create', item_type: 'runtime_note', item_id: note.id, agent: note.agent, agent_id: note.agent_id }, cwd);
|
|
51
61
|
commitMemoryChange(`runtime note: ${note.note_type ?? 'note'} (${note.agent})`, cwd);
|
|
52
62
|
});
|
|
@@ -64,10 +74,47 @@ export function deleteRuntimeNote(note, cwd) {
|
|
|
64
74
|
if (!fs.existsSync(filepath)) {
|
|
65
75
|
return false;
|
|
66
76
|
}
|
|
77
|
+
if ((note.visibility ?? 'shared') === 'shared') {
|
|
78
|
+
emitRegistryTombstone('runtime_note', note.id, {
|
|
79
|
+
agent: note.agent,
|
|
80
|
+
agent_id: note.agent_id,
|
|
81
|
+
session_id: note.session_id,
|
|
82
|
+
cwd,
|
|
83
|
+
});
|
|
84
|
+
registryFaultPoint('after_registry_journal');
|
|
85
|
+
}
|
|
67
86
|
fs.unlinkSync(filepath);
|
|
68
87
|
return true;
|
|
69
88
|
});
|
|
70
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The shared runtime notes that are journaled as post-images (pln#568): notes
|
|
92
|
+
* under `runtime/<agent>/*.json`, EXCLUDING `runtime/agent-runtime/` (which
|
|
93
|
+
* holds runtime EVENT files `evt_*.json`, not saveRuntimeNote post-images —
|
|
94
|
+
* they would otherwise be parsed as notes and report false drift). Single
|
|
95
|
+
* source of truth for the journaled-shared-note set, shared by the registry
|
|
96
|
+
* verifier (verify.ts) and the registry genesis backfill (genesis.ts).
|
|
97
|
+
*/
|
|
98
|
+
export function listSharedJournaledRuntimeNotes(cwd) {
|
|
99
|
+
const root = sharedRuntimeDir(cwd, 'read');
|
|
100
|
+
if (!fs.existsSync(root))
|
|
101
|
+
return [];
|
|
102
|
+
const notes = [];
|
|
103
|
+
for (const entry of fs.readdirSync(root).sort()) {
|
|
104
|
+
if (entry === 'agent-runtime')
|
|
105
|
+
continue;
|
|
106
|
+
const agentDir = path.join(root, entry);
|
|
107
|
+
if (!fs.existsSync(agentDir) || !fs.statSync(agentDir).isDirectory())
|
|
108
|
+
continue;
|
|
109
|
+
for (const file of fs.readdirSync(agentDir).filter((name) => name.endsWith('.json')).sort()) {
|
|
110
|
+
try {
|
|
111
|
+
notes.push(loadVersionedJsonFile('runtime_note', path.join(agentDir, file)).document);
|
|
112
|
+
}
|
|
113
|
+
catch { /* mirror listRuntimeNotes' tolerant read */ }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return notes.sort((a, b) => a.id.localeCompare(b.id));
|
|
117
|
+
}
|
|
71
118
|
function readAgentNotes(dir, agent) {
|
|
72
119
|
if (!fs.existsSync(dir))
|
|
73
120
|
return [];
|
|
@@ -136,4 +183,40 @@ export function generateRuntimeNoteId() {
|
|
|
136
183
|
const rand = crypto.randomBytes(4).toString('hex');
|
|
137
184
|
return `rtn_${rand}`;
|
|
138
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* can_b8d53d18 — soft migration for runtime notes created with the legacy
|
|
188
|
+
* `run_` prefix (the generateId fallback collided with agent_run ids).
|
|
189
|
+
* Rewrites each note's id to `rtn_<same suffix>` and renames its file.
|
|
190
|
+
* Old ids referenced in historical events stay historical; lookups are
|
|
191
|
+
* list-scan based so nothing else needs to change.
|
|
192
|
+
*/
|
|
193
|
+
export function migrateRuntimeNoteIdPrefixes(cwd) {
|
|
194
|
+
const result = { migrated: [], errors: [] };
|
|
195
|
+
const legacy = listRuntimeNotes({ visibility: 'all', includeAllHosts: true }, cwd)
|
|
196
|
+
.filter((note) => note.id.startsWith('run_'));
|
|
197
|
+
if (legacy.length === 0)
|
|
198
|
+
return result;
|
|
199
|
+
const existingIds = new Set(listRuntimeNotes({ visibility: 'all', includeAllHosts: true }, cwd).map((n) => n.id));
|
|
200
|
+
mutate({ cwd }, () => {
|
|
201
|
+
for (const note of legacy) {
|
|
202
|
+
try {
|
|
203
|
+
let newId = `rtn_${note.id.slice('run_'.length)}`;
|
|
204
|
+
while (existingIds.has(newId))
|
|
205
|
+
newId = generateRuntimeNoteId();
|
|
206
|
+
const oldPath = runtimeNotePath(note, cwd);
|
|
207
|
+
const migrated = { ...note, id: newId };
|
|
208
|
+
const newPath = runtimeNotePath(migrated, cwd);
|
|
209
|
+
saveVersionedJsonFile('runtime_note', newPath, RuntimeNoteSchema.parse(migrated));
|
|
210
|
+
if (fs.existsSync(oldPath) && oldPath !== newPath)
|
|
211
|
+
fs.unlinkSync(oldPath);
|
|
212
|
+
existingIds.add(newId);
|
|
213
|
+
result.migrated.push({ from: note.id, to: newId });
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
result.errors.push(`${note.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
139
222
|
//# sourceMappingURL=runtime.js.map
|
package/dist/core/schema.js
CHANGED
|
@@ -57,6 +57,33 @@ function coerceTags(val) {
|
|
|
57
57
|
/** Resilient tags schema that accepts string[] or JSON-serialized string. */
|
|
58
58
|
export const TagsSchema = z.preprocess(coerceTags, z.array(z.string()));
|
|
59
59
|
export const TagsWithDefaultSchema = z.preprocess(coerceTags, z.array(z.string()).default([]));
|
|
60
|
+
// --- Memory lifecycle (pln#544) ---
|
|
61
|
+
/**
|
|
62
|
+
* One confirmation/infirmation event on a memory item (decision, constraint,
|
|
63
|
+
* trap). The full history is stored on the item as a bounded list — the
|
|
64
|
+
* `last_confirmed_at` / `last_infirmed_at` / `confirmation_count` /
|
|
65
|
+
* `infirmation_count` denormalisations let read paths avoid scanning the log
|
|
66
|
+
* for the common "is this still trustworthy" question.
|
|
67
|
+
*
|
|
68
|
+
* - `confirm` / `infirm`: passive observation that the rule still holds /
|
|
69
|
+
* no longer holds.
|
|
70
|
+
* - `saved_me`: agent explicitly credits the item with avoiding a bug —
|
|
71
|
+
* reinforces ranking weight via reputation.ts.
|
|
72
|
+
* - `misled_me`: agent explicitly blames the item for a wrong move —
|
|
73
|
+
* sinks ranking weight, mirror of saved_me.
|
|
74
|
+
*/
|
|
75
|
+
export const MemoryConfirmationKindSchema = z.enum(['confirm', 'infirm', 'saved_me', 'misled_me']);
|
|
76
|
+
export const MemoryConfirmationEventSchema = z.object({
|
|
77
|
+
at: z.string(),
|
|
78
|
+
by: z.string(),
|
|
79
|
+
by_id: z.string().optional(),
|
|
80
|
+
session_id: z.string().optional(),
|
|
81
|
+
kind: MemoryConfirmationKindSchema,
|
|
82
|
+
/** Pointer at the evidence — file:line, commit sha, message id, command output. */
|
|
83
|
+
evidence: z.string().optional(),
|
|
84
|
+
/** Free-form note (the one-line "why" the agent attests). */
|
|
85
|
+
note: z.string().optional(),
|
|
86
|
+
});
|
|
60
87
|
// --- Entry schemas ---
|
|
61
88
|
export const ConstraintStatusSchema = z.enum(['active', 'resolved', 'expired']);
|
|
62
89
|
export const ConstraintCategorySchema = z.enum(['architecture', 'performance', 'security', 'reliability', 'compatibility', 'process', 'other']);
|
|
@@ -135,6 +162,18 @@ export const ConstraintSchema = z.object({
|
|
|
135
162
|
related_paths: z.array(z.string()).optional(),
|
|
136
163
|
plan_id: z.string().optional(),
|
|
137
164
|
expires_at: z.string().optional(),
|
|
165
|
+
// pln#544 — memory-lifecycle (confirm/decay/reinforce). Symmetric across
|
|
166
|
+
// constraint/decision/trap. `verified_at` (pln#530 perishable-fact
|
|
167
|
+
// re-verification) is kept as a narrower legacy signal alongside.
|
|
168
|
+
last_confirmed_at: z.string().optional(),
|
|
169
|
+
last_infirmed_at: z.string().optional(),
|
|
170
|
+
confirmation_count: z.number().int().nonnegative().optional(),
|
|
171
|
+
infirmation_count: z.number().int().nonnegative().optional(),
|
|
172
|
+
saved_me_count: z.number().int().nonnegative().optional(),
|
|
173
|
+
misled_me_count: z.number().int().nonnegative().optional(),
|
|
174
|
+
/** Bounded event log (most recent N) — older events are dropped, the
|
|
175
|
+
* counts remain accurate. Empty / absent means "never confirmed". */
|
|
176
|
+
confirmations: z.array(MemoryConfirmationEventSchema).optional(),
|
|
138
177
|
provenance: ProvenancePassthroughSchema,
|
|
139
178
|
});
|
|
140
179
|
export const DecisionSchema = z.object({
|
|
@@ -159,6 +198,14 @@ export const DecisionSchema = z.object({
|
|
|
159
198
|
// facts (tool behaviour, config values), probe before trusting the memory.
|
|
160
199
|
verified_at: z.string().optional(),
|
|
161
200
|
verify_cmd: z.string().optional(),
|
|
201
|
+
// pln#544 — memory-lifecycle (see ConstraintSchema).
|
|
202
|
+
last_confirmed_at: z.string().optional(),
|
|
203
|
+
last_infirmed_at: z.string().optional(),
|
|
204
|
+
confirmation_count: z.number().int().nonnegative().optional(),
|
|
205
|
+
infirmation_count: z.number().int().nonnegative().optional(),
|
|
206
|
+
saved_me_count: z.number().int().nonnegative().optional(),
|
|
207
|
+
misled_me_count: z.number().int().nonnegative().optional(),
|
|
208
|
+
confirmations: z.array(MemoryConfirmationEventSchema).optional(),
|
|
162
209
|
provenance: ProvenancePassthroughSchema,
|
|
163
210
|
});
|
|
164
211
|
export const TrapSchema = z.object({
|
|
@@ -187,6 +234,14 @@ export const TrapSchema = z.object({
|
|
|
187
234
|
// traps that go stale (e.g. a service_tier value that the API later rejects).
|
|
188
235
|
verified_at: z.string().optional(),
|
|
189
236
|
verify_cmd: z.string().optional(),
|
|
237
|
+
// pln#544 — memory-lifecycle (see ConstraintSchema).
|
|
238
|
+
last_confirmed_at: z.string().optional(),
|
|
239
|
+
last_infirmed_at: z.string().optional(),
|
|
240
|
+
confirmation_count: z.number().int().nonnegative().optional(),
|
|
241
|
+
infirmation_count: z.number().int().nonnegative().optional(),
|
|
242
|
+
saved_me_count: z.number().int().nonnegative().optional(),
|
|
243
|
+
misled_me_count: z.number().int().nonnegative().optional(),
|
|
244
|
+
confirmations: z.array(MemoryConfirmationEventSchema).optional(),
|
|
190
245
|
provenance: ProvenancePassthroughSchema,
|
|
191
246
|
});
|
|
192
247
|
export const HandoffContractSchema = z.object({
|
|
@@ -233,6 +288,17 @@ export const HandoffSchema = z.object({
|
|
|
233
288
|
review: HandoffReviewSchema.optional(),
|
|
234
289
|
snapshot: z.object({
|
|
235
290
|
diff: z.string().optional(),
|
|
291
|
+
/**
|
|
292
|
+
* pln#569 — digest of the FULL uncommitted diff when `diff` holds only a
|
|
293
|
+
* capped preview (auto-handoffs). Lets a reader know the inline diff is a
|
|
294
|
+
* prefix (and verify a re-fetched full diff) without storing the whole
|
|
295
|
+
* ~450 KB on the handoff. Absent ⇒ `diff` is complete.
|
|
296
|
+
*/
|
|
297
|
+
diff_digest: z.object({
|
|
298
|
+
full_bytes: z.number().int().nonnegative(),
|
|
299
|
+
sha256: z.string(),
|
|
300
|
+
truncated: z.boolean(),
|
|
301
|
+
}).optional(),
|
|
236
302
|
}).optional(),
|
|
237
303
|
provenance: ProvenancePassthroughSchema,
|
|
238
304
|
/**
|
|
@@ -263,6 +329,15 @@ export const PlanStepSchema = z.object({
|
|
|
263
329
|
assignee: z.string().optional(),
|
|
264
330
|
created_at: z.string(),
|
|
265
331
|
updated_at: z.string(),
|
|
332
|
+
// Step-level effort (pln#495). estimated_effort accepts legacy duration
|
|
333
|
+
// strings ("2h", "30m") via the same preprocess as plan-level; actual_effort
|
|
334
|
+
// stays a free string parsed by parseEffortMinutes when reported. started_at /
|
|
335
|
+
// completed_at are set on the todo→in_progress→done transitions so the
|
|
336
|
+
// estimation report can sum per-step durations and exclude inter-step idle.
|
|
337
|
+
estimated_effort: z.preprocess(coerceEffortToMinutes, z.number().int().positive().optional()),
|
|
338
|
+
actual_effort: z.string().optional(),
|
|
339
|
+
started_at: z.string().optional(),
|
|
340
|
+
completed_at: z.string().optional(),
|
|
266
341
|
});
|
|
267
342
|
export const PlanTypeEnumSchema = z.enum(['feat', 'fix', 'chore', 'spike', 'doc']);
|
|
268
343
|
export const PlanTypeSchema = PlanTypeEnumSchema.default('feat');
|
|
@@ -458,11 +533,25 @@ export const PreinstallConfigSchema = z.object({
|
|
|
458
533
|
denylist: z.array(z.string()).default([]),
|
|
459
534
|
socket_endpoint: z.string().default('https://mcp.socket.dev/'),
|
|
460
535
|
});
|
|
536
|
+
export const TokenDetectionConfigSchema = z.object({
|
|
537
|
+
enabled: z.boolean().default(true),
|
|
538
|
+
/** Entropy-based detection for high-randomness strings (typically API keys / tokens). */
|
|
539
|
+
entropy: z.object({
|
|
540
|
+
enabled: z.boolean().default(true),
|
|
541
|
+
/** Minimum string length to consider as a potential secret. */
|
|
542
|
+
min_length: z.number().int().min(8).max(1024).default(32),
|
|
543
|
+
/** Minimum Shannon entropy (bits per char) to flag the string. ~4.0 is a reasonable line. */
|
|
544
|
+
min_entropy: z.number().min(0).max(8).default(4.0),
|
|
545
|
+
}).prefault({}),
|
|
546
|
+
/** Per-detector overrides: { aws_access_key: false } disables that detector. */
|
|
547
|
+
detectors: z.record(z.string(), z.boolean()).default({}),
|
|
548
|
+
});
|
|
461
549
|
export const SecurityConfigSchema = z.object({
|
|
462
550
|
mode: z.enum(['warn', 'strict']).default('warn'),
|
|
463
551
|
strict_redaction: z.boolean().default(false),
|
|
464
552
|
block_sensitive_paths: z.boolean().default(true),
|
|
465
553
|
preinstall: PreinstallConfigSchema.optional(),
|
|
554
|
+
token_detection: TokenDetectionConfigSchema.prefault({}),
|
|
466
555
|
});
|
|
467
556
|
export const MarkdownConfigSchema = z.object({
|
|
468
557
|
max_items_per_section: z.number().default(20),
|
|
@@ -1043,6 +1132,9 @@ export const MemorySeedKindSchema = z.enum([
|
|
|
1043
1132
|
'warning',
|
|
1044
1133
|
'environment',
|
|
1045
1134
|
'tooling',
|
|
1135
|
+
'decision',
|
|
1136
|
+
'constraint',
|
|
1137
|
+
'trap',
|
|
1046
1138
|
]);
|
|
1047
1139
|
export const MemorySeedSourceKindSchema = z.enum([
|
|
1048
1140
|
'readme',
|
|
@@ -1080,6 +1172,7 @@ export const BootstrapProfileDocumentSchema = z.object({
|
|
|
1080
1172
|
schema_version: z.number().int().positive().optional(),
|
|
1081
1173
|
derived_at: z.string(),
|
|
1082
1174
|
repo_fingerprint: z.string().optional(),
|
|
1175
|
+
source_fingerprint: z.string().optional(),
|
|
1083
1176
|
summary: z.string(),
|
|
1084
1177
|
sources_scanned: z.array(z.string()).default([]),
|
|
1085
1178
|
git_available: z.boolean().default(false),
|
|
@@ -1310,5 +1403,26 @@ export const ConfigSchema = z.object({
|
|
|
1310
1403
|
shared_paths: z.array(z.string()).default([]),
|
|
1311
1404
|
exclude_shared: z.array(z.string()).default([]),
|
|
1312
1405
|
}).optional(),
|
|
1406
|
+
// Event-log store (pln#543). Absent ⇒ off — fresh and existing stores keep
|
|
1407
|
+
// today's behavior; the journal only activates when explicitly set here (or
|
|
1408
|
+
// via the BRAINCLAW_JOURNAL_MODE env override). The cutover (step 5) flips
|
|
1409
|
+
// the default to dual/primary.
|
|
1410
|
+
store: z.object({
|
|
1411
|
+
journal: z.object({
|
|
1412
|
+
mode: z.enum(['off', 'dual', 'primary', 'registryPrimary']).optional(),
|
|
1413
|
+
fsync: z.enum(['mutation', 'never']).optional(),
|
|
1414
|
+
/**
|
|
1415
|
+
* Phase-3 per-capability sub-flags (pln#566). Each ships + soaks GREEN
|
|
1416
|
+
* independently before its capability is trusted; all default off so a
|
|
1417
|
+
* bare `mode: primary` changes nothing until a capability is enabled.
|
|
1418
|
+
*/
|
|
1419
|
+
primary: z.object({
|
|
1420
|
+
checkpointRead: z.boolean().optional(),
|
|
1421
|
+
readReconcile: z.boolean().optional(),
|
|
1422
|
+
tombstoneDelete: z.boolean().optional(),
|
|
1423
|
+
perEntityPatch: z.boolean().optional(),
|
|
1424
|
+
}).optional(),
|
|
1425
|
+
}).optional(),
|
|
1426
|
+
}).optional(),
|
|
1313
1427
|
});
|
|
1314
1428
|
//# sourceMappingURL=schema.js.map
|
package/dist/core/search.js
CHANGED
|
@@ -10,9 +10,16 @@ function tokenize(text) {
|
|
|
10
10
|
.split(/\s+/)
|
|
11
11
|
.filter(t => t.length > 1);
|
|
12
12
|
}
|
|
13
|
-
function
|
|
13
|
+
function isLegacyDocument(document) {
|
|
14
|
+
return document.provenance !== null
|
|
15
|
+
&& typeof document.provenance === 'object'
|
|
16
|
+
&& document.provenance.kind === 'legacy';
|
|
17
|
+
}
|
|
18
|
+
function buildCorpus(state, includePending, cwd, includeLegacy = false) {
|
|
14
19
|
const docs = [];
|
|
15
20
|
const add = (section, item) => {
|
|
21
|
+
if (!includeLegacy && isLegacyDocument({ ...item, section }))
|
|
22
|
+
return;
|
|
16
23
|
const textParts = [item.text, item.author ?? '', ...(item.tags ?? []), ...(item.related_paths ?? [])];
|
|
17
24
|
docs.push({ ...item, section, terms: tokenize(textParts.join(' ')) });
|
|
18
25
|
};
|
|
@@ -128,7 +135,17 @@ export function searchCorpus(documents, options) {
|
|
|
128
135
|
}
|
|
129
136
|
export function search(options) {
|
|
130
137
|
const state = loadState(options.cwd);
|
|
131
|
-
const corpus = buildCorpus(state, options.includePending ?? false, options.cwd);
|
|
138
|
+
const corpus = buildCorpus(state, options.includePending ?? false, options.cwd, options.includeLegacy === true);
|
|
132
139
|
return searchCorpus(corpus, options);
|
|
133
140
|
}
|
|
141
|
+
export function countLegacySearchMatches(options) {
|
|
142
|
+
const state = loadState(options.cwd);
|
|
143
|
+
const legacyCorpus = buildCorpus(state, options.includePending ?? false, options.cwd, true)
|
|
144
|
+
.filter(isLegacyDocument);
|
|
145
|
+
return searchCorpus(legacyCorpus, {
|
|
146
|
+
...options,
|
|
147
|
+
includeLegacy: true,
|
|
148
|
+
maxResults: Number.MAX_SAFE_INTEGER,
|
|
149
|
+
}).length;
|
|
150
|
+
}
|
|
134
151
|
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural secret detectors.
|
|
3
|
+
*
|
|
4
|
+
* These run alongside the user-configured regex patterns in
|
|
5
|
+
* `config.redaction.patterns`. They look for well-known token shapes
|
|
6
|
+
* (GitHub PATs, AWS access keys, JWTs, etc.) — high-precision signal
|
|
7
|
+
* that should fire even when the operator hasn't added a custom pattern.
|
|
8
|
+
*
|
|
9
|
+
* Each detector has a stable `id` so operators can disable individual
|
|
10
|
+
* detectors via `security.token_detection.detectors[id] = false` without
|
|
11
|
+
* having to redefine the full list.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Detectors are tuned for low false-positive rates. Anchors and
|
|
15
|
+
* length constraints are intentional; loosening them turns noisy.
|
|
16
|
+
*/
|
|
17
|
+
export const BUILTIN_DETECTORS = [
|
|
18
|
+
// GitHub
|
|
19
|
+
{ id: 'github_pat', label: 'GitHub personal access token', pattern: /\bghp_[A-Za-z0-9]{36}\b/ },
|
|
20
|
+
{ id: 'github_pat_v2', label: 'GitHub fine-grained PAT', pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/ },
|
|
21
|
+
{ id: 'github_oauth', label: 'GitHub OAuth token', pattern: /\bgho_[A-Za-z0-9]{36}\b/ },
|
|
22
|
+
{ id: 'github_user_to_server', label: 'GitHub user-to-server token', pattern: /\bghu_[A-Za-z0-9]{36}\b/ },
|
|
23
|
+
{ id: 'github_server_to_server', label: 'GitHub server-to-server token', pattern: /\bghs_[A-Za-z0-9]{36}\b/ },
|
|
24
|
+
{ id: 'github_refresh', label: 'GitHub refresh token', pattern: /\bghr_[A-Za-z0-9]{36}\b/ },
|
|
25
|
+
// AWS
|
|
26
|
+
{ id: 'aws_access_key', label: 'AWS access key ID', pattern: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
27
|
+
{ id: 'aws_temp_access_key', label: 'AWS temporary access key ID', pattern: /\bASIA[0-9A-Z]{16}\b/ },
|
|
28
|
+
// Google
|
|
29
|
+
{ id: 'google_api_key', label: 'Google API key', pattern: /\bAIza[0-9A-Za-z_\-]{35}\b/ },
|
|
30
|
+
// Slack
|
|
31
|
+
{ id: 'slack_token', label: 'Slack token', pattern: /\bxox[abprs]-[0-9A-Za-z-]{10,}\b/ },
|
|
32
|
+
{ id: 'slack_webhook', label: 'Slack webhook', pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]{8,}\/B[A-Z0-9]{8,}\/[A-Za-z0-9]{24,}/ },
|
|
33
|
+
// Stripe
|
|
34
|
+
{ id: 'stripe_secret', label: 'Stripe secret key', pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{24,}\b/ },
|
|
35
|
+
// Generic structural
|
|
36
|
+
{ id: 'jwt', label: 'JSON Web Token', pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/ },
|
|
37
|
+
{ id: 'pem_private_key', label: 'PEM-encoded private key', pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY-----/ },
|
|
38
|
+
// Generic high-confidence: connection strings with embedded credentials.
|
|
39
|
+
{ id: 'url_basic_auth', label: 'URL with embedded credentials', pattern: /[a-z][a-z+.-]+:\/\/[^\s:@/]+:[^\s@/]{4,}@[A-Za-z0-9.-]+/ },
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Run every enabled structural detector across `text`. Detectors with an
|
|
43
|
+
* explicit `false` in `disabled` are skipped.
|
|
44
|
+
*/
|
|
45
|
+
export function runStructuralDetectors(text, disabled) {
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const d of BUILTIN_DETECTORS) {
|
|
48
|
+
if (disabled && disabled[d.id] === false)
|
|
49
|
+
continue;
|
|
50
|
+
const m = text.match(d.pattern);
|
|
51
|
+
if (m) {
|
|
52
|
+
out.push({
|
|
53
|
+
detectorId: d.id,
|
|
54
|
+
label: d.label,
|
|
55
|
+
excerpt: truncate(m[0]),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Shannon entropy in bits per character. Higher = more random.
|
|
63
|
+
* - English prose ~2.5–3.0
|
|
64
|
+
* - hex-encoded data ~3.5–4.0
|
|
65
|
+
* - base64-encoded data ~5.0–6.0
|
|
66
|
+
* - cryptographic keys ~5.5+
|
|
67
|
+
*/
|
|
68
|
+
export function shannonEntropy(s) {
|
|
69
|
+
if (!s)
|
|
70
|
+
return 0;
|
|
71
|
+
const counts = new Map();
|
|
72
|
+
for (const ch of s)
|
|
73
|
+
counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
74
|
+
let h = 0;
|
|
75
|
+
const n = s.length;
|
|
76
|
+
for (const c of counts.values()) {
|
|
77
|
+
const p = c / n;
|
|
78
|
+
h -= p * Math.log2(p);
|
|
79
|
+
}
|
|
80
|
+
return h;
|
|
81
|
+
}
|
|
82
|
+
const SECRET_KEYWORD_CONTEXT = /(?:api[_-]?key|secret|token|password|passwd|auth|bearer|access[_-]?key|private[_-]?key)/i;
|
|
83
|
+
const HIGH_ENTROPY_CANDIDATE = /[A-Za-z0-9_+/=\-]{16,}/g;
|
|
84
|
+
/**
|
|
85
|
+
* Entropy-based detection. Scans `text` for token-shaped substrings near
|
|
86
|
+
* a sensitive keyword. Two-stage gating keeps false positives low:
|
|
87
|
+
* 1. The substring is at least `minLength` chars of token-character class.
|
|
88
|
+
* 2. Shannon entropy ≥ `minEntropy` bits/char.
|
|
89
|
+
* 3. Either the substring or the surrounding text contains a secret
|
|
90
|
+
* keyword (api_key, token, secret, etc.).
|
|
91
|
+
*/
|
|
92
|
+
export function runEntropyDetector(text, options = {}) {
|
|
93
|
+
if (options.enabled === false)
|
|
94
|
+
return [];
|
|
95
|
+
const minLength = options.minLength ?? 32;
|
|
96
|
+
const minEntropy = options.minEntropy ?? 4.0;
|
|
97
|
+
const out = [];
|
|
98
|
+
HIGH_ENTROPY_CANDIDATE.lastIndex = 0;
|
|
99
|
+
let m;
|
|
100
|
+
while ((m = HIGH_ENTROPY_CANDIDATE.exec(text)) !== null) {
|
|
101
|
+
const token = m[0];
|
|
102
|
+
if (token.length < minLength)
|
|
103
|
+
continue;
|
|
104
|
+
const entropy = shannonEntropy(token);
|
|
105
|
+
if (entropy < minEntropy)
|
|
106
|
+
continue;
|
|
107
|
+
// Avoid flagging plain decimal numbers (very low entropy but pass length).
|
|
108
|
+
if (/^[0-9]+$/.test(token))
|
|
109
|
+
continue;
|
|
110
|
+
// Context check: keyword either in the candidate itself or in the surrounding 80 chars.
|
|
111
|
+
const start = Math.max(0, m.index - 40);
|
|
112
|
+
const end = Math.min(text.length, m.index + token.length + 40);
|
|
113
|
+
const context = text.slice(start, end);
|
|
114
|
+
if (!SECRET_KEYWORD_CONTEXT.test(context))
|
|
115
|
+
continue;
|
|
116
|
+
out.push({ excerpt: truncate(token), entropy: Math.round(entropy * 100) / 100 });
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
function truncate(s, maxLen = 48) {
|
|
121
|
+
if (s.length <= maxLen)
|
|
122
|
+
return s;
|
|
123
|
+
return s.slice(0, Math.max(8, maxLen / 2)) + '…' + s.slice(-Math.max(4, maxLen / 4));
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=security-detectors.js.map
|