@hoilab/ada-cli 0.84.12 → 0.84.13

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.
Files changed (62) hide show
  1. package/dist/core/agent-session.d.ts +11 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +84 -1
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/memory-engine/audit.d.ts +46 -0
  6. package/dist/core/memory-engine/audit.d.ts.map +1 -0
  7. package/dist/core/memory-engine/audit.js +165 -0
  8. package/dist/core/memory-engine/audit.js.map +1 -0
  9. package/dist/core/memory-engine/engine.d.ts +158 -0
  10. package/dist/core/memory-engine/engine.d.ts.map +1 -0
  11. package/dist/core/memory-engine/engine.js +948 -0
  12. package/dist/core/memory-engine/engine.js.map +1 -0
  13. package/dist/core/memory-engine/inverted-index.d.ts +46 -0
  14. package/dist/core/memory-engine/inverted-index.d.ts.map +1 -0
  15. package/dist/core/memory-engine/inverted-index.js +235 -0
  16. package/dist/core/memory-engine/inverted-index.js.map +1 -0
  17. package/dist/core/memory-engine/observation-store.d.ts +101 -0
  18. package/dist/core/memory-engine/observation-store.d.ts.map +1 -0
  19. package/dist/core/memory-engine/observation-store.js +429 -0
  20. package/dist/core/memory-engine/observation-store.js.map +1 -0
  21. package/dist/core/memory-engine/scanner.d.ts +11 -0
  22. package/dist/core/memory-engine/scanner.d.ts.map +1 -0
  23. package/dist/core/memory-engine/scanner.js +66 -0
  24. package/dist/core/memory-engine/scanner.js.map +1 -0
  25. package/dist/core/memory-engine/security.d.ts +31 -0
  26. package/dist/core/memory-engine/security.d.ts.map +1 -0
  27. package/dist/core/memory-engine/security.js +180 -0
  28. package/dist/core/memory-engine/security.js.map +1 -0
  29. package/dist/core/memory-engine/session-indexer.d.ts +71 -0
  30. package/dist/core/memory-engine/session-indexer.d.ts.map +1 -0
  31. package/dist/core/memory-engine/session-indexer.js +217 -0
  32. package/dist/core/memory-engine/session-indexer.js.map +1 -0
  33. package/dist/core/memory-engine/tools.d.ts +53 -0
  34. package/dist/core/memory-engine/tools.d.ts.map +1 -0
  35. package/dist/core/memory-engine/tools.js +220 -0
  36. package/dist/core/memory-engine/tools.js.map +1 -0
  37. package/dist/core/memory-engine/types.d.ts +134 -0
  38. package/dist/core/memory-engine/types.d.ts.map +1 -0
  39. package/dist/core/memory-engine/types.js +49 -0
  40. package/dist/core/memory-engine/types.js.map +1 -0
  41. package/dist/core/resource-loader.d.ts +4 -0
  42. package/dist/core/resource-loader.d.ts.map +1 -1
  43. package/dist/core/resource-loader.js +27 -8
  44. package/dist/core/resource-loader.js.map +1 -1
  45. package/dist/core/settings-manager.d.ts +48 -0
  46. package/dist/core/settings-manager.d.ts.map +1 -1
  47. package/dist/core/settings-manager.js +75 -0
  48. package/dist/core/settings-manager.js.map +1 -1
  49. package/dist/index.d.ts +9 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +9 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  54. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  55. package/dist/modes/interactive/components/settings-selector.js +12 -0
  56. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  57. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  58. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  59. package/dist/modes/interactive/interactive-mode.js +121 -0
  60. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  61. package/npm-shrinkwrap.json +2 -2
  62. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.js","sourceRoot":"","sources":["../../../src/core/memory-engine/scanner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,MAAM,eAAe,GAAgB;IACpC,EAAE,OAAO,EAAE,qDAAqD,EAAE,EAAE,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC5G,EAAE,OAAO,EAAE,qBAAqB,EAAE,EAAE,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE;IACvE,EAAE,OAAO,EAAE,+BAA+B,EAAE,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE;IACpF,EAAE,OAAO,EAAE,6BAA6B,EAAE,EAAE,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,EAAE;IACvF,EAAE,OAAO,EAAE,+DAA+D,EAAE,EAAE,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,EAAE;IACrH,EAAE,OAAO,EAAE,yFAAyF,EAAE,EAAE,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,EAAE;IACnJ,EAAE,OAAO,EAAE,kEAAkE,EAAE,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE;IACnH,EAAE,OAAO,EAAE,kEAAkE,EAAE,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE;IACnH,EAAE,OAAO,EAAE,oEAAoE,EAAE,EAAE,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE;IACvH,EAAE,OAAO,EAAE,kBAAkB,EAAE,EAAE,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE;IACrE,EAAE,OAAO,EAAE,yBAAyB,EAAE,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE;CAC1E,CAAC;AAEF,MAAM,eAAe,GAAgB;IACpC,EAAE,OAAO,EAAE,uBAAuB,EAAE,EAAE,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC/E,EAAE,OAAO,EAAE,sBAAsB,EAAE,EAAE,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC/E,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE;IACrE,EAAE,OAAO,EAAE,sBAAsB,EAAE,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC3E,EAAE,OAAO,EAAE,iBAAiB,EAAE,EAAE,EAAE,uBAAuB,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC7E,EAAE,OAAO,EAAE,iBAAiB,EAAE,EAAE,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,EAAE;IACzE,EAAE,OAAO,EAAE,kBAAkB,EAAE,EAAE,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,EAAE;IACxE,EAAE,OAAO,EAAE,kBAAkB,EAAE,EAAE,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,EAAE;IACxE,EAAE,OAAO,EAAE,iBAAiB,EAAE,EAAE,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE;IACpE,EAAE,OAAO,EAAE,sBAAsB,EAAE,EAAE,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,EAAE;IAC9E,EAAE,OAAO,EAAE,2CAA2C,EAAE,EAAE,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,EAAE;IACnG,EAAE,OAAO,EAAE,uBAAuB,EAAE,EAAE,EAAE,mBAAmB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IACjF,EAAE,OAAO,EAAE,oBAAoB,EAAE,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC3E,EAAE,OAAO,EAAE,wBAAwB,EAAE,EAAE,EAAE,oBAAoB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IACnF,EAAE,OAAO,EAAE,kBAAkB,EAAE,EAAE,EAAE,kBAAkB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC3E,EAAE,OAAO,EAAE,2BAA2B,EAAE,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAClF,EAAE,OAAO,EAAE,kBAAkB,EAAE,EAAE,EAAE,kBAAkB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC3E,EAAE,OAAO,EAAE,+BAA+B,EAAE,EAAE,EAAE,qBAAqB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC3F,EAAE,OAAO,EAAE,6BAA6B,EAAE,EAAE,EAAE,mBAAmB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IACvF,EAAE,OAAO,EAAE,6BAA6B,EAAE,EAAE,EAAE,kBAAkB,EAAE,QAAQ,EAAE,QAAQ,EAAE;CACtF,CAAC;AAEF,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC/B,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ;IAChD,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ;CAChD,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAiB;IAC3D,MAAM,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/E,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,+EAA+E,CAAC;IACxF,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,eAAe,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACjC,OAAO,2EAA2E,KAAK,CAAC,EAAE,IAAI,CAAC;QAChG,CAAC;IACF,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,eAAe,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACjC,OAAO,8DAA8D,KAAK,CAAC,EAAE,wCAAwC,CAAC;QACvH,CAAC;IACF,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ","sourcesContent":["/**\n * Content scanner — blocks prompt-injection, exfiltration and secret leaks\n * from ever being persisted into memory (pattern set captured from\n * pi-hermes-memory's content-scanner, which ported it from Hermes).\n */\n\ninterface ScanIssue {\n\tpattern: RegExp;\n\tid: string;\n\tseverity: \"high\" | \"medium\";\n}\n\nconst THREAT_PATTERNS: ScanIssue[] = [\n\t{ pattern: /ignore\\s+(previous|all|above|prior)\\s+instructions/i, id: \"prompt_injection\", severity: \"high\" },\n\t{ pattern: /you\\s+are\\s+now\\s+/i, id: \"role_hijack\", severity: \"high\" },\n\t{ pattern: /do\\s+not\\s+tell\\s+the\\s+user/i, id: \"deception_hide\", severity: \"high\" },\n\t{ pattern: /system\\s+prompt\\s+override/i, id: \"sys_prompt_override\", severity: \"high\" },\n\t{ pattern: /disregard\\s+(your|all|any)\\s+(instructions|rules|guidelines)/i, id: \"disregard_rules\", severity: \"high\" },\n\t{ pattern: /act\\s+as\\s+(if|though)\\s+you\\s+(have\\s+no|don'?t\\s+have)\\s+(restrictions|limits|rules)/i, id: \"bypass_restrictions\", severity: \"high\" },\n\t{ pattern: /curl\\s+[^\\n]*\\$\\{?\\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, id: \"exfil_curl\", severity: \"high\" },\n\t{ pattern: /wget\\s+[^\\n]*\\$\\{?\\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, id: \"exfil_wget\", severity: \"high\" },\n\t{ pattern: /cat\\s+[^\\n]*(\\.env|credentials|\\.netrc|\\.pgpass|\\.npmrc|\\.pypirc)/i, id: \"read_secrets\", severity: \"high\" },\n\t{ pattern: /authorized_keys/i, id: \"ssh_backdoor\", severity: \"high\" },\n\t{ pattern: /\\$HOME\\/\\.ssh|~\\/\\.ssh/i, id: \"ssh_access\", severity: \"high\" },\n];\n\nconst SECRET_PATTERNS: ScanIssue[] = [\n\t{ pattern: /\\bsk-ant-api\\S{10,}\\b/, id: \"anthropic_api_key\", severity: \"high\" },\n\t{ pattern: /\\bsk-or-v1-\\S{10,}\\b/, id: \"openrouter_api_key\", severity: \"high\" },\n\t{ pattern: /\\bsk-\\S{20,}\\b/, id: \"openai_api_key\", severity: \"high\" },\n\t{ pattern: /\\bAKIA[0-9A-Z]{16}\\b/, id: \"aws_access_key\", severity: \"high\" },\n\t{ pattern: /\\bghp_\\S{10,}\\b/, id: \"github_personal_token\", severity: \"high\" },\n\t{ pattern: /\\bghu_\\S{10,}\\b/, id: \"github_user_token\", severity: \"high\" },\n\t{ pattern: /\\bxoxb-\\S{10,}\\b/, id: \"slack_bot_token\", severity: \"high\" },\n\t{ pattern: /\\bxapp-\\S{10,}\\b/, id: \"slack_app_token\", severity: \"high\" },\n\t{ pattern: /\\bntn_\\S{10,}\\b/, id: \"notion_token\", severity: \"high\" },\n\t{ pattern: /\\bBearer\\s+\\S{20,}\\b/, id: \"bearer_auth_token\", severity: \"high\" },\n\t{ pattern: /-----BEGIN\\s+(?:RSA\\s+)?PRIVATE\\sKEY-----/, id: \"private_key_block\", severity: \"high\" },\n\t{ pattern: /\\bANTHROPIC_API_KEY\\b/, id: \"env_anthropic_key\", severity: \"medium\" },\n\t{ pattern: /\\bOPENAI_API_KEY\\b/, id: \"env_openai_key\", severity: \"medium\" },\n\t{ pattern: /\\bOPENROUTER_API_KEY\\b/, id: \"env_openrouter_key\", severity: \"medium\" },\n\t{ pattern: /\\bGITHUB_TOKEN\\b/, id: \"env_github_token\", severity: \"medium\" },\n\t{ pattern: /\\bAWS_SECRET_ACCESS_KEY\\b/, id: \"env_aws_secret\", severity: \"medium\" },\n\t{ pattern: /\\bDATABASE_URL\\b/, id: \"env_database_url\", severity: \"medium\" },\n\t{ pattern: /\\bpassword\\s*[=:]\\s*\\S{6,}\\b/i, id: \"password_assignment\", severity: \"medium\" },\n\t{ pattern: /\\bsecret\\s*[=:]\\s*\\S{6,}\\b/i, id: \"secret_assignment\", severity: \"medium\" },\n\t{ pattern: /\\btoken\\s*[=:]\\s*\\S{10,}\\b/i, id: \"token_assignment\", severity: \"medium\" },\n];\n\nconst INVISIBLE_CHARS = new Set([\n\t\"\\u200b\", \"\\u200c\", \"\\u200d\", \"\\u2060\", \"\\ufeff\",\n\t\"\\u202a\", \"\\u202b\", \"\\u202c\", \"\\u202d\", \"\\u202e\",\n]);\n\n/**\n * Scan memory content before persisting. Returns an error message when the\n * content must be rejected, or null when it is safe to store.\n */\nexport function scanContent(content: string): string | null {\n\tconst visible = [...content].filter((ch) => !INVISIBLE_CHARS.has(ch)).join(\"\");\n\tif (visible !== content) {\n\t\treturn \"Content contains invisible/zero-width characters and was rejected for safety.\";\n\t}\n\tfor (const issue of THREAT_PATTERNS) {\n\t\tif (issue.pattern.test(content)) {\n\t\t\treturn `Content was rejected: looks like prompt injection or data exfiltration (${issue.id}).`;\n\t\t}\n\t}\n\tfor (const issue of SECRET_PATTERNS) {\n\t\tif (issue.pattern.test(content)) {\n\t\t\treturn `Content was rejected: possible secret/credential detected (${issue.id}). Secrets are never stored in memory.`;\n\t\t}\n\t}\n\treturn null;\n}\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Security & privacy classification for the Ada Memory Engine.
3
+ *
4
+ * Enterprise-grade guards:
5
+ * - Credentials/secrets/payment data are ALWAYS blocked at every level.
6
+ * - PII (email, phone, national IDs, IBAN, DOB) blocking depends on the
7
+ * configured privacy level (RGPD / AI Act friendly).
8
+ * - Every rejection is written to the audit log with a reason.
9
+ * - Prompt-injection/exfiltration patterns are always blocked.
10
+ */
11
+ export type PrivacyLevel = "strict" | "balanced" | "permissive";
12
+ export type SensitiveCategory = "credential" | "payment" | "secret-assignment" | "pii-email" | "pii-phone" | "pii-id" | "pii-bank" | "pii-dob" | "injection" | "exfiltration";
13
+ export interface SensitivityResult {
14
+ /** True when the content must NOT be stored. */
15
+ blocked: boolean;
16
+ /** Categories detected. */
17
+ categories: SensitiveCategory[];
18
+ /** Human-readable reason (in the configured language). */
19
+ reason: string;
20
+ /** True when content contains permitted-but-noteworthy categories. */
21
+ noteworthy: boolean;
22
+ }
23
+ /**
24
+ * Classify content against the security policy.
25
+ *
26
+ * @param content Text to classify.
27
+ * @param level Configured privacy level.
28
+ * @param locale "es" or "en" for the reason language.
29
+ */
30
+ export declare function classifySensitive(content: string, level?: PrivacyLevel, locale?: "es" | "en"): SensitivityResult;
31
+ //# sourceMappingURL=security.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../../src/core/memory-engine/security.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,CAAC;AAEhE,MAAM,MAAM,iBAAiB,GAC1B,YAAY,GACZ,SAAS,GACT,mBAAmB,GACnB,WAAW,GACX,WAAW,GACX,QAAQ,GACR,UAAU,GACV,SAAS,GACT,WAAW,GACX,cAAc,CAAC;AAElB,MAAM,WAAW,iBAAiB;IACjC,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,2BAA2B;IAC3B,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,UAAU,EAAE,OAAO,CAAC;CACpB;AAyID;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,YAAuB,EAAE,MAAM,GAAE,IAAI,GAAG,IAAW,GAAG,iBAAiB,CA+ChI","sourcesContent":["/**\n * Security & privacy classification for the Ada Memory Engine.\n *\n * Enterprise-grade guards:\n * - Credentials/secrets/payment data are ALWAYS blocked at every level.\n * - PII (email, phone, national IDs, IBAN, DOB) blocking depends on the\n * configured privacy level (RGPD / AI Act friendly).\n * - Every rejection is written to the audit log with a reason.\n * - Prompt-injection/exfiltration patterns are always blocked.\n */\n\nexport type PrivacyLevel = \"strict\" | \"balanced\" | \"permissive\";\n\nexport type SensitiveCategory =\n\t| \"credential\"\n\t| \"payment\"\n\t| \"secret-assignment\"\n\t| \"pii-email\"\n\t| \"pii-phone\"\n\t| \"pii-id\"\n\t| \"pii-bank\"\n\t| \"pii-dob\"\n\t| \"injection\"\n\t| \"exfiltration\";\n\nexport interface SensitivityResult {\n\t/** True when the content must NOT be stored. */\n\tblocked: boolean;\n\t/** Categories detected. */\n\tcategories: SensitiveCategory[];\n\t/** Human-readable reason (in the configured language). */\n\treason: string;\n\t/** True when content contains permitted-but-noteworthy categories. */\n\tnoteworthy: boolean;\n}\n\ninterface Pattern {\n\tcategory: SensitiveCategory;\n\tpattern: RegExp;\n\t/** Always blocked regardless of privacy level. */\n\talways?: boolean;\n\t/** Blocked at this level and stricter. */\n\tblockAt?: PrivacyLevel;\n\t/** English reason label. */\n\tlabel: string;\n}\n\n// ─── Credentials & secrets — ALWAYS blocked ───\nconst ALWAYS_BLOCKED: Pattern[] = [\n\t{ category: \"credential\", pattern: /\\bsk-ant-api\\S{10,}\\b/, always: true, label: \"Anthropic API key\" },\n\t{ category: \"credential\", pattern: /\\bsk-or-v1-\\S{10,}\\b/, always: true, label: \"OpenRouter API key\" },\n\t{ category: \"credential\", pattern: /\\bsk-[A-Za-z0-9]{20,}\\b/, always: true, label: \"OpenAI-style API key\" },\n\t{ category: \"credential\", pattern: /\\bAKIA[0-9A-Z]{16}\\b/, always: true, label: \"AWS access key\" },\n\t{ category: \"credential\", pattern: /\\bghp_[A-Za-z0-9]{20,}\\b/, always: true, label: \"GitHub personal token\" },\n\t{ category: \"credential\", pattern: /\\bghu_[A-Za-z0-9]{20,}\\b/, always: true, label: \"GitHub user token\" },\n\t{ category: \"credential\", pattern: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/, always: true, label: \"Slack token\" },\n\t{ category: \"credential\", pattern: /\\bxapp-[A-Za-z0-9-]{10,}\\b/, always: true, label: \"Slack app token\" },\n\t{ category: \"credential\", pattern: /\\bntn_[A-Za-z0-9]{20,}\\b/, always: true, label: \"Notion token\" },\n\t{ category: \"credential\", pattern: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}/, always: true, label: \"JWT token\" },\n\t{ category: \"credential\", pattern: /-----BEGIN\\s+(?:RSA\\s+|EC\\s+|OPENSSH\\s+)?PRIVATE\\s+KEY-----/, always: true, label: \"Private key block\" },\n\t{ category: \"credential\", pattern: /\\bBearer\\s+[A-Za-z0-9._~+/=-]{20,}\\b/i, always: true, label: \"Bearer auth token\" },\n\t{ category: \"credential\", pattern: /\\bapi[_-]?key\\s*[=:]\\s*[A-Za-z0-9_-]{16,}\\b/i, always: true, label: \"API key assignment\" },\n\t{ category: \"credential\", pattern: /\\b(client|app|consumer)_?secret\\s*[=:]\\s*[A-Za-z0-9_-]{16,}\\b/i, always: true, label: \"OAuth client secret\" },\n\t{ category: \"credential\", pattern: /\\bwebhook[_-]?url\\s*[=:]\\s*https?:\\/\\/[^\\s]+/i, always: true, label: \"Webhook URL\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(password|passwd|pwd)\\s*[=:]\\s*\\S{6,}\\b/i, always: true, label: \"password value\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(secret|token|api[_-]?key|access[_-]?key)\\s*[=:]\\s*\\S{10,}\\b/i, always: true, label: \"secret/token value\" },\n\t{ category: \"credential\", pattern: /\\b(ANTHROPIC|OPENAI|OPENROUTER|GEMINI|DEEPSEEK|MISTRAL|AZURE)_?(API)?_?KEY\\b/i, always: true, label: \"provider API key env var\" },\n\t{ category: \"credential\", pattern: /\\bGITHUB_TOKEN\\b|\\bAWS_SECRET_ACCESS_KEY\\b|\\bDATABASE_URL\\b/i, always: true, label: \"secret env var name\" },\n\t{ category: \"credential\", pattern: /(?:mongodb|postgres|mysql|redis|amqp):\\/\\/[^\\s]*:[^\\s@]+@/, always: true, label: \"connection string with credentials\" },\n];\n\n// ─── Payment data — ALWAYS blocked ───\nconst PAYMENT: Pattern[] = [\n\t{\n\t\tcategory: \"payment\",\n\t\tpattern: /\\b(?:4\\d{3}|5[1-5]\\d{2}|3[47]\\d{2}|6(?:011|5\\d{2}))[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b/,\n\t\talways: true,\n\t\tlabel: \"payment card number\",\n\t},\n\t{ category: \"payment\", pattern: /\\bcvv\\s*[=:]\\s*\\d{3,4}\\b/i, always: true, label: \"card verification value\" },\n\t{ category: \"payment\", pattern: /\\b(?:expiry|exp)\\s*[=:]\\s*(?:0[1-9]|1[0-2])\\/?\\d{2}\\b/i, always: true, label: \"card expiry\" },\n];\n\n// ─── PII — level-dependent ───\nconst PII: Pattern[] = [\n\t{\n\t\tcategory: \"pii-email\",\n\t\tpattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"personal email address\",\n\t},\n\t{\n\t\tcategory: \"pii-phone\",\n\t\tpattern: /(?:\\+\\d{1,3}[\\s-]?)?(?:\\(?\\d{2,4}\\)?[\\s-]?)?\\d{3}[\\s-]?\\d{3}[\\s-]?\\d{3,4}\\b/,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"phone number\",\n\t},\n\t{\n\t\tcategory: \"pii-id\",\n\t\tpattern: /\\b\\d{8}[A-Z]\\b|\\b[A-Z]\\d{7}[A-Z0-9]\\b/, // DNI / NIE (Spain)\n\t\tblockAt: \"strict\",\n\t\tlabel: \"national ID (DNI/NIE)\",\n\t},\n\t{\n\t\tcategory: \"pii-bank\",\n\t\tpattern: /\\b[A-Z]{2}\\d{2}[A-Z0-9]{11,30}\\b/, // IBAN\n\t\tblockAt: \"balanced\",\n\t\tlabel: \"IBAN / bank account\",\n\t},\n\t{\n\t\tcategory: \"pii-dob\",\n\t\tpattern: /\\b(?:birth|birthday|born|nacimiento|fecha de nacimiento)\\s*[=:]\\s*(?:\\d{1,2}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{2,4})/i,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"date of birth\",\n\t},\n];\n\n// ─── Prompt injection / exfiltration — ALWAYS blocked ───\nconst INJECTION: Pattern[] = [\n\t{ category: \"injection\", pattern: /ignore\\s+(previous|all|above|prior)\\s+instructions/i, always: true, label: \"prompt injection\" },\n\t{ category: \"injection\", pattern: /you\\s+are\\s+now\\s+/i, always: true, label: \"role hijack\" },\n\t{ category: \"injection\", pattern: /do\\s+not\\s+tell\\s+the\\s+user/i, always: true, label: \"deceptive instruction\" },\n\t{ category: \"injection\", pattern: /disregard\\s+(your|all|any)\\s+(instructions|rules|guidelines)/i, always: true, label: \"instruction override\" },\n\t{ category: \"injection\", pattern: /system\\s+prompt\\s+override/i, always: true, label: \"system prompt override\" },\n\t{ category: \"exfiltration\", pattern: /curl\\s+[^\\n]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, always: true, label: \"credential exfiltration (curl)\" },\n\t{ category: \"exfiltration\", pattern: /wget\\s+[^\\n]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, always: true, label: \"credential exfiltration (wget)\" },\n\t{ category: \"exfiltration\", pattern: /cat\\s+[^\\n]*(\\.env|credentials|\\.netrc|\\.pgpass|\\.npmrc|\\.pypirc)/i, always: true, label: \"secret file read\" },\n\t{ category: \"exfiltration\", pattern: /\\$HOME\\/\\.ssh|~\\/\\.ssh/i, always: true, label: \"SSH key path\" },\n];\n\nconst LEVEL_ORDER: Record<PrivacyLevel, number> = { strict: 0, balanced: 1, permissive: 2 };\n\nconst INVISIBLE_CHARS = new Set([\n\t\"\\u200b\", \"\\u200c\", \"\\u200d\", \"\\u2060\", \"\\ufeff\",\n\t\"\\u202a\", \"\\u202b\", \"\\u202c\", \"\\u202d\", \"\\u202e\",\n]);\n\nconst REASON_EN = {\n\tcredential: \"Looks like a credential or secret (secrets are never stored in memory)\",\n\tpayment: \"Looks like payment card data (never stored in memory)\",\n\t\"secret-assignment\": \"Looks like a password/secret/token value (never stored in memory)\",\n\t\"pii-email\": \"Looks like a personal email address (blocked by the strict privacy level)\",\n\t\"pii-phone\": \"Looks like a phone number (blocked by the strict privacy level)\",\n\t\"pii-id\": \"Looks like a national ID (DNI/NIE) (blocked by the strict privacy level)\",\n\t\"pii-bank\": \"Looks like a bank account / IBAN (blocked by the privacy level)\",\n\t\"pii-dob\": \"Looks like a date of birth (blocked by the strict privacy level)\",\n\tinjection: \"Looks like a prompt-injection attempt (never stored)\",\n\texfiltration: \"Looks like data exfiltration (never stored)\",\n};\n\nconst REASON_ES: Record<SensitiveCategory, string> = {\n\tcredential: \"Parece una credencial o secreto (los secretos nunca se almacenan en memoria)\",\n\tpayment: \"Parecen datos de tarjeta de pago (nunca se almacenan en memoria)\",\n\t\"secret-assignment\": \"Parece una contraseña/secreto/token (nunca se almacena en memoria)\",\n\t\"pii-email\": \"Parece un correo electrónico personal (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-phone\": \"Parece un número de teléfono (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-id\": \"Parece un documento de identidad (DNI/NIE) (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-bank\": \"Parece una cuenta bancaria / IBAN (bloqueado por el nivel de privacidad)\",\n\t\"pii-dob\": \"Parece una fecha de nacimiento (bloqueada por el nivel de privacidad estricto)\",\n\tinjection: \"Parece un intento de prompt injection (nunca se almacena)\",\n\texfiltration: \"Parece exfiltración de datos (nunca se almacena)\",\n};\n\nfunction testPattern(pattern: Pattern, content: string): boolean {\n\ttry {\n\t\treturn pattern.pattern.test(content);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Classify content against the security policy.\n *\n * @param content Text to classify.\n * @param level Configured privacy level.\n * @param locale \"es\" or \"en\" for the reason language.\n */\nexport function classifySensitive(content: string, level: PrivacyLevel = \"strict\", locale: \"es\" | \"en\" = \"es\"): SensitivityResult {\n\tconst categories = new Set<SensitiveCategory>();\n\tconst reasons: string[] = [];\n\tconst labels: string[] = [];\n\n\t// Invisible characters are always suspicious (injection vectors).\n\tif ([...content].some((ch) => INVISIBLE_CHARS.has(ch))) {\n\t\treturn {\n\t\t\tblocked: true,\n\t\t\tcategories: [\"injection\"],\n\t\t\treason: locale === \"es\"\n\t\t\t\t? \"El contenido contiene caracteres invisibles/zero-width y fue rechazado por seguridad.\"\n\t\t\t\t: \"Content contains invisible/zero-width characters and was rejected for security.\",\n\t\t\tnoteworthy: true,\n\t\t};\n\t}\n\n\tconst allPatterns = [...ALWAYS_BLOCKED, ...PAYMENT, ...PII, ...INJECTION];\n\tfor (const pattern of allPatterns) {\n\t\tif (!testPattern(pattern, content)) continue;\n\t\tconst levelIdx = LEVEL_ORDER[level];\n\t\tconst blockAtIdx = pattern.blockAt ? LEVEL_ORDER[pattern.blockAt] : 0;\n\t\tconst blocked = pattern.always === true || levelIdx <= blockAtIdx;\n\t\tcategories.add(pattern.category);\n\t\tif (blocked) {\n\t\t\tlabels.push(pattern.label);\n\t\t\tif (!reasons.includes(pattern.category)) reasons.push(pattern.category);\n\t\t}\n\t}\n\n\tif (reasons.length > 0) {\n\t\tconst localeMap = locale === \"es\" ? REASON_ES : REASON_EN;\n\t\tconst detail = reasons.map((r) => localeMap[r as SensitiveCategory]).join(\"; \");\n\t\treturn {\n\t\t\tblocked: true,\n\t\t\tcategories: [...categories],\n\t\t\treason: detail,\n\t\t\tnoteworthy: true,\n\t\t};\n\t}\n\n\treturn {\n\t\tblocked: false,\n\t\tcategories: [...categories],\n\t\treason: \"\",\n\t\tnoteworthy: categories.size > 0,\n\t};\n}\n"]}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Security & privacy classification for the Ada Memory Engine.
3
+ *
4
+ * Enterprise-grade guards:
5
+ * - Credentials/secrets/payment data are ALWAYS blocked at every level.
6
+ * - PII (email, phone, national IDs, IBAN, DOB) blocking depends on the
7
+ * configured privacy level (RGPD / AI Act friendly).
8
+ * - Every rejection is written to the audit log with a reason.
9
+ * - Prompt-injection/exfiltration patterns are always blocked.
10
+ */
11
+ // ─── Credentials & secrets — ALWAYS blocked ───
12
+ const ALWAYS_BLOCKED = [
13
+ { category: "credential", pattern: /\bsk-ant-api\S{10,}\b/, always: true, label: "Anthropic API key" },
14
+ { category: "credential", pattern: /\bsk-or-v1-\S{10,}\b/, always: true, label: "OpenRouter API key" },
15
+ { category: "credential", pattern: /\bsk-[A-Za-z0-9]{20,}\b/, always: true, label: "OpenAI-style API key" },
16
+ { category: "credential", pattern: /\bAKIA[0-9A-Z]{16}\b/, always: true, label: "AWS access key" },
17
+ { category: "credential", pattern: /\bghp_[A-Za-z0-9]{20,}\b/, always: true, label: "GitHub personal token" },
18
+ { category: "credential", pattern: /\bghu_[A-Za-z0-9]{20,}\b/, always: true, label: "GitHub user token" },
19
+ { category: "credential", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, always: true, label: "Slack token" },
20
+ { category: "credential", pattern: /\bxapp-[A-Za-z0-9-]{10,}\b/, always: true, label: "Slack app token" },
21
+ { category: "credential", pattern: /\bntn_[A-Za-z0-9]{20,}\b/, always: true, label: "Notion token" },
22
+ { category: "credential", pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/, always: true, label: "JWT token" },
23
+ { category: "credential", pattern: /-----BEGIN\s+(?:RSA\s+|EC\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/, always: true, label: "Private key block" },
24
+ { category: "credential", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}\b/i, always: true, label: "Bearer auth token" },
25
+ { category: "credential", pattern: /\bapi[_-]?key\s*[=:]\s*[A-Za-z0-9_-]{16,}\b/i, always: true, label: "API key assignment" },
26
+ { category: "credential", pattern: /\b(client|app|consumer)_?secret\s*[=:]\s*[A-Za-z0-9_-]{16,}\b/i, always: true, label: "OAuth client secret" },
27
+ { category: "credential", pattern: /\bwebhook[_-]?url\s*[=:]\s*https?:\/\/[^\s]+/i, always: true, label: "Webhook URL" },
28
+ { category: "secret-assignment", pattern: /\b(password|passwd|pwd)\s*[=:]\s*\S{6,}\b/i, always: true, label: "password value" },
29
+ { category: "secret-assignment", pattern: /\b(secret|token|api[_-]?key|access[_-]?key)\s*[=:]\s*\S{10,}\b/i, always: true, label: "secret/token value" },
30
+ { category: "credential", pattern: /\b(ANTHROPIC|OPENAI|OPENROUTER|GEMINI|DEEPSEEK|MISTRAL|AZURE)_?(API)?_?KEY\b/i, always: true, label: "provider API key env var" },
31
+ { category: "credential", pattern: /\bGITHUB_TOKEN\b|\bAWS_SECRET_ACCESS_KEY\b|\bDATABASE_URL\b/i, always: true, label: "secret env var name" },
32
+ { category: "credential", pattern: /(?:mongodb|postgres|mysql|redis|amqp):\/\/[^\s]*:[^\s@]+@/, always: true, label: "connection string with credentials" },
33
+ ];
34
+ // ─── Payment data — ALWAYS blocked ───
35
+ const PAYMENT = [
36
+ {
37
+ category: "payment",
38
+ pattern: /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/,
39
+ always: true,
40
+ label: "payment card number",
41
+ },
42
+ { category: "payment", pattern: /\bcvv\s*[=:]\s*\d{3,4}\b/i, always: true, label: "card verification value" },
43
+ { category: "payment", pattern: /\b(?:expiry|exp)\s*[=:]\s*(?:0[1-9]|1[0-2])\/?\d{2}\b/i, always: true, label: "card expiry" },
44
+ ];
45
+ // ─── PII — level-dependent ───
46
+ const PII = [
47
+ {
48
+ category: "pii-email",
49
+ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/,
50
+ blockAt: "strict",
51
+ label: "personal email address",
52
+ },
53
+ {
54
+ category: "pii-phone",
55
+ pattern: /(?:\+\d{1,3}[\s-]?)?(?:\(?\d{2,4}\)?[\s-]?)?\d{3}[\s-]?\d{3}[\s-]?\d{3,4}\b/,
56
+ blockAt: "strict",
57
+ label: "phone number",
58
+ },
59
+ {
60
+ category: "pii-id",
61
+ pattern: /\b\d{8}[A-Z]\b|\b[A-Z]\d{7}[A-Z0-9]\b/, // DNI / NIE (Spain)
62
+ blockAt: "strict",
63
+ label: "national ID (DNI/NIE)",
64
+ },
65
+ {
66
+ category: "pii-bank",
67
+ pattern: /\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/, // IBAN
68
+ blockAt: "balanced",
69
+ label: "IBAN / bank account",
70
+ },
71
+ {
72
+ category: "pii-dob",
73
+ pattern: /\b(?:birth|birthday|born|nacimiento|fecha de nacimiento)\s*[=:]\s*(?:\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4})/i,
74
+ blockAt: "strict",
75
+ label: "date of birth",
76
+ },
77
+ ];
78
+ // ─── Prompt injection / exfiltration — ALWAYS blocked ───
79
+ const INJECTION = [
80
+ { category: "injection", pattern: /ignore\s+(previous|all|above|prior)\s+instructions/i, always: true, label: "prompt injection" },
81
+ { category: "injection", pattern: /you\s+are\s+now\s+/i, always: true, label: "role hijack" },
82
+ { category: "injection", pattern: /do\s+not\s+tell\s+the\s+user/i, always: true, label: "deceptive instruction" },
83
+ { category: "injection", pattern: /disregard\s+(your|all|any)\s+(instructions|rules|guidelines)/i, always: true, label: "instruction override" },
84
+ { category: "injection", pattern: /system\s+prompt\s+override/i, always: true, label: "system prompt override" },
85
+ { category: "exfiltration", pattern: /curl\s+[^\n]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, always: true, label: "credential exfiltration (curl)" },
86
+ { category: "exfiltration", pattern: /wget\s+[^\n]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, always: true, label: "credential exfiltration (wget)" },
87
+ { category: "exfiltration", pattern: /cat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass|\.npmrc|\.pypirc)/i, always: true, label: "secret file read" },
88
+ { category: "exfiltration", pattern: /\$HOME\/\.ssh|~\/\.ssh/i, always: true, label: "SSH key path" },
89
+ ];
90
+ const LEVEL_ORDER = { strict: 0, balanced: 1, permissive: 2 };
91
+ const INVISIBLE_CHARS = new Set([
92
+ "\u200b", "\u200c", "\u200d", "\u2060", "\ufeff",
93
+ "\u202a", "\u202b", "\u202c", "\u202d", "\u202e",
94
+ ]);
95
+ const REASON_EN = {
96
+ credential: "Looks like a credential or secret (secrets are never stored in memory)",
97
+ payment: "Looks like payment card data (never stored in memory)",
98
+ "secret-assignment": "Looks like a password/secret/token value (never stored in memory)",
99
+ "pii-email": "Looks like a personal email address (blocked by the strict privacy level)",
100
+ "pii-phone": "Looks like a phone number (blocked by the strict privacy level)",
101
+ "pii-id": "Looks like a national ID (DNI/NIE) (blocked by the strict privacy level)",
102
+ "pii-bank": "Looks like a bank account / IBAN (blocked by the privacy level)",
103
+ "pii-dob": "Looks like a date of birth (blocked by the strict privacy level)",
104
+ injection: "Looks like a prompt-injection attempt (never stored)",
105
+ exfiltration: "Looks like data exfiltration (never stored)",
106
+ };
107
+ const REASON_ES = {
108
+ credential: "Parece una credencial o secreto (los secretos nunca se almacenan en memoria)",
109
+ payment: "Parecen datos de tarjeta de pago (nunca se almacenan en memoria)",
110
+ "secret-assignment": "Parece una contraseña/secreto/token (nunca se almacena en memoria)",
111
+ "pii-email": "Parece un correo electrónico personal (bloqueado por el nivel de privacidad estricto)",
112
+ "pii-phone": "Parece un número de teléfono (bloqueado por el nivel de privacidad estricto)",
113
+ "pii-id": "Parece un documento de identidad (DNI/NIE) (bloqueado por el nivel de privacidad estricto)",
114
+ "pii-bank": "Parece una cuenta bancaria / IBAN (bloqueado por el nivel de privacidad)",
115
+ "pii-dob": "Parece una fecha de nacimiento (bloqueada por el nivel de privacidad estricto)",
116
+ injection: "Parece un intento de prompt injection (nunca se almacena)",
117
+ exfiltration: "Parece exfiltración de datos (nunca se almacena)",
118
+ };
119
+ function testPattern(pattern, content) {
120
+ try {
121
+ return pattern.pattern.test(content);
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ /**
128
+ * Classify content against the security policy.
129
+ *
130
+ * @param content Text to classify.
131
+ * @param level Configured privacy level.
132
+ * @param locale "es" or "en" for the reason language.
133
+ */
134
+ export function classifySensitive(content, level = "strict", locale = "es") {
135
+ const categories = new Set();
136
+ const reasons = [];
137
+ const labels = [];
138
+ // Invisible characters are always suspicious (injection vectors).
139
+ if ([...content].some((ch) => INVISIBLE_CHARS.has(ch))) {
140
+ return {
141
+ blocked: true,
142
+ categories: ["injection"],
143
+ reason: locale === "es"
144
+ ? "El contenido contiene caracteres invisibles/zero-width y fue rechazado por seguridad."
145
+ : "Content contains invisible/zero-width characters and was rejected for security.",
146
+ noteworthy: true,
147
+ };
148
+ }
149
+ const allPatterns = [...ALWAYS_BLOCKED, ...PAYMENT, ...PII, ...INJECTION];
150
+ for (const pattern of allPatterns) {
151
+ if (!testPattern(pattern, content))
152
+ continue;
153
+ const levelIdx = LEVEL_ORDER[level];
154
+ const blockAtIdx = pattern.blockAt ? LEVEL_ORDER[pattern.blockAt] : 0;
155
+ const blocked = pattern.always === true || levelIdx <= blockAtIdx;
156
+ categories.add(pattern.category);
157
+ if (blocked) {
158
+ labels.push(pattern.label);
159
+ if (!reasons.includes(pattern.category))
160
+ reasons.push(pattern.category);
161
+ }
162
+ }
163
+ if (reasons.length > 0) {
164
+ const localeMap = locale === "es" ? REASON_ES : REASON_EN;
165
+ const detail = reasons.map((r) => localeMap[r]).join("; ");
166
+ return {
167
+ blocked: true,
168
+ categories: [...categories],
169
+ reason: detail,
170
+ noteworthy: true,
171
+ };
172
+ }
173
+ return {
174
+ blocked: false,
175
+ categories: [...categories],
176
+ reason: "",
177
+ noteworthy: categories.size > 0,
178
+ };
179
+ }
180
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.js","sourceRoot":"","sources":["../../../src/core/memory-engine/security.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Security & privacy classification for the Ada Memory Engine.\n *\n * Enterprise-grade guards:\n * - Credentials/secrets/payment data are ALWAYS blocked at every level.\n * - PII (email, phone, national IDs, IBAN, DOB) blocking depends on the\n * configured privacy level (RGPD / AI Act friendly).\n * - Every rejection is written to the audit log with a reason.\n * - Prompt-injection/exfiltration patterns are always blocked.\n */\n\nexport type PrivacyLevel = \"strict\" | \"balanced\" | \"permissive\";\n\nexport type SensitiveCategory =\n\t| \"credential\"\n\t| \"payment\"\n\t| \"secret-assignment\"\n\t| \"pii-email\"\n\t| \"pii-phone\"\n\t| \"pii-id\"\n\t| \"pii-bank\"\n\t| \"pii-dob\"\n\t| \"injection\"\n\t| \"exfiltration\";\n\nexport interface SensitivityResult {\n\t/** True when the content must NOT be stored. */\n\tblocked: boolean;\n\t/** Categories detected. */\n\tcategories: SensitiveCategory[];\n\t/** Human-readable reason (in the configured language). */\n\treason: string;\n\t/** True when content contains permitted-but-noteworthy categories. */\n\tnoteworthy: boolean;\n}\n\ninterface Pattern {\n\tcategory: SensitiveCategory;\n\tpattern: RegExp;\n\t/** Always blocked regardless of privacy level. */\n\talways?: boolean;\n\t/** Blocked at this level and stricter. */\n\tblockAt?: PrivacyLevel;\n\t/** English reason label. */\n\tlabel: string;\n}\n\n// ─── Credentials & secrets — ALWAYS blocked ───\nconst ALWAYS_BLOCKED: Pattern[] = [\n\t{ category: \"credential\", pattern: /\\bsk-ant-api\\S{10,}\\b/, always: true, label: \"Anthropic API key\" },\n\t{ category: \"credential\", pattern: /\\bsk-or-v1-\\S{10,}\\b/, always: true, label: \"OpenRouter API key\" },\n\t{ category: \"credential\", pattern: /\\bsk-[A-Za-z0-9]{20,}\\b/, always: true, label: \"OpenAI-style API key\" },\n\t{ category: \"credential\", pattern: /\\bAKIA[0-9A-Z]{16}\\b/, always: true, label: \"AWS access key\" },\n\t{ category: \"credential\", pattern: /\\bghp_[A-Za-z0-9]{20,}\\b/, always: true, label: \"GitHub personal token\" },\n\t{ category: \"credential\", pattern: /\\bghu_[A-Za-z0-9]{20,}\\b/, always: true, label: \"GitHub user token\" },\n\t{ category: \"credential\", pattern: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/, always: true, label: \"Slack token\" },\n\t{ category: \"credential\", pattern: /\\bxapp-[A-Za-z0-9-]{10,}\\b/, always: true, label: \"Slack app token\" },\n\t{ category: \"credential\", pattern: /\\bntn_[A-Za-z0-9]{20,}\\b/, always: true, label: \"Notion token\" },\n\t{ category: \"credential\", pattern: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}/, always: true, label: \"JWT token\" },\n\t{ category: \"credential\", pattern: /-----BEGIN\\s+(?:RSA\\s+|EC\\s+|OPENSSH\\s+)?PRIVATE\\s+KEY-----/, always: true, label: \"Private key block\" },\n\t{ category: \"credential\", pattern: /\\bBearer\\s+[A-Za-z0-9._~+/=-]{20,}\\b/i, always: true, label: \"Bearer auth token\" },\n\t{ category: \"credential\", pattern: /\\bapi[_-]?key\\s*[=:]\\s*[A-Za-z0-9_-]{16,}\\b/i, always: true, label: \"API key assignment\" },\n\t{ category: \"credential\", pattern: /\\b(client|app|consumer)_?secret\\s*[=:]\\s*[A-Za-z0-9_-]{16,}\\b/i, always: true, label: \"OAuth client secret\" },\n\t{ category: \"credential\", pattern: /\\bwebhook[_-]?url\\s*[=:]\\s*https?:\\/\\/[^\\s]+/i, always: true, label: \"Webhook URL\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(password|passwd|pwd)\\s*[=:]\\s*\\S{6,}\\b/i, always: true, label: \"password value\" },\n\t{ category: \"secret-assignment\", pattern: /\\b(secret|token|api[_-]?key|access[_-]?key)\\s*[=:]\\s*\\S{10,}\\b/i, always: true, label: \"secret/token value\" },\n\t{ category: \"credential\", pattern: /\\b(ANTHROPIC|OPENAI|OPENROUTER|GEMINI|DEEPSEEK|MISTRAL|AZURE)_?(API)?_?KEY\\b/i, always: true, label: \"provider API key env var\" },\n\t{ category: \"credential\", pattern: /\\bGITHUB_TOKEN\\b|\\bAWS_SECRET_ACCESS_KEY\\b|\\bDATABASE_URL\\b/i, always: true, label: \"secret env var name\" },\n\t{ category: \"credential\", pattern: /(?:mongodb|postgres|mysql|redis|amqp):\\/\\/[^\\s]*:[^\\s@]+@/, always: true, label: \"connection string with credentials\" },\n];\n\n// ─── Payment data — ALWAYS blocked ───\nconst PAYMENT: Pattern[] = [\n\t{\n\t\tcategory: \"payment\",\n\t\tpattern: /\\b(?:4\\d{3}|5[1-5]\\d{2}|3[47]\\d{2}|6(?:011|5\\d{2}))[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b/,\n\t\talways: true,\n\t\tlabel: \"payment card number\",\n\t},\n\t{ category: \"payment\", pattern: /\\bcvv\\s*[=:]\\s*\\d{3,4}\\b/i, always: true, label: \"card verification value\" },\n\t{ category: \"payment\", pattern: /\\b(?:expiry|exp)\\s*[=:]\\s*(?:0[1-9]|1[0-2])\\/?\\d{2}\\b/i, always: true, label: \"card expiry\" },\n];\n\n// ─── PII — level-dependent ───\nconst PII: Pattern[] = [\n\t{\n\t\tcategory: \"pii-email\",\n\t\tpattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"personal email address\",\n\t},\n\t{\n\t\tcategory: \"pii-phone\",\n\t\tpattern: /(?:\\+\\d{1,3}[\\s-]?)?(?:\\(?\\d{2,4}\\)?[\\s-]?)?\\d{3}[\\s-]?\\d{3}[\\s-]?\\d{3,4}\\b/,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"phone number\",\n\t},\n\t{\n\t\tcategory: \"pii-id\",\n\t\tpattern: /\\b\\d{8}[A-Z]\\b|\\b[A-Z]\\d{7}[A-Z0-9]\\b/, // DNI / NIE (Spain)\n\t\tblockAt: \"strict\",\n\t\tlabel: \"national ID (DNI/NIE)\",\n\t},\n\t{\n\t\tcategory: \"pii-bank\",\n\t\tpattern: /\\b[A-Z]{2}\\d{2}[A-Z0-9]{11,30}\\b/, // IBAN\n\t\tblockAt: \"balanced\",\n\t\tlabel: \"IBAN / bank account\",\n\t},\n\t{\n\t\tcategory: \"pii-dob\",\n\t\tpattern: /\\b(?:birth|birthday|born|nacimiento|fecha de nacimiento)\\s*[=:]\\s*(?:\\d{1,2}[\\/\\-.]\\d{1,2}[\\/\\-.]\\d{2,4})/i,\n\t\tblockAt: \"strict\",\n\t\tlabel: \"date of birth\",\n\t},\n];\n\n// ─── Prompt injection / exfiltration — ALWAYS blocked ───\nconst INJECTION: Pattern[] = [\n\t{ category: \"injection\", pattern: /ignore\\s+(previous|all|above|prior)\\s+instructions/i, always: true, label: \"prompt injection\" },\n\t{ category: \"injection\", pattern: /you\\s+are\\s+now\\s+/i, always: true, label: \"role hijack\" },\n\t{ category: \"injection\", pattern: /do\\s+not\\s+tell\\s+the\\s+user/i, always: true, label: \"deceptive instruction\" },\n\t{ category: \"injection\", pattern: /disregard\\s+(your|all|any)\\s+(instructions|rules|guidelines)/i, always: true, label: \"instruction override\" },\n\t{ category: \"injection\", pattern: /system\\s+prompt\\s+override/i, always: true, label: \"system prompt override\" },\n\t{ category: \"exfiltration\", pattern: /curl\\s+[^\\n]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, always: true, label: \"credential exfiltration (curl)\" },\n\t{ category: \"exfiltration\", pattern: /wget\\s+[^\\n]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, always: true, label: \"credential exfiltration (wget)\" },\n\t{ category: \"exfiltration\", pattern: /cat\\s+[^\\n]*(\\.env|credentials|\\.netrc|\\.pgpass|\\.npmrc|\\.pypirc)/i, always: true, label: \"secret file read\" },\n\t{ category: \"exfiltration\", pattern: /\\$HOME\\/\\.ssh|~\\/\\.ssh/i, always: true, label: \"SSH key path\" },\n];\n\nconst LEVEL_ORDER: Record<PrivacyLevel, number> = { strict: 0, balanced: 1, permissive: 2 };\n\nconst INVISIBLE_CHARS = new Set([\n\t\"\\u200b\", \"\\u200c\", \"\\u200d\", \"\\u2060\", \"\\ufeff\",\n\t\"\\u202a\", \"\\u202b\", \"\\u202c\", \"\\u202d\", \"\\u202e\",\n]);\n\nconst REASON_EN = {\n\tcredential: \"Looks like a credential or secret (secrets are never stored in memory)\",\n\tpayment: \"Looks like payment card data (never stored in memory)\",\n\t\"secret-assignment\": \"Looks like a password/secret/token value (never stored in memory)\",\n\t\"pii-email\": \"Looks like a personal email address (blocked by the strict privacy level)\",\n\t\"pii-phone\": \"Looks like a phone number (blocked by the strict privacy level)\",\n\t\"pii-id\": \"Looks like a national ID (DNI/NIE) (blocked by the strict privacy level)\",\n\t\"pii-bank\": \"Looks like a bank account / IBAN (blocked by the privacy level)\",\n\t\"pii-dob\": \"Looks like a date of birth (blocked by the strict privacy level)\",\n\tinjection: \"Looks like a prompt-injection attempt (never stored)\",\n\texfiltration: \"Looks like data exfiltration (never stored)\",\n};\n\nconst REASON_ES: Record<SensitiveCategory, string> = {\n\tcredential: \"Parece una credencial o secreto (los secretos nunca se almacenan en memoria)\",\n\tpayment: \"Parecen datos de tarjeta de pago (nunca se almacenan en memoria)\",\n\t\"secret-assignment\": \"Parece una contraseña/secreto/token (nunca se almacena en memoria)\",\n\t\"pii-email\": \"Parece un correo electrónico personal (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-phone\": \"Parece un número de teléfono (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-id\": \"Parece un documento de identidad (DNI/NIE) (bloqueado por el nivel de privacidad estricto)\",\n\t\"pii-bank\": \"Parece una cuenta bancaria / IBAN (bloqueado por el nivel de privacidad)\",\n\t\"pii-dob\": \"Parece una fecha de nacimiento (bloqueada por el nivel de privacidad estricto)\",\n\tinjection: \"Parece un intento de prompt injection (nunca se almacena)\",\n\texfiltration: \"Parece exfiltración de datos (nunca se almacena)\",\n};\n\nfunction testPattern(pattern: Pattern, content: string): boolean {\n\ttry {\n\t\treturn pattern.pattern.test(content);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Classify content against the security policy.\n *\n * @param content Text to classify.\n * @param level Configured privacy level.\n * @param locale \"es\" or \"en\" for the reason language.\n */\nexport function classifySensitive(content: string, level: PrivacyLevel = \"strict\", locale: \"es\" | \"en\" = \"es\"): SensitivityResult {\n\tconst categories = new Set<SensitiveCategory>();\n\tconst reasons: string[] = [];\n\tconst labels: string[] = [];\n\n\t// Invisible characters are always suspicious (injection vectors).\n\tif ([...content].some((ch) => INVISIBLE_CHARS.has(ch))) {\n\t\treturn {\n\t\t\tblocked: true,\n\t\t\tcategories: [\"injection\"],\n\t\t\treason: locale === \"es\"\n\t\t\t\t? \"El contenido contiene caracteres invisibles/zero-width y fue rechazado por seguridad.\"\n\t\t\t\t: \"Content contains invisible/zero-width characters and was rejected for security.\",\n\t\t\tnoteworthy: true,\n\t\t};\n\t}\n\n\tconst allPatterns = [...ALWAYS_BLOCKED, ...PAYMENT, ...PII, ...INJECTION];\n\tfor (const pattern of allPatterns) {\n\t\tif (!testPattern(pattern, content)) continue;\n\t\tconst levelIdx = LEVEL_ORDER[level];\n\t\tconst blockAtIdx = pattern.blockAt ? LEVEL_ORDER[pattern.blockAt] : 0;\n\t\tconst blocked = pattern.always === true || levelIdx <= blockAtIdx;\n\t\tcategories.add(pattern.category);\n\t\tif (blocked) {\n\t\t\tlabels.push(pattern.label);\n\t\t\tif (!reasons.includes(pattern.category)) reasons.push(pattern.category);\n\t\t}\n\t}\n\n\tif (reasons.length > 0) {\n\t\tconst localeMap = locale === \"es\" ? REASON_ES : REASON_EN;\n\t\tconst detail = reasons.map((r) => localeMap[r as SensitiveCategory]).join(\"; \");\n\t\treturn {\n\t\t\tblocked: true,\n\t\t\tcategories: [...categories],\n\t\t\treason: detail,\n\t\t\tnoteworthy: true,\n\t\t};\n\t}\n\n\treturn {\n\t\tblocked: false,\n\t\tcategories: [...categories],\n\t\treason: \"\",\n\t\tnoteworthy: categories.size > 0,\n\t};\n}\n"]}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * SessionIndexer — indexes past conversations (layer 3c) from the session
3
+ * JSONL files written by session-manager.ts.
4
+ *
5
+ * - Incremental backfill: meta.json tracks per-file size+mtime, so only
6
+ * changed files are re-parsed on startup.
7
+ * - Live indexing: messages are upserted as they are emitted.
8
+ * - The index is a derived artifact; it can be rebuilt at any time.
9
+ */
10
+ export interface ParsedSessionFile {
11
+ sessionId: string;
12
+ cwd: string | null;
13
+ sessionName: string | null;
14
+ messages: Array<{
15
+ id: string;
16
+ role: "user" | "assistant";
17
+ content: string;
18
+ timestamp: string;
19
+ }>;
20
+ }
21
+ export interface SessionIndexerOptions {
22
+ /** Agent root (~/.ada/agent). Sessions live under <agentDir>/sessions. */
23
+ agentDir: string;
24
+ /** Directory for the engine index artifacts. */
25
+ indexDir: string;
26
+ }
27
+ /** Extract message text from the JSONL line's message.content array. */
28
+ export declare function extractText(content: unknown): string;
29
+ export declare function parseSessionFile(content: string): ParsedSessionFile | null;
30
+ export interface SessionHit {
31
+ sessionId: string;
32
+ sessionName: string | null;
33
+ project: string;
34
+ role: "user" | "assistant";
35
+ content: string;
36
+ timestamp: string;
37
+ score: number;
38
+ }
39
+ export declare class SessionIndexer {
40
+ private index;
41
+ private meta;
42
+ private metaPath;
43
+ private indexFilePath;
44
+ private agentDir;
45
+ constructor(options: SessionIndexerOptions);
46
+ load(): Promise<void>;
47
+ private listSessionFiles;
48
+ /**
49
+ * Incremental backfill: index files whose size/mtime changed since the
50
+ * last run. Returns the number of files indexed.
51
+ */
52
+ backfillIncremental(): Promise<number>;
53
+ /** Live upsert of a single message (called on message_end). */
54
+ upsertMessage(message: {
55
+ id: string;
56
+ sessionId: string;
57
+ role: "user" | "assistant";
58
+ content: string;
59
+ timestamp: string;
60
+ cwd?: string | null;
61
+ sessionName?: string | null;
62
+ }): void;
63
+ search(query: string, options?: {
64
+ project?: string | null;
65
+ role?: "user" | "assistant";
66
+ limit?: number;
67
+ }): SessionHit[];
68
+ /** Persist index + metadata. */
69
+ save(): Promise<void>;
70
+ }
71
+ //# sourceMappingURL=session-indexer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-indexer.d.ts","sourceRoot":"","sources":["../../../src/core/memory-engine/session-indexer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,MAAM,WAAW,iBAAiB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,KAAK,CAAC;QACf,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;QAC3B,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;CACH;AAED,MAAM,WAAW,qBAAqB;IACrC,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;CACjB;AAYD,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAWpD;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CA8C1E;AAMD,MAAM,WAAW,UAAU;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,cAAc;IAC1B,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,IAAI,CAA8C;IAC1D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,OAAO,EAAE,qBAAqB,EAKzC;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAS1B;YAEa,gBAAgB;IAuB9B;;;OAGG;IACG,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,CAkC3C;IAED,+DAA+D;IAC/D,aAAa,CAAC,OAAO,EAAE;QACtB,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;QAC3B,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,GAAG,IAAI,CAaP;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,UAAU,EAAE,CAqB1H;IAED,gCAAgC;IAC1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAI1B;CACD","sourcesContent":["/**\n * SessionIndexer — indexes past conversations (layer 3c) from the session\n * JSONL files written by session-manager.ts.\n *\n * - Incremental backfill: meta.json tracks per-file size+mtime, so only\n * changed files are re-parsed on startup.\n * - Live indexing: messages are upserted as they are emitted.\n * - The index is a derived artifact; it can be rebuilt at any time.\n */\n\nimport { readFile, writeFile, mkdir, stat, readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport { InvertedIndex } from \"./inverted-index.ts\";\n\nexport interface ParsedSessionFile {\n\tsessionId: string;\n\tcwd: string | null;\n\tsessionName: string | null;\n\tmessages: Array<{\n\t\tid: string;\n\t\trole: \"user\" | \"assistant\";\n\t\tcontent: string;\n\t\ttimestamp: string;\n\t}>;\n}\n\nexport interface SessionIndexerOptions {\n\t/** Agent root (~/.ada/agent). Sessions live under <agentDir>/sessions. */\n\tagentDir: string;\n\t/** Directory for the engine index artifacts. */\n\tindexDir: string;\n}\n\ninterface FileMeta {\n\tsize: number;\n\tmtimeMs: number;\n}\n\ninterface SessionMetaFile {\n\tversion: 1;\n\tfiles: Record<string, FileMeta>;\n}\n\n/** Extract message text from the JSONL line's message.content array. */\nexport function extractText(content: unknown): string {\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\tconst parts: string[] = [];\n\tfor (const block of content) {\n\t\tif (block && typeof block === \"object\" && (block as { type?: string }).type === \"text\") {\n\t\t\tconst text = (block as { text?: unknown }).text;\n\t\t\tif (typeof text === \"string\") parts.push(text);\n\t\t}\n\t}\n\treturn parts.join(\"\\n\");\n}\n\nexport function parseSessionFile(content: string): ParsedSessionFile | null {\n\tlet sessionId: string | null = null;\n\tlet cwd: string | null = null;\n\tlet sessionName: string | null = null;\n\tconst messages: ParsedSessionFile[\"messages\"] = [];\n\n\tfor (const line of content.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed) continue;\n\t\tlet entry: Record<string, unknown>;\n\t\ttry {\n\t\t\tentry = JSON.parse(trimmed) as Record<string, unknown>;\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!entry || typeof entry !== \"object\") continue;\n\n\t\tif (entry.type === \"session\") {\n\t\t\tif (typeof entry.id === \"string\") sessionId = entry.id;\n\t\t\tif (typeof entry.cwd === \"string\") cwd = entry.cwd;\n\t\t\tif (typeof entry.name === \"string\" && entry.name) sessionName = entry.name;\n\t\t\tcontinue;\n\t\t}\n\t\tif (entry.type === \"session_name\" && typeof entry.name === \"string\") {\n\t\t\tsessionName = entry.name;\n\t\t\tcontinue;\n\t\t}\n\t\tif (entry.type !== \"message\") continue;\n\n\t\tconst msg = entry.message as { role?: unknown; content?: unknown; timestamp?: unknown } | undefined;\n\t\tif (!msg || typeof msg !== \"object\") continue;\n\t\tconst role = msg.role;\n\t\tif (role !== \"user\" && role !== \"assistant\") continue;\n\t\tconst text = extractText(msg.content);\n\t\tif (!text.trim()) continue;\n\t\tconst messageId = typeof entry.id === \"string\" ? entry.id : `${sessionId ?? \"?\"}:${messages.length}`;\n\t\tmessages.push({\n\t\t\tid: messageId,\n\t\t\trole,\n\t\t\tcontent: text.slice(0, 4000),\n\t\t\ttimestamp: typeof entry.timestamp === \"string\" ? entry.timestamp : String(Date.now()),\n\t\t});\n\t}\n\n\tif (!sessionId) return null;\n\treturn { sessionId, cwd, sessionName, messages };\n}\n\nfunction sessionRoot(agentDir: string): string {\n\treturn join(agentDir, \"sessions\");\n}\n\nexport interface SessionHit {\n\tsessionId: string;\n\tsessionName: string | null;\n\tproject: string;\n\trole: \"user\" | \"assistant\";\n\tcontent: string;\n\ttimestamp: string;\n\tscore: number;\n}\n\nexport class SessionIndexer {\n\tprivate index: InvertedIndex;\n\tprivate meta: SessionMetaFile = { version: 1, files: {} };\n\tprivate metaPath: string;\n\tprivate indexFilePath: string;\n\tprivate agentDir: string;\n\n\tconstructor(options: SessionIndexerOptions) {\n\t\tthis.agentDir = options.agentDir;\n\t\tthis.metaPath = join(options.indexDir, \"sessions-meta.json\");\n\t\tthis.indexFilePath = join(options.indexDir, \"sessions.idx.json\");\n\t\tthis.index = new InvertedIndex(\"sessions\");\n\t}\n\n\tasync load(): Promise<void> {\n\t\tawait this.index.load(this.indexFilePath);\n\t\ttry {\n\t\t\tconst raw = await readFile(this.metaPath, \"utf-8\");\n\t\t\tconst parsed = JSON.parse(raw) as SessionMetaFile;\n\t\t\tif (parsed.version === 1) this.meta = parsed;\n\t\t} catch {\n\t\t\tthis.meta = { version: 1, files: {} };\n\t\t}\n\t}\n\n\tprivate async listSessionFiles(): Promise<string[]> {\n\t\tconst root = sessionRoot(this.agentDir);\n\t\tconst out: string[] = [];\n\t\tconst walk = async (dir: string): Promise<void> => {\n\t\t\tlet entries;\n\t\t\ttry {\n\t\t\t\tentries = await readdir(dir, { withFileTypes: true });\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfor (const entry of entries) {\n\t\t\t\tconst full = join(dir, entry.name);\n\t\t\t\tif (entry.isDirectory()) {\n\t\t\t\t\tawait walk(full);\n\t\t\t\t} else if (entry.name.endsWith(\".jsonl\")) {\n\t\t\t\t\tout.push(full);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tawait walk(root);\n\t\treturn out.sort();\n\t}\n\n\t/**\n\t * Incremental backfill: index files whose size/mtime changed since the\n\t * last run. Returns the number of files indexed.\n\t */\n\tasync backfillIncremental(): Promise<number> {\n\t\tconst files = await this.listSessionFiles();\n\t\tlet indexed = 0;\n\t\tfor (const file of files) {\n\t\t\tlet st;\n\t\t\ttry {\n\t\t\t\tst = await stat(file);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst rel = relative(this.agentDir, file);\n\t\t\tconst previous = this.meta.files[rel];\n\t\t\tif (previous && previous.size === st.size && previous.mtimeMs === st.mtimeMs) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlet content: string;\n\t\t\ttry {\n\t\t\t\tcontent = await readFile(file, \"utf-8\");\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst parsed = parseSessionFile(content);\n\t\t\tif (!parsed) continue;\n\t\t\tfor (const msg of parsed.messages) {\n\t\t\t\tthis.index.upsert(\n\t\t\t\t\t`${parsed.sessionId}:${msg.id}`,\n\t\t\t\t\tmsg.content,\n\t\t\t\t\t{ role: msg.role, timestamp: msg.timestamp, cwd: parsed.cwd, sessionId: parsed.sessionId, sessionName: parsed.sessionName },\n\t\t\t\t);\n\t\t\t}\n\t\t\tthis.meta.files[rel] = { size: st.size, mtimeMs: st.mtimeMs };\n\t\t\tindexed++;\n\t\t}\n\t\treturn indexed;\n\t}\n\n\t/** Live upsert of a single message (called on message_end). */\n\tupsertMessage(message: {\n\t\tid: string;\n\t\tsessionId: string;\n\t\trole: \"user\" | \"assistant\";\n\t\tcontent: string;\n\t\ttimestamp: string;\n\t\tcwd?: string | null;\n\t\tsessionName?: string | null;\n\t}): void {\n\t\tif (!message.content.trim()) return;\n\t\tthis.index.upsert(\n\t\t\t`${message.sessionId}:${message.id}`,\n\t\t\tmessage.content.slice(0, 4000),\n\t\t\t{\n\t\t\t\trole: message.role,\n\t\t\t\ttimestamp: message.timestamp,\n\t\t\t\tcwd: message.cwd ?? null,\n\t\t\t\tsessionId: message.sessionId,\n\t\t\t\tsessionName: message.sessionName ?? null,\n\t\t\t},\n\t\t);\n\t}\n\n\tsearch(query: string, options: { project?: string | null; role?: \"user\" | \"assistant\"; limit?: number } = {}): SessionHit[] {\n\t\tconst limit = options.limit ?? 10;\n\t\tconst results = this.index.search(query, limit * 8, 0.15);\n\t\tconst out: SessionHit[] = [];\n\t\tfor (const result of results) {\n\t\t\tconst meta = this.index.getDocMeta(result.id);\n\t\t\tif (!meta) continue;\n\t\t\tif (options.project && meta.cwd !== options.project) continue;\n\t\t\tif (options.role && meta.role !== options.role) continue;\n\t\t\tout.push({\n\t\t\t\tsessionId: (meta.sessionId as string) ?? \"\",\n\t\t\t\tsessionName: (meta.sessionName as string | null) ?? null,\n\t\t\t\tproject: (meta.cwd as string) ?? \"\",\n\t\t\t\trole: (meta.role as \"user\" | \"assistant\") ?? \"user\",\n\t\t\t\tcontent: result.snippet,\n\t\t\t\ttimestamp: (meta.timestamp as string) ?? \"\",\n\t\t\t\tscore: result.score,\n\t\t\t});\n\t\t\tif (out.length >= limit) break;\n\t\t}\n\t\treturn out;\n\t}\n\n\t/** Persist index + metadata. */\n\tasync save(): Promise<void> {\n\t\tawait mkdir(this.metaPath.replace(/[^/]+$/, \"\"), { recursive: true });\n\t\tawait this.index.save(this.indexFilePath);\n\t\tawait writeFile(this.metaPath, JSON.stringify(this.meta), \"utf-8\");\n\t}\n}\n"]}
@@ -0,0 +1,217 @@
1
+ /**
2
+ * SessionIndexer — indexes past conversations (layer 3c) from the session
3
+ * JSONL files written by session-manager.ts.
4
+ *
5
+ * - Incremental backfill: meta.json tracks per-file size+mtime, so only
6
+ * changed files are re-parsed on startup.
7
+ * - Live indexing: messages are upserted as they are emitted.
8
+ * - The index is a derived artifact; it can be rebuilt at any time.
9
+ */
10
+ import { readFile, writeFile, mkdir, stat, readdir } from "node:fs/promises";
11
+ import { join, relative } from "node:path";
12
+ import { InvertedIndex } from "./inverted-index.js";
13
+ /** Extract message text from the JSONL line's message.content array. */
14
+ export function extractText(content) {
15
+ if (typeof content === "string")
16
+ return content;
17
+ if (!Array.isArray(content))
18
+ return "";
19
+ const parts = [];
20
+ for (const block of content) {
21
+ if (block && typeof block === "object" && block.type === "text") {
22
+ const text = block.text;
23
+ if (typeof text === "string")
24
+ parts.push(text);
25
+ }
26
+ }
27
+ return parts.join("\n");
28
+ }
29
+ export function parseSessionFile(content) {
30
+ let sessionId = null;
31
+ let cwd = null;
32
+ let sessionName = null;
33
+ const messages = [];
34
+ for (const line of content.split("\n")) {
35
+ const trimmed = line.trim();
36
+ if (!trimmed)
37
+ continue;
38
+ let entry;
39
+ try {
40
+ entry = JSON.parse(trimmed);
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ if (!entry || typeof entry !== "object")
46
+ continue;
47
+ if (entry.type === "session") {
48
+ if (typeof entry.id === "string")
49
+ sessionId = entry.id;
50
+ if (typeof entry.cwd === "string")
51
+ cwd = entry.cwd;
52
+ if (typeof entry.name === "string" && entry.name)
53
+ sessionName = entry.name;
54
+ continue;
55
+ }
56
+ if (entry.type === "session_name" && typeof entry.name === "string") {
57
+ sessionName = entry.name;
58
+ continue;
59
+ }
60
+ if (entry.type !== "message")
61
+ continue;
62
+ const msg = entry.message;
63
+ if (!msg || typeof msg !== "object")
64
+ continue;
65
+ const role = msg.role;
66
+ if (role !== "user" && role !== "assistant")
67
+ continue;
68
+ const text = extractText(msg.content);
69
+ if (!text.trim())
70
+ continue;
71
+ const messageId = typeof entry.id === "string" ? entry.id : `${sessionId ?? "?"}:${messages.length}`;
72
+ messages.push({
73
+ id: messageId,
74
+ role,
75
+ content: text.slice(0, 4000),
76
+ timestamp: typeof entry.timestamp === "string" ? entry.timestamp : String(Date.now()),
77
+ });
78
+ }
79
+ if (!sessionId)
80
+ return null;
81
+ return { sessionId, cwd, sessionName, messages };
82
+ }
83
+ function sessionRoot(agentDir) {
84
+ return join(agentDir, "sessions");
85
+ }
86
+ export class SessionIndexer {
87
+ index;
88
+ meta = { version: 1, files: {} };
89
+ metaPath;
90
+ indexFilePath;
91
+ agentDir;
92
+ constructor(options) {
93
+ this.agentDir = options.agentDir;
94
+ this.metaPath = join(options.indexDir, "sessions-meta.json");
95
+ this.indexFilePath = join(options.indexDir, "sessions.idx.json");
96
+ this.index = new InvertedIndex("sessions");
97
+ }
98
+ async load() {
99
+ await this.index.load(this.indexFilePath);
100
+ try {
101
+ const raw = await readFile(this.metaPath, "utf-8");
102
+ const parsed = JSON.parse(raw);
103
+ if (parsed.version === 1)
104
+ this.meta = parsed;
105
+ }
106
+ catch {
107
+ this.meta = { version: 1, files: {} };
108
+ }
109
+ }
110
+ async listSessionFiles() {
111
+ const root = sessionRoot(this.agentDir);
112
+ const out = [];
113
+ const walk = async (dir) => {
114
+ let entries;
115
+ try {
116
+ entries = await readdir(dir, { withFileTypes: true });
117
+ }
118
+ catch {
119
+ return;
120
+ }
121
+ for (const entry of entries) {
122
+ const full = join(dir, entry.name);
123
+ if (entry.isDirectory()) {
124
+ await walk(full);
125
+ }
126
+ else if (entry.name.endsWith(".jsonl")) {
127
+ out.push(full);
128
+ }
129
+ }
130
+ };
131
+ await walk(root);
132
+ return out.sort();
133
+ }
134
+ /**
135
+ * Incremental backfill: index files whose size/mtime changed since the
136
+ * last run. Returns the number of files indexed.
137
+ */
138
+ async backfillIncremental() {
139
+ const files = await this.listSessionFiles();
140
+ let indexed = 0;
141
+ for (const file of files) {
142
+ let st;
143
+ try {
144
+ st = await stat(file);
145
+ }
146
+ catch {
147
+ continue;
148
+ }
149
+ const rel = relative(this.agentDir, file);
150
+ const previous = this.meta.files[rel];
151
+ if (previous && previous.size === st.size && previous.mtimeMs === st.mtimeMs) {
152
+ continue;
153
+ }
154
+ let content;
155
+ try {
156
+ content = await readFile(file, "utf-8");
157
+ }
158
+ catch {
159
+ continue;
160
+ }
161
+ const parsed = parseSessionFile(content);
162
+ if (!parsed)
163
+ continue;
164
+ for (const msg of parsed.messages) {
165
+ this.index.upsert(`${parsed.sessionId}:${msg.id}`, msg.content, { role: msg.role, timestamp: msg.timestamp, cwd: parsed.cwd, sessionId: parsed.sessionId, sessionName: parsed.sessionName });
166
+ }
167
+ this.meta.files[rel] = { size: st.size, mtimeMs: st.mtimeMs };
168
+ indexed++;
169
+ }
170
+ return indexed;
171
+ }
172
+ /** Live upsert of a single message (called on message_end). */
173
+ upsertMessage(message) {
174
+ if (!message.content.trim())
175
+ return;
176
+ this.index.upsert(`${message.sessionId}:${message.id}`, message.content.slice(0, 4000), {
177
+ role: message.role,
178
+ timestamp: message.timestamp,
179
+ cwd: message.cwd ?? null,
180
+ sessionId: message.sessionId,
181
+ sessionName: message.sessionName ?? null,
182
+ });
183
+ }
184
+ search(query, options = {}) {
185
+ const limit = options.limit ?? 10;
186
+ const results = this.index.search(query, limit * 8, 0.15);
187
+ const out = [];
188
+ for (const result of results) {
189
+ const meta = this.index.getDocMeta(result.id);
190
+ if (!meta)
191
+ continue;
192
+ if (options.project && meta.cwd !== options.project)
193
+ continue;
194
+ if (options.role && meta.role !== options.role)
195
+ continue;
196
+ out.push({
197
+ sessionId: meta.sessionId ?? "",
198
+ sessionName: meta.sessionName ?? null,
199
+ project: meta.cwd ?? "",
200
+ role: meta.role ?? "user",
201
+ content: result.snippet,
202
+ timestamp: meta.timestamp ?? "",
203
+ score: result.score,
204
+ });
205
+ if (out.length >= limit)
206
+ break;
207
+ }
208
+ return out;
209
+ }
210
+ /** Persist index + metadata. */
211
+ async save() {
212
+ await mkdir(this.metaPath.replace(/[^/]+$/, ""), { recursive: true });
213
+ await this.index.save(this.indexFilePath);
214
+ await writeFile(this.metaPath, JSON.stringify(this.meta), "utf-8");
215
+ }
216
+ }
217
+ //# sourceMappingURL=session-indexer.js.map