@codyswann/lisa 2.145.1 → 2.146.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/configs/eslint/base.d.ts.map +1 -1
  2. package/dist/configs/eslint/base.js +1 -0
  3. package/dist/configs/eslint/base.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa-agy/plugin.json +1 -1
  8. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  9. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  10. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  11. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-expo-agy/plugin.json +1 -1
  18. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  23. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  28. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  33. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-rails-agy/plugin.json +1 -1
  38. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  43. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-wiki/scripts/_wiki-lib.mjs +19 -0
  48. package/plugins/lisa-wiki/scripts/ingest-git.mjs +10 -9
  49. package/plugins/lisa-wiki/scripts/ingest-memory.mjs +14 -9
  50. package/plugins/lisa-wiki/scripts/ingest-roles.mjs +13 -3
  51. package/plugins/lisa-wiki/scripts/ingest_slack_channel.py +39 -7
  52. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  53. package/plugins/lisa-wiki-agy/scripts/_wiki-lib.mjs +19 -0
  54. package/plugins/lisa-wiki-agy/scripts/ingest-git.mjs +10 -9
  55. package/plugins/lisa-wiki-agy/scripts/ingest-memory.mjs +14 -9
  56. package/plugins/lisa-wiki-agy/scripts/ingest-roles.mjs +13 -3
  57. package/plugins/lisa-wiki-agy/scripts/ingest_slack_channel.py +39 -7
  58. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-copilot/scripts/_wiki-lib.mjs +19 -0
  60. package/plugins/lisa-wiki-copilot/scripts/ingest-git.mjs +10 -9
  61. package/plugins/lisa-wiki-copilot/scripts/ingest-memory.mjs +14 -9
  62. package/plugins/lisa-wiki-copilot/scripts/ingest-roles.mjs +13 -3
  63. package/plugins/lisa-wiki-copilot/scripts/ingest_slack_channel.py +39 -7
  64. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki-cursor/scripts/_wiki-lib.mjs +19 -0
  66. package/plugins/lisa-wiki-cursor/scripts/ingest-git.mjs +10 -9
  67. package/plugins/lisa-wiki-cursor/scripts/ingest-memory.mjs +14 -9
  68. package/plugins/lisa-wiki-cursor/scripts/ingest-roles.mjs +13 -3
  69. package/plugins/lisa-wiki-cursor/scripts/ingest_slack_channel.py +39 -7
  70. package/plugins/src/wiki/scripts/_wiki-lib.mjs +19 -0
  71. package/plugins/src/wiki/scripts/ingest-git.mjs +10 -9
  72. package/plugins/src/wiki/scripts/ingest-memory.mjs +14 -9
  73. package/plugins/src/wiki/scripts/ingest-roles.mjs +13 -3
  74. package/plugins/src/wiki/scripts/ingest_slack_channel.py +39 -7
  75. package/typescript/copy-overwrite/eslint.ignore.config.json +1 -0
@@ -22,10 +22,37 @@ from typing import Any
22
22
 
23
23
 
24
24
  TOKEN_PATTERNS = [
25
- re.compile(r"xox[pbar]-[A-Za-z0-9-]+"),
26
- re.compile(r"(?i)bearer\s+[A-Za-z0-9._~+/=-]{20,}"),
27
- re.compile(r"AKIA[0-9A-Z]{16}"),
28
- re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.S),
25
+ (re.compile(r"xox[pbar]-[A-Za-z0-9-]+"), "[REDACTED:OAUTH_TOKEN]"),
26
+ (
27
+ re.compile(r"(?i)bearer\s+[A-Za-z0-9._~+/=-]{20,}"),
28
+ "[REDACTED:OAUTH_TOKEN]",
29
+ ),
30
+ (re.compile(r"AKIA[0-9A-Z]{16}"), "[REDACTED:API_KEY]"),
31
+ (
32
+ re.compile(
33
+ r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----",
34
+ re.S,
35
+ ),
36
+ "[REDACTED:PRIVATE_KEY]",
37
+ ),
38
+ (
39
+ re.compile(r"\b(?!000|666|9\d\d)\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b"),
40
+ "[REDACTED:SSN]",
41
+ ),
42
+ (
43
+ re.compile(
44
+ r"\b(?:password|passwd|pwd)\s*[:=]\s*(['\"]?)([^\s'\",;]{8,})\1",
45
+ re.I,
46
+ ),
47
+ "[REDACTED:PASSWORD]",
48
+ ),
49
+ (
50
+ re.compile(
51
+ r"\b(?:api[_-]?key|access[_-]?key|secret[_-]?key|client[_-]?secret)\s*[:=]\s*(['\"]?)([A-Za-z0-9._-]{20,})\1",
52
+ re.I,
53
+ ),
54
+ "[REDACTED:API_KEY]",
55
+ ),
29
56
  ]
30
57
 
31
58
 
@@ -49,8 +76,13 @@ def ts_from_input(value: str | None) -> str | None:
49
76
 
50
77
  def redact(text: str) -> str:
51
78
  out = text
52
- for pattern in TOKEN_PATTERNS:
53
- out = pattern.sub("[REDACTED]", out)
79
+ for pattern, replacement in TOKEN_PATTERNS:
80
+ out = pattern.sub(
81
+ lambda match: match.group(0).replace(match.group(2), replacement)
82
+ if len(match.groups()) >= 2
83
+ else replacement,
84
+ out,
85
+ )
54
86
  return out
55
87
 
56
88
 
@@ -289,7 +321,7 @@ def main() -> int:
289
321
  for reply in replies:
290
322
  lines.extend(render_message(reply))
291
323
 
292
- source_path.write_text("\n".join(lines), encoding="utf-8")
324
+ source_path.write_text(redact("\n".join(lines)), encoding="utf-8")
293
325
 
294
326
  latest_ts = previous_state.get("latest_message_ts")
295
327
  if messages:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.145.1",
3
+ "version": "2.146.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
+ import { sanitizeWikiSourceText } from "./wiki-safety.mjs";
8
9
 
9
10
  /** Read and parse a JSON file, or return undefined if missing/invalid. */
10
11
  export function readJsonSafe(file) {
@@ -59,6 +60,24 @@ export function walkFiles(dir, { ext } = {}) {
59
60
  return out.sort();
60
61
  }
61
62
 
63
+ /**
64
+ * Sanitize a wiki source note immediately before it is persisted.
65
+ *
66
+ * Connectors should keep fetched raw material in memory or temporary locations
67
+ * outside the repo, render the source note, then call this helper at the final
68
+ * `wiki/sources/**` write boundary. The return value contains safe finding
69
+ * metadata only; callers can include it in handoff metadata when useful.
70
+ */
71
+ export function writeSanitizedSourceNote(file, rawText, sourceMetadata = {}) {
72
+ const result = sanitizeWikiSourceText(rawText, {
73
+ ...sourceMetadata,
74
+ path: sourceMetadata.path ?? file,
75
+ });
76
+ fs.mkdirSync(path.dirname(file), { recursive: true });
77
+ fs.writeFileSync(file, result.text);
78
+ return result;
79
+ }
80
+
62
81
  /**
63
82
  * Minimal frontmatter detector. Returns whether a leading `--- ... ---` block
64
83
  * exists and the top-level keys it declares (enough to check required fields;
@@ -15,7 +15,7 @@
15
15
  import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import { execFileSync } from "node:child_process";
18
- import { readJsonSafe, SECRET_PATTERNS } from "./_wiki-lib.mjs";
18
+ import { readJsonSafe, writeSanitizedSourceNote } from "./_wiki-lib.mjs";
19
19
 
20
20
  const argv = process.argv.slice(2);
21
21
  const opt = (n, d) => {
@@ -53,12 +53,6 @@ const commitExists = c => {
53
53
  return false;
54
54
  }
55
55
  };
56
- const redact = t =>
57
- SECRET_PATTERNS.reduce(
58
- (acc, { re }) => acc.replace(new RegExp(re, "g"), "[REDACTED]"),
59
- t
60
- );
61
-
62
56
  if (
63
57
  !fs.existsSync(path.join(repo, ".git")) &&
64
58
  !tryGit(["rev-parse", "--is-inside-work-tree"])
@@ -165,8 +159,11 @@ ${
165
159
  }
166
160
  `;
167
161
 
168
- fs.mkdirSync(sourceDir, { recursive: true });
169
- fs.writeFileSync(notePath, redact(note));
162
+ const safety = writeSanitizedSourceNote(notePath, note, {
163
+ sourceId: path.relative(process.cwd(), notePath),
164
+ sourceSystem: "git",
165
+ project: slug,
166
+ });
170
167
 
171
168
  const meta = {
172
169
  connector: "git",
@@ -174,6 +171,10 @@ const meta = {
174
171
  ranAt: new Date().toISOString(),
175
172
  proposedCursor: { lastCommit: head, lastPr },
176
173
  sourceNotes: [path.relative(process.cwd(), notePath)],
174
+ safety: {
175
+ reviewRequired: safety.reviewRequired,
176
+ findings: safety.findings,
177
+ },
177
178
  };
178
179
  if (emitMeta) {
179
180
  fs.mkdirSync(path.dirname(emitMeta), { recursive: true });
@@ -15,7 +15,11 @@
15
15
  import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import os from "node:os";
18
- import { loadConfig, walkFiles, SECRET_PATTERNS } from "./_wiki-lib.mjs";
18
+ import {
19
+ loadConfig,
20
+ walkFiles,
21
+ writeSanitizedSourceNote,
22
+ } from "./_wiki-lib.mjs";
19
23
 
20
24
  const argv = process.argv.slice(2);
21
25
  const opt = (n, d) => {
@@ -79,15 +83,10 @@ if (!(under(claudeMem) || under(projectCodexMem) || allowedRoots.some(under))) {
79
83
  );
80
84
  }
81
85
 
82
- const redact = t =>
83
- SECRET_PATTERNS.reduce(
84
- (acc, { re }) => acc.replace(new RegExp(re, "g"), "[REDACTED]"),
85
- t
86
- );
87
86
  const mdFiles = walkFiles(resolvedMem, { ext: ".md" });
88
87
  const date = new Date().toISOString().slice(0, 10);
89
88
  const entries = mdFiles.map(f => {
90
- const body = redact(fs.readFileSync(f, "utf8")).trim();
89
+ const body = fs.readFileSync(f, "utf8").trim();
91
90
  return `### ${path.basename(f)}\n\n${body}`;
92
91
  });
93
92
 
@@ -110,8 +109,10 @@ sensitivity: internal
110
109
  ${entries.join("\n\n") || "_(no memory files)_"}
111
110
  `;
112
111
 
113
- fs.mkdirSync(sourceDir, { recursive: true });
114
- fs.writeFileSync(notePath, note);
112
+ const safety = writeSanitizedSourceNote(notePath, note, {
113
+ sourceId: path.relative(process.cwd(), notePath),
114
+ sourceSystem: "memory",
115
+ });
115
116
 
116
117
  const meta = {
117
118
  connector: "memory",
@@ -119,6 +120,10 @@ const meta = {
119
120
  ranAt: new Date().toISOString(),
120
121
  proposedCursor: { files: mdFiles.length, lastIngest: date },
121
122
  sourceNotes: [path.relative(process.cwd(), notePath)],
123
+ safety: {
124
+ reviewRequired: safety.reviewRequired,
125
+ findings: safety.findings,
126
+ },
122
127
  };
123
128
  if (emitMeta) {
124
129
  fs.mkdirSync(path.dirname(emitMeta), { recursive: true });
@@ -10,7 +10,11 @@
10
10
  */
11
11
  import fs from "node:fs";
12
12
  import path from "node:path";
13
- import { loadConfig, walkFiles } from "./_wiki-lib.mjs";
13
+ import {
14
+ loadConfig,
15
+ walkFiles,
16
+ writeSanitizedSourceNote,
17
+ } from "./_wiki-lib.mjs";
14
18
 
15
19
  const argv = process.argv.slice(2);
16
20
  const opt = (n, d) => {
@@ -61,8 +65,10 @@ ${rosterRows}
61
65
  ${staffPages.length ? staffPages.map(p => `- \`${path.relative(wikiRoot, p)}\``).join("\n") : "_(none)_"}
62
66
  `;
63
67
 
64
- fs.mkdirSync(sourceDir, { recursive: true });
65
- fs.writeFileSync(notePath, note);
68
+ const safety = writeSanitizedSourceNote(notePath, note, {
69
+ sourceId: path.relative(process.cwd(), notePath),
70
+ sourceSystem: "roles",
71
+ });
66
72
 
67
73
  const meta = {
68
74
  connector: "roles",
@@ -74,6 +80,10 @@ const meta = {
74
80
  lastIngest: date,
75
81
  },
76
82
  sourceNotes: [path.relative(process.cwd(), notePath)],
83
+ safety: {
84
+ reviewRequired: safety.reviewRequired,
85
+ findings: safety.findings,
86
+ },
77
87
  };
78
88
  if (emitMeta) {
79
89
  fs.mkdirSync(path.dirname(emitMeta), { recursive: true });
@@ -22,10 +22,37 @@ from typing import Any
22
22
 
23
23
 
24
24
  TOKEN_PATTERNS = [
25
- re.compile(r"xox[pbar]-[A-Za-z0-9-]+"),
26
- re.compile(r"(?i)bearer\s+[A-Za-z0-9._~+/=-]{20,}"),
27
- re.compile(r"AKIA[0-9A-Z]{16}"),
28
- re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.S),
25
+ (re.compile(r"xox[pbar]-[A-Za-z0-9-]+"), "[REDACTED:OAUTH_TOKEN]"),
26
+ (
27
+ re.compile(r"(?i)bearer\s+[A-Za-z0-9._~+/=-]{20,}"),
28
+ "[REDACTED:OAUTH_TOKEN]",
29
+ ),
30
+ (re.compile(r"AKIA[0-9A-Z]{16}"), "[REDACTED:API_KEY]"),
31
+ (
32
+ re.compile(
33
+ r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----",
34
+ re.S,
35
+ ),
36
+ "[REDACTED:PRIVATE_KEY]",
37
+ ),
38
+ (
39
+ re.compile(r"\b(?!000|666|9\d\d)\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b"),
40
+ "[REDACTED:SSN]",
41
+ ),
42
+ (
43
+ re.compile(
44
+ r"\b(?:password|passwd|pwd)\s*[:=]\s*(['\"]?)([^\s'\",;]{8,})\1",
45
+ re.I,
46
+ ),
47
+ "[REDACTED:PASSWORD]",
48
+ ),
49
+ (
50
+ re.compile(
51
+ r"\b(?:api[_-]?key|access[_-]?key|secret[_-]?key|client[_-]?secret)\s*[:=]\s*(['\"]?)([A-Za-z0-9._-]{20,})\1",
52
+ re.I,
53
+ ),
54
+ "[REDACTED:API_KEY]",
55
+ ),
29
56
  ]
30
57
 
31
58
 
@@ -49,8 +76,13 @@ def ts_from_input(value: str | None) -> str | None:
49
76
 
50
77
  def redact(text: str) -> str:
51
78
  out = text
52
- for pattern in TOKEN_PATTERNS:
53
- out = pattern.sub("[REDACTED]", out)
79
+ for pattern, replacement in TOKEN_PATTERNS:
80
+ out = pattern.sub(
81
+ lambda match: match.group(0).replace(match.group(2), replacement)
82
+ if len(match.groups()) >= 2
83
+ else replacement,
84
+ out,
85
+ )
54
86
  return out
55
87
 
56
88
 
@@ -289,7 +321,7 @@ def main() -> int:
289
321
  for reply in replies:
290
322
  lines.extend(render_message(reply))
291
323
 
292
- source_path.write_text("\n".join(lines), encoding="utf-8")
324
+ source_path.write_text(redact("\n".join(lines)), encoding="utf-8")
293
325
 
294
326
  latest_ts = previous_state.get("latest_message_ts")
295
327
  if messages:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.145.1",
3
+ "version": "2.146.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
+ import { sanitizeWikiSourceText } from "./wiki-safety.mjs";
8
9
 
9
10
  /** Read and parse a JSON file, or return undefined if missing/invalid. */
10
11
  export function readJsonSafe(file) {
@@ -59,6 +60,24 @@ export function walkFiles(dir, { ext } = {}) {
59
60
  return out.sort();
60
61
  }
61
62
 
63
+ /**
64
+ * Sanitize a wiki source note immediately before it is persisted.
65
+ *
66
+ * Connectors should keep fetched raw material in memory or temporary locations
67
+ * outside the repo, render the source note, then call this helper at the final
68
+ * `wiki/sources/**` write boundary. The return value contains safe finding
69
+ * metadata only; callers can include it in handoff metadata when useful.
70
+ */
71
+ export function writeSanitizedSourceNote(file, rawText, sourceMetadata = {}) {
72
+ const result = sanitizeWikiSourceText(rawText, {
73
+ ...sourceMetadata,
74
+ path: sourceMetadata.path ?? file,
75
+ });
76
+ fs.mkdirSync(path.dirname(file), { recursive: true });
77
+ fs.writeFileSync(file, result.text);
78
+ return result;
79
+ }
80
+
62
81
  /**
63
82
  * Minimal frontmatter detector. Returns whether a leading `--- ... ---` block
64
83
  * exists and the top-level keys it declares (enough to check required fields;
@@ -15,7 +15,7 @@
15
15
  import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import { execFileSync } from "node:child_process";
18
- import { readJsonSafe, SECRET_PATTERNS } from "./_wiki-lib.mjs";
18
+ import { readJsonSafe, writeSanitizedSourceNote } from "./_wiki-lib.mjs";
19
19
 
20
20
  const argv = process.argv.slice(2);
21
21
  const opt = (n, d) => {
@@ -53,12 +53,6 @@ const commitExists = c => {
53
53
  return false;
54
54
  }
55
55
  };
56
- const redact = t =>
57
- SECRET_PATTERNS.reduce(
58
- (acc, { re }) => acc.replace(new RegExp(re, "g"), "[REDACTED]"),
59
- t
60
- );
61
-
62
56
  if (
63
57
  !fs.existsSync(path.join(repo, ".git")) &&
64
58
  !tryGit(["rev-parse", "--is-inside-work-tree"])
@@ -165,8 +159,11 @@ ${
165
159
  }
166
160
  `;
167
161
 
168
- fs.mkdirSync(sourceDir, { recursive: true });
169
- fs.writeFileSync(notePath, redact(note));
162
+ const safety = writeSanitizedSourceNote(notePath, note, {
163
+ sourceId: path.relative(process.cwd(), notePath),
164
+ sourceSystem: "git",
165
+ project: slug,
166
+ });
170
167
 
171
168
  const meta = {
172
169
  connector: "git",
@@ -174,6 +171,10 @@ const meta = {
174
171
  ranAt: new Date().toISOString(),
175
172
  proposedCursor: { lastCommit: head, lastPr },
176
173
  sourceNotes: [path.relative(process.cwd(), notePath)],
174
+ safety: {
175
+ reviewRequired: safety.reviewRequired,
176
+ findings: safety.findings,
177
+ },
177
178
  };
178
179
  if (emitMeta) {
179
180
  fs.mkdirSync(path.dirname(emitMeta), { recursive: true });
@@ -15,7 +15,11 @@
15
15
  import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import os from "node:os";
18
- import { loadConfig, walkFiles, SECRET_PATTERNS } from "./_wiki-lib.mjs";
18
+ import {
19
+ loadConfig,
20
+ walkFiles,
21
+ writeSanitizedSourceNote,
22
+ } from "./_wiki-lib.mjs";
19
23
 
20
24
  const argv = process.argv.slice(2);
21
25
  const opt = (n, d) => {
@@ -79,15 +83,10 @@ if (!(under(claudeMem) || under(projectCodexMem) || allowedRoots.some(under))) {
79
83
  );
80
84
  }
81
85
 
82
- const redact = t =>
83
- SECRET_PATTERNS.reduce(
84
- (acc, { re }) => acc.replace(new RegExp(re, "g"), "[REDACTED]"),
85
- t
86
- );
87
86
  const mdFiles = walkFiles(resolvedMem, { ext: ".md" });
88
87
  const date = new Date().toISOString().slice(0, 10);
89
88
  const entries = mdFiles.map(f => {
90
- const body = redact(fs.readFileSync(f, "utf8")).trim();
89
+ const body = fs.readFileSync(f, "utf8").trim();
91
90
  return `### ${path.basename(f)}\n\n${body}`;
92
91
  });
93
92
 
@@ -110,8 +109,10 @@ sensitivity: internal
110
109
  ${entries.join("\n\n") || "_(no memory files)_"}
111
110
  `;
112
111
 
113
- fs.mkdirSync(sourceDir, { recursive: true });
114
- fs.writeFileSync(notePath, note);
112
+ const safety = writeSanitizedSourceNote(notePath, note, {
113
+ sourceId: path.relative(process.cwd(), notePath),
114
+ sourceSystem: "memory",
115
+ });
115
116
 
116
117
  const meta = {
117
118
  connector: "memory",
@@ -119,6 +120,10 @@ const meta = {
119
120
  ranAt: new Date().toISOString(),
120
121
  proposedCursor: { files: mdFiles.length, lastIngest: date },
121
122
  sourceNotes: [path.relative(process.cwd(), notePath)],
123
+ safety: {
124
+ reviewRequired: safety.reviewRequired,
125
+ findings: safety.findings,
126
+ },
122
127
  };
123
128
  if (emitMeta) {
124
129
  fs.mkdirSync(path.dirname(emitMeta), { recursive: true });
@@ -10,7 +10,11 @@
10
10
  */
11
11
  import fs from "node:fs";
12
12
  import path from "node:path";
13
- import { loadConfig, walkFiles } from "./_wiki-lib.mjs";
13
+ import {
14
+ loadConfig,
15
+ walkFiles,
16
+ writeSanitizedSourceNote,
17
+ } from "./_wiki-lib.mjs";
14
18
 
15
19
  const argv = process.argv.slice(2);
16
20
  const opt = (n, d) => {
@@ -61,8 +65,10 @@ ${rosterRows}
61
65
  ${staffPages.length ? staffPages.map(p => `- \`${path.relative(wikiRoot, p)}\``).join("\n") : "_(none)_"}
62
66
  `;
63
67
 
64
- fs.mkdirSync(sourceDir, { recursive: true });
65
- fs.writeFileSync(notePath, note);
68
+ const safety = writeSanitizedSourceNote(notePath, note, {
69
+ sourceId: path.relative(process.cwd(), notePath),
70
+ sourceSystem: "roles",
71
+ });
66
72
 
67
73
  const meta = {
68
74
  connector: "roles",
@@ -74,6 +80,10 @@ const meta = {
74
80
  lastIngest: date,
75
81
  },
76
82
  sourceNotes: [path.relative(process.cwd(), notePath)],
83
+ safety: {
84
+ reviewRequired: safety.reviewRequired,
85
+ findings: safety.findings,
86
+ },
77
87
  };
78
88
  if (emitMeta) {
79
89
  fs.mkdirSync(path.dirname(emitMeta), { recursive: true });
@@ -22,10 +22,37 @@ from typing import Any
22
22
 
23
23
 
24
24
  TOKEN_PATTERNS = [
25
- re.compile(r"xox[pbar]-[A-Za-z0-9-]+"),
26
- re.compile(r"(?i)bearer\s+[A-Za-z0-9._~+/=-]{20,}"),
27
- re.compile(r"AKIA[0-9A-Z]{16}"),
28
- re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.S),
25
+ (re.compile(r"xox[pbar]-[A-Za-z0-9-]+"), "[REDACTED:OAUTH_TOKEN]"),
26
+ (
27
+ re.compile(r"(?i)bearer\s+[A-Za-z0-9._~+/=-]{20,}"),
28
+ "[REDACTED:OAUTH_TOKEN]",
29
+ ),
30
+ (re.compile(r"AKIA[0-9A-Z]{16}"), "[REDACTED:API_KEY]"),
31
+ (
32
+ re.compile(
33
+ r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----",
34
+ re.S,
35
+ ),
36
+ "[REDACTED:PRIVATE_KEY]",
37
+ ),
38
+ (
39
+ re.compile(r"\b(?!000|666|9\d\d)\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b"),
40
+ "[REDACTED:SSN]",
41
+ ),
42
+ (
43
+ re.compile(
44
+ r"\b(?:password|passwd|pwd)\s*[:=]\s*(['\"]?)([^\s'\",;]{8,})\1",
45
+ re.I,
46
+ ),
47
+ "[REDACTED:PASSWORD]",
48
+ ),
49
+ (
50
+ re.compile(
51
+ r"\b(?:api[_-]?key|access[_-]?key|secret[_-]?key|client[_-]?secret)\s*[:=]\s*(['\"]?)([A-Za-z0-9._-]{20,})\1",
52
+ re.I,
53
+ ),
54
+ "[REDACTED:API_KEY]",
55
+ ),
29
56
  ]
30
57
 
31
58
 
@@ -49,8 +76,13 @@ def ts_from_input(value: str | None) -> str | None:
49
76
 
50
77
  def redact(text: str) -> str:
51
78
  out = text
52
- for pattern in TOKEN_PATTERNS:
53
- out = pattern.sub("[REDACTED]", out)
79
+ for pattern, replacement in TOKEN_PATTERNS:
80
+ out = pattern.sub(
81
+ lambda match: match.group(0).replace(match.group(2), replacement)
82
+ if len(match.groups()) >= 2
83
+ else replacement,
84
+ out,
85
+ )
54
86
  return out
55
87
 
56
88
 
@@ -289,7 +321,7 @@ def main() -> int:
289
321
  for reply in replies:
290
322
  lines.extend(render_message(reply))
291
323
 
292
- source_path.write_text("\n".join(lines), encoding="utf-8")
324
+ source_path.write_text(redact("\n".join(lines)), encoding="utf-8")
293
325
 
294
326
  latest_ts = previous_state.get("latest_message_ts")
295
327
  if messages:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.145.1",
3
+ "version": "2.146.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
+ import { sanitizeWikiSourceText } from "./wiki-safety.mjs";
8
9
 
9
10
  /** Read and parse a JSON file, or return undefined if missing/invalid. */
10
11
  export function readJsonSafe(file) {
@@ -59,6 +60,24 @@ export function walkFiles(dir, { ext } = {}) {
59
60
  return out.sort();
60
61
  }
61
62
 
63
+ /**
64
+ * Sanitize a wiki source note immediately before it is persisted.
65
+ *
66
+ * Connectors should keep fetched raw material in memory or temporary locations
67
+ * outside the repo, render the source note, then call this helper at the final
68
+ * `wiki/sources/**` write boundary. The return value contains safe finding
69
+ * metadata only; callers can include it in handoff metadata when useful.
70
+ */
71
+ export function writeSanitizedSourceNote(file, rawText, sourceMetadata = {}) {
72
+ const result = sanitizeWikiSourceText(rawText, {
73
+ ...sourceMetadata,
74
+ path: sourceMetadata.path ?? file,
75
+ });
76
+ fs.mkdirSync(path.dirname(file), { recursive: true });
77
+ fs.writeFileSync(file, result.text);
78
+ return result;
79
+ }
80
+
62
81
  /**
63
82
  * Minimal frontmatter detector. Returns whether a leading `--- ... ---` block
64
83
  * exists and the top-level keys it declares (enough to check required fields;
@@ -15,7 +15,7 @@
15
15
  import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import { execFileSync } from "node:child_process";
18
- import { readJsonSafe, SECRET_PATTERNS } from "./_wiki-lib.mjs";
18
+ import { readJsonSafe, writeSanitizedSourceNote } from "./_wiki-lib.mjs";
19
19
 
20
20
  const argv = process.argv.slice(2);
21
21
  const opt = (n, d) => {
@@ -53,12 +53,6 @@ const commitExists = c => {
53
53
  return false;
54
54
  }
55
55
  };
56
- const redact = t =>
57
- SECRET_PATTERNS.reduce(
58
- (acc, { re }) => acc.replace(new RegExp(re, "g"), "[REDACTED]"),
59
- t
60
- );
61
-
62
56
  if (
63
57
  !fs.existsSync(path.join(repo, ".git")) &&
64
58
  !tryGit(["rev-parse", "--is-inside-work-tree"])
@@ -165,8 +159,11 @@ ${
165
159
  }
166
160
  `;
167
161
 
168
- fs.mkdirSync(sourceDir, { recursive: true });
169
- fs.writeFileSync(notePath, redact(note));
162
+ const safety = writeSanitizedSourceNote(notePath, note, {
163
+ sourceId: path.relative(process.cwd(), notePath),
164
+ sourceSystem: "git",
165
+ project: slug,
166
+ });
170
167
 
171
168
  const meta = {
172
169
  connector: "git",
@@ -174,6 +171,10 @@ const meta = {
174
171
  ranAt: new Date().toISOString(),
175
172
  proposedCursor: { lastCommit: head, lastPr },
176
173
  sourceNotes: [path.relative(process.cwd(), notePath)],
174
+ safety: {
175
+ reviewRequired: safety.reviewRequired,
176
+ findings: safety.findings,
177
+ },
177
178
  };
178
179
  if (emitMeta) {
179
180
  fs.mkdirSync(path.dirname(emitMeta), { recursive: true });