@100xprompt/brain 0.0.0-stage → 0.1.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/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Secret filtering for everything the brain stores in memory or shows through a tool. Deterministic, offline, idempotent:
|
|
2
|
+
// a secret becomes `[redacted]`, anything else is left alone. Deliberately conservative about what counts as a secret
|
|
3
|
+
// (git hashes, UUIDs, file paths and ordinary prose must survive), and aggressive about the shapes that are always secrets
|
|
4
|
+
// (provider key prefixes, PEM blocks, JWTs, bearer headers, `NAME_KEY=value` assignments, credentials inside URLs).
|
|
5
|
+
|
|
6
|
+
const MARK = "[redacted]"
|
|
7
|
+
|
|
8
|
+
// Values that are references to a secret, not the secret: `$API_KEY`, `${TOKEN}`, `process.env.X`, `<your-key>`, `getToken()`.
|
|
9
|
+
const REFERENCE_VALUE = /^(?:\$|\{|<|%|process\.|os\.|env\b|ENV\b|config\.|settings\.|secrets?\.|args\.|opts?\.|options\.|this\.|self\.|null\b|undefined\b|true\b|false\b|none\b|None\b|\[redacted\]|\*+$|x{3,}$|\.{3})/i
|
|
10
|
+
|
|
11
|
+
const PROVIDER_TOKENS: RegExp[] = [
|
|
12
|
+
/sk-(?:ant|proj|live|or-v1|svcacct)?[-_]?[A-Za-z0-9_-]{20,}/g, // OpenAI, Anthropic, OpenRouter and look-alikes
|
|
13
|
+
/\bsk_(?:live|test)_[A-Za-z0-9]{16,}\b/g, // Stripe secret keys
|
|
14
|
+
/\b[rp]k_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
|
|
15
|
+
/\bAIza[0-9A-Za-z_-]{30,}\b/g, // Google API keys
|
|
16
|
+
/\bgh[pousr]_[A-Za-z0-9]{30,}\b/g, // GitHub tokens
|
|
17
|
+
/\bgithub_pat_[A-Za-z0-9_]{40,}\b/g,
|
|
18
|
+
/\bglpat-[A-Za-z0-9_-]{16,}\b/g, // GitLab
|
|
19
|
+
/\bxox[abeprs]-[A-Za-z0-9-]{10,}\b/g, // Slack
|
|
20
|
+
/\b(?:AKIA|ASIA|AGPA|AIDA|AROA)[0-9A-Z]{16}\b/g, // AWS access key ids
|
|
21
|
+
/\bnpm_[A-Za-z0-9]{30,}\b/g,
|
|
22
|
+
/\bhf_[A-Za-z0-9]{30,}\b/g,
|
|
23
|
+
/\bya29\.[0-9A-Za-z_-]{20,}/g, // Google OAuth access tokens
|
|
24
|
+
/\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, // SendGrid
|
|
25
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, // JWT
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const PEM = /-----BEGIN [A-Z0-9 ]*(?:PRIVATE KEY|CERTIFICATE|PGP PRIVATE KEY BLOCK)-----[\s\S]*?(?:-----END [A-Z0-9 ]*-----|$)/g
|
|
29
|
+
|
|
30
|
+
const AUTH_HEADER = /\b(Authorization|Proxy-Authorization|X-Api-Key|X-Auth-Token|Api-Key)(["']?\s*[:=]\s*["']?)(?:(?:Bearer|Basic|Token|Digest)\s+)?[^\s"'`,;]{6,}/gi
|
|
31
|
+
|
|
32
|
+
const URL_CREDENTIALS = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"']+:[^\s/@"']+@/gi
|
|
33
|
+
|
|
34
|
+
// NAME = value, where NAME ends in key/token/secret/password-like words. A bare `key` is not enough (prose, map keys).
|
|
35
|
+
const ASSIGNMENT =
|
|
36
|
+
/((?:^|[^\w.-])[\w.-]*(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?key|private[_-]?key|auth[_-]?key|client[_-]?secret|secret|token|password|passwd|pwd|credentials?|passphrase))(["']?\s*[:=]\s*["']?)(\[redacted\]|"[^"\n]*"|'[^'\n]*'|[^\s"'`,;)}\]]+)/gi
|
|
37
|
+
|
|
38
|
+
// A long unbroken mixed-case+digit run (base64/hex-ish secrets). Hex-only runs are left alone (git and content hashes).
|
|
39
|
+
const LONG_RUN = /(?<![\w/.+=-])[A-Za-z0-9_+-]{40,}={0,2}(?![\w/.+=-])/g
|
|
40
|
+
|
|
41
|
+
function isMixedSecret(s: string): boolean {
|
|
42
|
+
if (/^[0-9a-f]+$/i.test(s)) return false // git sha, sha256, md5: public identifiers
|
|
43
|
+
if (/^[a-z]+(?:[-_][a-z]+)+$/i.test(s)) return false // kebab/snake words
|
|
44
|
+
const upper = /[A-Z]/.test(s)
|
|
45
|
+
const lower = /[a-z]/.test(s)
|
|
46
|
+
const digit = /\d/.test(s)
|
|
47
|
+
const kinds = [upper, lower, digit].filter(Boolean).length
|
|
48
|
+
if (kinds < 3) return false
|
|
49
|
+
// identifiers like VeryLongCamelCaseFunctionName2 have few digit/letter switches; random strings switch a lot
|
|
50
|
+
let switches = 0
|
|
51
|
+
for (let i = 1; i < s.length; i++) if (/\d/.test(s[i]!) !== /\d/.test(s[i - 1]!)) switches++
|
|
52
|
+
return switches >= 6
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function redact(text: string): string {
|
|
56
|
+
if (!text) return text
|
|
57
|
+
let out = text
|
|
58
|
+
out = out.replace(PEM, MARK)
|
|
59
|
+
out = out.replace(AUTH_HEADER, (_m, name: string, sep: string) => `${name}${sep}${MARK}`)
|
|
60
|
+
out = out.replace(URL_CREDENTIALS, (_m, scheme: string) => `${scheme}${MARK}@`)
|
|
61
|
+
for (const re of PROVIDER_TOKENS) out = out.replace(re, MARK)
|
|
62
|
+
out = out.replace(ASSIGNMENT, (m, name: string, sep: string, value: string) => {
|
|
63
|
+
const bare = value.replace(/^["']|["']$/g, "")
|
|
64
|
+
if (bare.length < 4 || REFERENCE_VALUE.test(bare) || bare.includes("(")) return m
|
|
65
|
+
return `${name}${sep}${MARK}`
|
|
66
|
+
})
|
|
67
|
+
out = out.replace(LONG_RUN, (m) => (isMixedSecret(m) ? MARK : m))
|
|
68
|
+
return out
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** True when `redact` would change the text. */
|
|
72
|
+
export const hasSecret = (text: string): boolean => redact(text) !== text
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Which part of a long stored output to show when the model asks for it without saying where to look.
|
|
2
|
+
//
|
|
3
|
+
// Why not "the first N characters": an agent given a truncated first chunk almost never asks for the next one
|
|
4
|
+
// ("Agents Don't Paginate", arXiv 2608.26130), and the thing that matters in a log (the failure) is at its end.
|
|
5
|
+
// So the default window is the head, the tail and, when the caller says what it is after, the best-matching
|
|
6
|
+
// chunk(s) of the middle, ranked by BM25 over line-aligned chunks. No model, deterministic.
|
|
7
|
+
//
|
|
8
|
+
// Every omission is marked with the exact character range left out (offsets into the SAME text the caller sees,
|
|
9
|
+
// so `offset=` / `grep=` continue from it).
|
|
10
|
+
|
|
11
|
+
/** Target size of one relevance chunk, in characters. */
|
|
12
|
+
export const CHUNK_CHARS = 600
|
|
13
|
+
|
|
14
|
+
export interface Span {
|
|
15
|
+
start: number
|
|
16
|
+
end: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Splits text into consecutive spans of about `target` characters that end on a line boundary (a line break is kept
|
|
21
|
+
* at the end of its span). A line longer than `target` is cut into pieces of `target` characters.
|
|
22
|
+
* The spans cover the text exactly: contiguous, no gaps, no overlap.
|
|
23
|
+
*/
|
|
24
|
+
export function chunkSpans(text: string, target = CHUNK_CHARS): Span[] {
|
|
25
|
+
const spans: Span[] = []
|
|
26
|
+
let start = 0
|
|
27
|
+
let i = 0
|
|
28
|
+
const n = text.length
|
|
29
|
+
while (i < n) {
|
|
30
|
+
const nl = text.indexOf("\n", i)
|
|
31
|
+
const lineEnd = nl < 0 ? n : nl + 1
|
|
32
|
+
if (lineEnd - i > target) {
|
|
33
|
+
// A long line: close the open chunk first, then cut the line itself.
|
|
34
|
+
if (i > start) spans.push({ start, end: i })
|
|
35
|
+
let p = i
|
|
36
|
+
while (lineEnd - p > target) {
|
|
37
|
+
spans.push({ start: p, end: p + target })
|
|
38
|
+
p += target
|
|
39
|
+
}
|
|
40
|
+
start = p
|
|
41
|
+
i = lineEnd
|
|
42
|
+
if (i - start >= target) {
|
|
43
|
+
spans.push({ start, end: i })
|
|
44
|
+
start = i
|
|
45
|
+
}
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
i = lineEnd
|
|
49
|
+
if (i - start >= target) {
|
|
50
|
+
spans.push({ start, end: i })
|
|
51
|
+
start = i
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (start < n) spans.push({ start, end: n })
|
|
55
|
+
return spans
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const STOP = new Set(["the", "a", "an", "and", "or", "of", "to", "in", "on", "is", "it", "for", "at", "by", "as", "be", "this", "that", "with"])
|
|
59
|
+
|
|
60
|
+
/** Lower-cased word tokens (letters, digits, underscore; any script), stop words and one-letter tokens dropped. */
|
|
61
|
+
export function tokenize(s: string): string[] {
|
|
62
|
+
const out: string[] = []
|
|
63
|
+
for (const m of s.toLowerCase().matchAll(/[\p{L}\p{N}_]+/gu)) {
|
|
64
|
+
const t = m[0]
|
|
65
|
+
if (t.length > 1 && !STOP.has(t)) out.push(t)
|
|
66
|
+
}
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* BM25 score of each chunk for the query (k1 1.2, b 0.75, the usual idf with +1 so it is never negative).
|
|
72
|
+
* A chunk with no query term scores 0.
|
|
73
|
+
*/
|
|
74
|
+
export function bm25(query: string, chunks: string[]): number[] {
|
|
75
|
+
const q = [...new Set(tokenize(query))]
|
|
76
|
+
const scores = new Array<number>(chunks.length).fill(0)
|
|
77
|
+
if (!q.length || !chunks.length) return scores
|
|
78
|
+
const docs = chunks.map((c) => {
|
|
79
|
+
const tf = new Map<string, number>()
|
|
80
|
+
let len = 0
|
|
81
|
+
for (const t of tokenize(c)) {
|
|
82
|
+
tf.set(t, (tf.get(t) ?? 0) + 1)
|
|
83
|
+
len++
|
|
84
|
+
}
|
|
85
|
+
return { tf, len }
|
|
86
|
+
})
|
|
87
|
+
const avg = docs.reduce((a, d) => a + d.len, 0) / docs.length || 1
|
|
88
|
+
const N = docs.length
|
|
89
|
+
for (const term of q) {
|
|
90
|
+
let df = 0
|
|
91
|
+
for (const d of docs) if (d.tf.has(term)) df++
|
|
92
|
+
if (!df) continue
|
|
93
|
+
const idf = Math.log(1 + (N - df + 0.5) / (df + 0.5))
|
|
94
|
+
docs.forEach((d, i) => {
|
|
95
|
+
const f = d.tf.get(term)
|
|
96
|
+
if (!f) return
|
|
97
|
+
scores[i]! += idf * ((f * 2.2) / (f + 1.2 * (0.25 + 0.75 * (d.len / avg))))
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
return scores
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const count = (s: string, ch: string) => {
|
|
104
|
+
let n = 0
|
|
105
|
+
for (let i = s.indexOf(ch); i >= 0; i = s.indexOf(ch, i + 1)) n++
|
|
106
|
+
return n
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The marker for a skipped range [a, b) of `text`. */
|
|
110
|
+
export function omission(text: string, a: number, b: number): string {
|
|
111
|
+
const lines = count(text.slice(a, b), "\n")
|
|
112
|
+
return `[brain: omitted characters ${a}-${b} (${b - a} chars, ${lines} line break${lines === 1 ? "" : "s"}); get them with offset=${a} limit=${b - a}, or grep=<regex>]`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const MARKER_RESERVE = 190
|
|
116
|
+
const MAX_MIDDLE = 3
|
|
117
|
+
|
|
118
|
+
export interface WindowOptions {
|
|
119
|
+
/** What the caller is after: the best-matching chunks of the middle are added. */
|
|
120
|
+
query?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The text itself when it fits in `budget` characters; otherwise a window of about `budget` characters (markers
|
|
125
|
+
* included) made of the head, the tail and (with a query that matches something) up to three middle chunks, in
|
|
126
|
+
* file order, each gap replaced by an omission marker. With no query the room goes to head and tail equally;
|
|
127
|
+
* with one, head 35%, tail 35%, middle 30%.
|
|
128
|
+
*/
|
|
129
|
+
export function windowOf(text: string, budget: number, opts: WindowOptions = {}): string {
|
|
130
|
+
budget = Math.max(600, Math.floor(budget))
|
|
131
|
+
if (text.length <= budget) return text
|
|
132
|
+
const n = text.length
|
|
133
|
+
const content = budget - MARKER_RESERVE * (MAX_MIDDLE + 2)
|
|
134
|
+
const room = Math.max(200, content)
|
|
135
|
+
const hasQuery = !!opts.query && tokenize(opts.query).length > 0
|
|
136
|
+
|
|
137
|
+
const spans = hasQuery ? chunkSpans(text) : []
|
|
138
|
+
const scores = hasQuery ? bm25(opts.query!, spans.map((s) => text.slice(s.start, s.end))) : []
|
|
139
|
+
const anyHit = scores.some((s) => s > 0)
|
|
140
|
+
const headB = Math.floor(room * (anyHit ? 0.35 : 0.5))
|
|
141
|
+
const tailB = Math.floor(room * (anyHit ? 0.35 : 0.5))
|
|
142
|
+
const midB = room - headB - tailB
|
|
143
|
+
|
|
144
|
+
// Head ends on a line boundary when one is near; the tail starts on one.
|
|
145
|
+
let headEnd = headB
|
|
146
|
+
if (text[headEnd - 1] !== "\n") {
|
|
147
|
+
const k = text.lastIndexOf("\n", headEnd - 1)
|
|
148
|
+
if (k + 1 >= headB * 0.5) headEnd = k + 1
|
|
149
|
+
}
|
|
150
|
+
let tailStart = n - tailB
|
|
151
|
+
if (text[tailStart - 1] !== "\n") {
|
|
152
|
+
const k = text.indexOf("\n", tailStart)
|
|
153
|
+
if (k >= 0 && k + 1 - tailStart <= tailB * 0.5) tailStart = k + 1
|
|
154
|
+
}
|
|
155
|
+
if (headEnd >= tailStart) return text // cannot happen when text.length > budget; stay safe
|
|
156
|
+
|
|
157
|
+
const picked: Span[] = []
|
|
158
|
+
if (anyHit) {
|
|
159
|
+
const order = spans
|
|
160
|
+
.map((s, i) => ({ s, score: scores[i]! }))
|
|
161
|
+
.filter((x) => x.score > 0 && x.s.start >= headEnd && x.s.end <= tailStart)
|
|
162
|
+
.sort((a, b) => b.score - a.score || a.s.start - b.s.start)
|
|
163
|
+
let used = 0
|
|
164
|
+
for (const x of order) {
|
|
165
|
+
if (picked.length >= MAX_MIDDLE) break
|
|
166
|
+
const size = x.s.end - x.s.start
|
|
167
|
+
if (used + size > midB) continue
|
|
168
|
+
picked.push(x.s)
|
|
169
|
+
used += size
|
|
170
|
+
}
|
|
171
|
+
picked.sort((a, b) => a.start - b.start)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const parts: string[] = [text.slice(0, headEnd)]
|
|
175
|
+
let at = headEnd
|
|
176
|
+
const gap = (to: number) => {
|
|
177
|
+
if (to > at) parts.push(`${parts[parts.length - 1]!.endsWith("\n") ? "" : "\n"}${omission(text, at, to)}\n`)
|
|
178
|
+
}
|
|
179
|
+
for (const s of picked) {
|
|
180
|
+
gap(s.start)
|
|
181
|
+
parts.push(text.slice(s.start, s.end))
|
|
182
|
+
at = s.end
|
|
183
|
+
}
|
|
184
|
+
gap(tailStart)
|
|
185
|
+
parts.push(text.slice(tailStart))
|
|
186
|
+
return parts.join("")
|
|
187
|
+
}
|