ctxpool 0.1.0 → 0.4.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 (58) hide show
  1. package/README.md +95 -105
  2. package/dist/cli.js +328 -184
  3. package/dist/cli.js.map +1 -1
  4. package/dist/clipboard.js +8 -10
  5. package/dist/clipboard.js.map +1 -1
  6. package/dist/format.d.ts +32 -0
  7. package/dist/format.js +110 -0
  8. package/dist/format.js.map +1 -0
  9. package/dist/handoff.d.ts +16 -13
  10. package/dist/handoff.js +441 -706
  11. package/dist/handoff.js.map +1 -1
  12. package/dist/mcp.js +99 -221
  13. package/dist/mcp.js.map +1 -1
  14. package/dist/pack.d.ts +14 -0
  15. package/dist/pack.js +121 -0
  16. package/dist/pack.js.map +1 -0
  17. package/dist/paths.d.ts +15 -0
  18. package/dist/paths.js +35 -5
  19. package/dist/paths.js.map +1 -1
  20. package/dist/redact.d.ts +6 -0
  21. package/dist/redact.js +85 -0
  22. package/dist/redact.js.map +1 -0
  23. package/dist/repo.d.ts +6 -1
  24. package/dist/repo.js +109 -123
  25. package/dist/repo.js.map +1 -1
  26. package/dist/session.d.ts +40 -1
  27. package/dist/session.js +1295 -302
  28. package/dist/session.js.map +1 -1
  29. package/dist/store.d.ts +13 -6
  30. package/dist/store.js +88 -115
  31. package/dist/store.js.map +1 -1
  32. package/dist/text.d.ts +6 -0
  33. package/dist/text.js +25 -0
  34. package/dist/text.js.map +1 -0
  35. package/dist/types.d.ts +44 -7
  36. package/dist/ui.d.ts +10 -1
  37. package/dist/ui.js +329 -383
  38. package/dist/ui.js.map +1 -1
  39. package/dist/util.d.ts +10 -1
  40. package/dist/util.js +40 -10
  41. package/dist/util.js.map +1 -1
  42. package/dist/verify.d.ts +16 -0
  43. package/dist/verify.js +74 -0
  44. package/dist/verify.js.map +1 -0
  45. package/dist/version.d.ts +1 -0
  46. package/dist/version.js +4 -0
  47. package/dist/version.js.map +1 -0
  48. package/dist-web/assets/contextpool-mark-4vSRiUeZ.svg +1 -0
  49. package/dist-web/assets/index-BBjy_S2c.css +2 -0
  50. package/dist-web/assets/index-QwGbBEu2.js +17 -0
  51. package/dist-web/index.html +4 -3
  52. package/package.json +19 -18
  53. package/dist/logs.d.ts +0 -9
  54. package/dist/logs.js +0 -82
  55. package/dist/logs.js.map +0 -1
  56. package/dist-web/assets/contextpool-mark-Bu_00i5t.svg +0 -16
  57. package/dist-web/assets/index-BfHRocKl.js +0 -14
  58. package/dist-web/assets/index-l1QjYqDb.css +0 -2
package/dist/pack.js ADDED
@@ -0,0 +1,121 @@
1
+ import { hasRedactions, redactSecrets } from "./redact.js";
2
+ import { estimateTokens, formatTokens, labelize } from "./text.js";
3
+ export const CATEGORY_ORDER = [
4
+ "intent",
5
+ "instruction",
6
+ "next-action",
7
+ "decision",
8
+ "constraint",
9
+ "verification",
10
+ "risk",
11
+ "changed-file",
12
+ "transcript"
13
+ ];
14
+ export const CATEGORY_TITLES = {
15
+ intent: "Intent",
16
+ instruction: "Instructions",
17
+ "next-action": "Next actions",
18
+ decision: "Decisions",
19
+ constraint: "Constraints",
20
+ verification: "Verification",
21
+ risk: "Risks",
22
+ "changed-file": "Changed files",
23
+ transcript: "Transcript"
24
+ };
25
+ export function createContextPack(name, blocks = [], options = {}) {
26
+ const now = options.now || new Date().toISOString();
27
+ const id = `pack_${shortHash(`${name}:${now}:${Math.random()}`)}`;
28
+ return {
29
+ id,
30
+ name: name.trim() || "Untitled pack",
31
+ createdAt: now,
32
+ updatedAt: now,
33
+ includeRawTranscript: options.includeRawTranscript === true,
34
+ items: blocks.map((block, index) => packItem(block, index, id))
35
+ };
36
+ }
37
+ export function packItem(block, order, seed = "") {
38
+ return { id: `item_${shortHash(`${seed}:${block.id}:${order}:${Math.random()}`)}`, blockId: block.id, block, order, enabled: true };
39
+ }
40
+ export function normalizeContextPack(pack) {
41
+ return {
42
+ ...pack,
43
+ includeRawTranscript: pack.includeRawTranscript === true,
44
+ name: pack.name.trim() || "Untitled pack",
45
+ items: pack.items
46
+ .map((item, index) => ({
47
+ ...item,
48
+ order: Number.isFinite(item.order) ? item.order : index,
49
+ enabled: item.enabled !== false,
50
+ block: { ...item.block, text: redactSecrets(item.block.text), estimatedChars: item.block.text.length }
51
+ }))
52
+ .sort((a, b) => a.order - b.order)
53
+ .map((item, index) => ({ ...item, order: index }))
54
+ };
55
+ }
56
+ /**
57
+ * Render the agent-ready prompt for a pack. Transcript blocks are left out unless the pack opts in,
58
+ * so a pack can be shared without leaking raw conversation.
59
+ */
60
+ export function renderContextPackOutput(pack) {
61
+ const normalized = normalizeContextPack(pack);
62
+ const enabled = normalized.items.filter((item) => item.enabled);
63
+ const exported = enabled.filter((item) => normalized.includeRawTranscript || item.block.category !== "transcript");
64
+ const withheld = enabled.length - exported.length;
65
+ const warnings = [];
66
+ if (withheld)
67
+ warnings.push(`${withheld} transcript ${withheld === 1 ? "block is" : "blocks are"} left out. Turn on “Include transcript” to export ${withheld === 1 ? "it" : "them"}.`);
68
+ if (exported.some((item) => hasRedactions(item.block.text)))
69
+ warnings.push("Possible secrets were redacted.");
70
+ if (!exported.length)
71
+ warnings.push("Nothing to export yet. Add or enable blocks.");
72
+ const body = renderBody(exported);
73
+ const estimatedChars = body.length;
74
+ const estimatedTokens = estimateTokens(body);
75
+ const header = [
76
+ `# Context: ${normalized.name}`,
77
+ "",
78
+ `${exported.length} ${exported.length === 1 ? "block" : "blocks"} · ~${formatTokens(estimatedTokens)} tokens · transcript ${normalized.includeRawTranscript ? "included" : "excluded"}`,
79
+ "",
80
+ "Use this context to continue the work without re-deriving earlier decisions, checks, or next steps."
81
+ ];
82
+ const markdown = [...header, ...(body ? ["", body] : [])].join("\n");
83
+ return {
84
+ pack: normalized,
85
+ markdown,
86
+ json: {
87
+ id: normalized.id,
88
+ name: normalized.name,
89
+ createdAt: normalized.createdAt,
90
+ updatedAt: normalized.updatedAt,
91
+ includeRawTranscript: normalized.includeRawTranscript,
92
+ estimatedChars,
93
+ estimatedTokens,
94
+ warnings,
95
+ items: exported
96
+ },
97
+ estimatedChars,
98
+ estimatedTokens,
99
+ warnings
100
+ };
101
+ }
102
+ function renderBody(items) {
103
+ const sections = [];
104
+ for (const category of CATEGORY_ORDER) {
105
+ const inCategory = items.filter((item) => item.block.category === category);
106
+ if (!inCategory.length)
107
+ continue;
108
+ sections.push([`## ${CATEGORY_TITLES[category] || labelize(category)}`, ...inCategory.map((item) => `- ${item.block.text} _(${item.block.sourceTask}, ${item.block.provenance.kind})_`)].join("\n"));
109
+ }
110
+ return sections.join("\n\n");
111
+ }
112
+ /** FNV-1a, enough for collision-resistant local ids without a crypto dependency. */
113
+ function shortHash(value) {
114
+ let hash = 0x811c9dc5;
115
+ for (let index = 0; index < value.length; index++) {
116
+ hash ^= value.charCodeAt(index);
117
+ hash = Math.imul(hash, 0x01000193);
118
+ }
119
+ return (hash >>> 0).toString(16).padStart(8, "0") + Date.now().toString(36);
120
+ }
121
+ //# sourceMappingURL=pack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pack.js","sourceRoot":"","sources":["../src/pack.ts"],"names":[],"mappings":"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"}
package/dist/paths.d.ts CHANGED
@@ -6,5 +6,20 @@ export type CtxPaths = {
6
6
  teamExportsDir: string;
7
7
  configPath: string;
8
8
  };
9
+ export type CtxConfig = {
10
+ version: 1;
11
+ ui: {
12
+ host: string;
13
+ port: number;
14
+ };
15
+ ollama: {
16
+ enabled: boolean;
17
+ model?: string;
18
+ };
19
+ };
20
+ export declare const DEFAULT_CONFIG: CtxConfig;
9
21
  export declare function getCtxPaths(): CtxPaths;
22
+ /** Create the store directories. They hold transcripts and diffs, so they are private to the user. */
10
23
  export declare function ensureCtxDirs(paths?: CtxPaths): CtxPaths;
24
+ /** Read config.json, falling back to defaults for anything missing or malformed. */
25
+ export declare function loadConfig(paths?: CtxPaths): CtxConfig;
package/dist/paths.js CHANGED
@@ -1,6 +1,11 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ export const DEFAULT_CONFIG = {
5
+ version: 1,
6
+ ui: { host: "127.0.0.1", port: 4377 },
7
+ ollama: { enabled: false }
8
+ };
4
9
  export function getCtxPaths() {
5
10
  const home = process.env.CTXPOOL_HOME || join(homedir(), ".ctxpool");
6
11
  return {
@@ -12,11 +17,36 @@ export function getCtxPaths() {
12
17
  configPath: join(home, "config.json")
13
18
  };
14
19
  }
20
+ /** Create the store directories. They hold transcripts and diffs, so they are private to the user. */
15
21
  export function ensureCtxDirs(paths = getCtxPaths()) {
16
- mkdirSync(paths.home, { recursive: true });
17
- mkdirSync(paths.rawDir, { recursive: true });
18
- mkdirSync(paths.handoffsDir, { recursive: true });
19
- mkdirSync(paths.teamExportsDir, { recursive: true });
22
+ for (const dir of [paths.home, paths.rawDir, paths.handoffsDir, paths.teamExportsDir]) {
23
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
24
+ }
20
25
  return paths;
21
26
  }
27
+ /** Read config.json, falling back to defaults for anything missing or malformed. */
28
+ export function loadConfig(paths = getCtxPaths()) {
29
+ if (!existsSync(paths.configPath))
30
+ return DEFAULT_CONFIG;
31
+ let raw;
32
+ try {
33
+ raw = JSON.parse(readFileSync(paths.configPath, "utf8"));
34
+ }
35
+ catch {
36
+ return DEFAULT_CONFIG;
37
+ }
38
+ const ui = (raw.ui || {});
39
+ const ollama = (raw.ollama || {});
40
+ return {
41
+ version: 1,
42
+ ui: {
43
+ host: typeof ui.host === "string" && ui.host ? ui.host : DEFAULT_CONFIG.ui.host,
44
+ port: typeof ui.port === "number" && Number.isInteger(ui.port) ? ui.port : DEFAULT_CONFIG.ui.port
45
+ },
46
+ ollama: {
47
+ enabled: ollama.enabled === true,
48
+ model: typeof ollama.model === "string" && ollama.model ? ollama.model : undefined
49
+ }
50
+ };
51
+ }
22
52
  //# sourceMappingURL=paths.js.map
package/dist/paths.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAWjC,MAAM,UAAU,WAAW;IACzB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;IACrE,OAAO;QACL,IAAI;QACJ,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC;QAChC,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC;QACzB,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC;QACnC,cAAc,EAAE,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC;QAC1C,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC;KACtC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAK,GAAG,WAAW,EAAE;IACjD,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAiBjC,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,OAAO,EAAE,CAAC;IACV,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE;IACrC,MAAM,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE;CAC3B,CAAC;AAEF,MAAM,UAAU,WAAW;IACzB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;IACrE,OAAO;QACL,IAAI;QACJ,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC;QAChC,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC;QACzB,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC;QACnC,cAAc,EAAE,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC;QAC1C,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC;KACtC,CAAC;AACJ,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,aAAa,CAAC,KAAK,GAAG,WAAW,EAAE;IACjD,KAAK,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;QACtF,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,UAAU,CAAC,KAAK,GAAG,WAAW,EAAE;IAC9C,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC;QAAE,OAAO,cAAc,CAAC;IACzD,IAAI,GAA4B,CAAC;IACjC,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,MAAM,CAAC,CAA4B,CAAC;IACtF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,cAAc,CAAC;IACxB,CAAC;IACD,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAA4B,CAAC;IACrD,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAA4B,CAAC;IAC7D,OAAO;QACL,OAAO,EAAE,CAAC;QACV,EAAE,EAAE;YACF,IAAI,EAAE,OAAO,EAAE,CAAC,IAAI,KAAK,QAAQ,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,EAAE,CAAC,IAAI;YAC/E,IAAI,EAAE,OAAO,EAAE,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,EAAE,CAAC,IAAI;SAClG;QACD,MAAM,EAAE;YACN,OAAO,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI;YAChC,KAAK,EAAE,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;SACnF;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ export declare function redactSecrets(text: string): string;
2
+ export declare function hasRedactions(text: string): boolean;
3
+ /** Recursively redact every string in a JSON-like value. */
4
+ export declare function redactDeep<T>(value: T): T;
5
+ /** Files whose contents should never be copied into a handoff, even redacted. */
6
+ export declare function isSensitivePath(path: string): boolean;
package/dist/redact.js ADDED
@@ -0,0 +1,85 @@
1
+ // Secret redaction shared by the CLI, MCP server, and web UI. Keep this module free of Node imports.
2
+ const TOKEN_RULES = [
3
+ [/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/g, "[redacted-private-key]"],
4
+ [/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, "[redacted-aws-key]"],
5
+ [/\bgithub_pat_[A-Za-z0-9_]{22,}/g, "[redacted-token]"],
6
+ [/\bgh[pousr]_[A-Za-z0-9]{30,}/g, "[redacted-token]"],
7
+ [/\bglpat-[A-Za-z0-9_-]{20,}/g, "[redacted-token]"],
8
+ [/\bsk-(?:proj-|ant-[a-z]+\d*-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}/g, "[redacted-token]"],
9
+ [/\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g, "[redacted-token]"],
10
+ [/\bwhsec_[A-Za-z0-9+/=]{20,}/g, "[redacted-token]"],
11
+ [/\bxox[abposr]-[A-Za-z0-9-]{10,}/g, "[redacted-token]"],
12
+ [/\bAIza[0-9A-Za-z_-]{35}/g, "[redacted-token]"],
13
+ [/\bnpm_[A-Za-z0-9]{36}/g, "[redacted-token]"],
14
+ [/\bhf_[A-Za-z0-9]{30,}/g, "[redacted-token]"],
15
+ [/\bpypi-[A-Za-z0-9_-]{50,}/g, "[redacted-token]"],
16
+ [/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, "[redacted-jwt]"],
17
+ [/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/-]{20,}=*/g, "$1 [redacted]"]
18
+ ];
19
+ const URL_CREDENTIALS = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s:/@]+):([^\s/@]+)@/gi;
20
+ const ASSIGNMENT = /\b([A-Za-z0-9_.-]*?(?:api[_-]?key|secret|token|passw(?:or)?d|private[_-]?key|access[_-]?key|client[_-]?secret|database[_-]?url|dsn)(?:[_.-][A-Za-z0-9]+)*)\b(["']?\s*[:=]\s*["'`]?)([^\s"'`,;)}\]]+)/gi;
21
+ const NOT_A_LITERAL = /^(?:\$|process\.env|import\.meta|os\.environ|env\b|<|\{|\[redacted|null$|undefined$|true$|false$|none$|string$|number$|boolean$|any$|unknown$|await$|new$|this\b|typeof$|x{3,}|\*{3,}|\.{3})/i;
22
+ const HIGH_ENTROPY = /[A-Za-z0-9+/_-]{32,}={0,2}/g;
23
+ export function redactSecrets(text) {
24
+ if (!text)
25
+ return text;
26
+ let next = text;
27
+ for (const [pattern, replacement] of TOKEN_RULES)
28
+ next = next.replace(pattern, replacement);
29
+ next = next.replace(URL_CREDENTIALS, "$1$2:[redacted]@");
30
+ next = next.replace(ASSIGNMENT, (match, key, separator, value) => {
31
+ if (value.length < 4 || value.includes("(") || NOT_A_LITERAL.test(value))
32
+ return match;
33
+ return `${key}${separator}[redacted]`;
34
+ });
35
+ return next.replace(HIGH_ENTROPY, (token) => (looksLikeSecret(token) ? "[redacted-secret]" : token));
36
+ }
37
+ export function hasRedactions(text) {
38
+ return /\[redacted(?:-[a-z-]+)?\]/.test(text);
39
+ }
40
+ /** Recursively redact every string in a JSON-like value. */
41
+ export function redactDeep(value) {
42
+ if (typeof value === "string")
43
+ return redactSecrets(value);
44
+ if (Array.isArray(value))
45
+ return value.map((item) => redactDeep(item));
46
+ if (value && typeof value === "object") {
47
+ const result = {};
48
+ for (const [key, item] of Object.entries(value))
49
+ result[key] = redactDeep(item);
50
+ return result;
51
+ }
52
+ return value;
53
+ }
54
+ /** Files whose contents should never be copied into a handoff, even redacted. */
55
+ export function isSensitivePath(path) {
56
+ const name = path.split(/[\\/]/).pop() || path;
57
+ if (/^\.env\.(?:example|sample|template|defaults)$/i.test(name))
58
+ return false;
59
+ return (/^\.env(?:\..+)?$/i.test(name) ||
60
+ /^(?:\.npmrc|\.pypirc|\.netrc|\.pgpass|credentials(?:\.json)?|secrets?\.(?:json|ya?ml|toml))$/i.test(name) ||
61
+ /^id_(?:rsa|dsa|ecdsa|ed25519)$/i.test(name) ||
62
+ /\.(?:pem|key|p12|pfx|jks|keystore)$/i.test(name));
63
+ }
64
+ function looksLikeSecret(token) {
65
+ const bare = token.replace(/[-_=]/g, "");
66
+ if (/^[0-9a-f]+$/i.test(bare))
67
+ return false;
68
+ if (!/[a-z]/.test(token) || !/[A-Z]/.test(token) || !/[0-9]/.test(token))
69
+ return false;
70
+ if (token.split("/").length > 3)
71
+ return false;
72
+ return shannonEntropy(token) >= 4.5;
73
+ }
74
+ function shannonEntropy(value) {
75
+ const counts = new Map();
76
+ for (const char of value)
77
+ counts.set(char, (counts.get(char) || 0) + 1);
78
+ let entropy = 0;
79
+ for (const count of counts.values()) {
80
+ const p = count / value.length;
81
+ entropy -= p * Math.log2(p);
82
+ }
83
+ return entropy;
84
+ }
85
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"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"}
package/dist/repo.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import type { FileChange, GitSnapshot } from "./types.js";
2
+ export declare function gitTopLevel(cwd?: string): string | undefined;
2
3
  export declare function captureGitSnapshot(cwd?: string): GitSnapshot;
3
4
  export declare function inferTaskName(explicitTask: string | undefined, repo: GitSnapshot): string;
4
- export declare function parseFileChanges(statusOutput: string, numstatOutput?: string, patch?: string): FileChange[];
5
+ /**
6
+ * Parse `git status --porcelain=v1` output into file changes. Accepts NUL-separated (-z) or
7
+ * newline-separated output; numstat may be in either form too.
8
+ */
9
+ export declare function parseFileChanges(statusOutput: string, numstatOutput?: string): FileChange[];
package/dist/repo.js CHANGED
@@ -1,13 +1,21 @@
1
1
  import { basename } from "node:path";
2
+ import { isSensitivePath, redactSecrets } from "./redact.js";
2
3
  import { limit, runOptional, slugify } from "./util.js";
3
4
  const MAX_DIFF_PATCH_CHARS = 120_000;
5
+ const MAX_FILE_CHANGES = 200;
6
+ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
7
+ const GIT_FLAGS = ["-c", "core.quotePath=false", "-c", "color.ui=never"];
8
+ function git(args, cwd) {
9
+ return runOptional("git", [...GIT_FLAGS, ...args], cwd);
10
+ }
11
+ export function gitTopLevel(cwd = process.cwd()) {
12
+ return git(["rev-parse", "--show-toplevel"], cwd).trim() || undefined;
13
+ }
4
14
  export function captureGitSnapshot(cwd = process.cwd()) {
5
- const topLevel = runOptional("git", ["rev-parse", "--show-toplevel"], cwd);
6
- const repoPath = topLevel || cwd;
7
- const isGitRepo = Boolean(topLevel);
8
- if (!isGitRepo) {
15
+ const topLevel = gitTopLevel(cwd);
16
+ if (!topLevel) {
9
17
  return {
10
- repoPath,
18
+ repoPath: cwd,
11
19
  isGitRepo: false,
12
20
  branch: "no-git",
13
21
  commit: "unknown",
@@ -16,84 +24,81 @@ export function captureGitSnapshot(cwd = process.cwd()) {
16
24
  diffStat: "",
17
25
  diffPatch: "",
18
26
  diffPatchTruncated: false,
19
- stagedDiffPatch: "",
20
- stagedDiffPatchTruncated: false,
21
- unstagedDiffPatch: "",
22
- unstagedDiffPatchTruncated: false,
23
27
  recentCommits: []
24
28
  };
25
29
  }
26
- const branch = runOptional("git", ["branch", "--show-current"], repoPath) || "detached";
27
- const commit = runOptional("git", ["rev-parse", "--short", "HEAD"], repoPath) || "unknown";
28
- const status = runOptional("git", ["status", "--porcelain=v1"], repoPath);
29
- const diffStat = runOptional("git", ["diff", "--stat"], repoPath);
30
- const allPatch = truncatePatch(runOptional("git", ["diff", "HEAD", "--no-ext-diff", "--unified=3"], repoPath));
31
- const stagedPatch = truncatePatch(runOptional("git", ["diff", "--cached", "--no-ext-diff", "--unified=3"], repoPath));
32
- const unstagedPatch = truncatePatch(runOptional("git", ["diff", "--no-ext-diff", "--unified=3"], repoPath));
33
- const numstat = runOptional("git", ["diff", "HEAD", "--numstat"], repoPath);
34
- const recent = runOptional("git", ["log", "--oneline", "-5"], repoPath);
35
- const fileChanges = parseFileChanges(status, numstat, allPatch.patch);
30
+ const repoPath = topLevel;
31
+ const hasHead = Boolean(git(["rev-parse", "--verify", "--quiet", "HEAD"], repoPath).trim());
32
+ const base = hasHead ? "HEAD" : EMPTY_TREE;
33
+ const branch = git(["branch", "--show-current"], repoPath).trim() || (hasHead ? "detached" : "unborn");
34
+ const commit = hasHead ? git(["rev-parse", "--short", "HEAD"], repoPath).trim() : "none";
35
+ const status = git(["status", "--porcelain=v1", "-z"], repoPath);
36
+ const numstat = git(["diff", base, "--numstat", "-z", "--no-ext-diff"], repoPath);
37
+ const fileChanges = parseFileChanges(status, numstat).slice(0, MAX_FILE_CHANGES);
38
+ const patch = buildPatch(git(["diff", base, "--no-ext-diff", "--unified=3"], repoPath));
36
39
  return {
37
40
  repoPath,
38
- isGitRepo,
41
+ isGitRepo: true,
39
42
  branch,
40
43
  commit,
41
44
  dirtyFiles: limit(fileChanges.map((change) => change.path), 40),
42
- fileChanges: limitFileChanges(fileChanges, 120),
43
- diffStat,
44
- diffPatch: allPatch.patch,
45
- diffPatchTruncated: allPatch.truncated,
46
- stagedDiffPatch: stagedPatch.patch,
47
- stagedDiffPatchTruncated: stagedPatch.truncated,
48
- unstagedDiffPatch: unstagedPatch.patch,
49
- unstagedDiffPatchTruncated: unstagedPatch.truncated,
50
- recentCommits: limit(recent.split("\n"), 5)
45
+ fileChanges,
46
+ diffStat: git(["diff", base, "--shortstat", "--no-ext-diff"], repoPath).trim(),
47
+ diffPatch: patch.patch,
48
+ diffPatchTruncated: patch.truncated,
49
+ recentCommits: limit(git(["log", "--oneline", "-5"], repoPath).split("\n"), 5)
51
50
  };
52
51
  }
52
+ const BRANCH_PREFIX = /^(?:feature|feat|fix|bugfix|hotfix|chore|task|refactor|docs|claude|codex|cursor|agent)\//;
53
+ const TRUNK_BRANCHES = ["main", "master", "develop", "dev", "trunk", "detached", "unborn", "no-git"];
53
54
  export function inferTaskName(explicitTask, repo) {
54
55
  if (explicitTask)
55
56
  return slugify(explicitTask);
56
57
  const branch = repo.branch.trim();
57
- if (branch && !["main", "master", "develop", "dev", "trunk", "detached", "no-git"].includes(branch)) {
58
- return slugify(branch.replace(/^(feature|feat|fix|bugfix|chore|task)\//, ""));
59
- }
58
+ if (branch && !TRUNK_BRANCHES.includes(branch))
59
+ return slugify(branch.replace(BRANCH_PREFIX, ""));
60
60
  return slugify(basename(repo.repoPath));
61
61
  }
62
- export function parseFileChanges(statusOutput, numstatOutput = "", patch = "") {
62
+ /**
63
+ * Parse `git status --porcelain=v1` output into file changes. Accepts NUL-separated (-z) or
64
+ * newline-separated output; numstat may be in either form too.
65
+ */
66
+ export function parseFileChanges(statusOutput, numstatOutput = "") {
63
67
  const stats = parseNumstat(numstatOutput);
64
- const diffs = splitPatchByFile(patch);
65
- return statusOutput
66
- .split("\n")
67
- .map((line) => parseStatusLine(line, stats, diffs))
68
- .filter((change) => Boolean(change));
69
- }
70
- function parseStatusLine(line, stats, diffs) {
71
- if (!line.trim())
72
- return undefined;
73
- const trimmedUnstaged = /^[MADRC] .+/.test(line) && line[2] !== " ";
74
- const indexStatus = trimmedUnstaged ? " " : line[0] || " ";
75
- const worktreeStatus = trimmedUnstaged ? line[0] : line[1] || " ";
76
- const rawPath = (trimmedUnstaged ? line.slice(2) : line.slice(3)).trim();
77
- if (!rawPath)
78
- return undefined;
79
- const renamed = rawPath.includes(" -> ");
80
- const [oldPath, currentPath] = renamed ? rawPath.split(" -> ", 2) : ["", rawPath];
81
- const path = currentPath || rawPath;
82
- const status = mapStatus(indexStatus, worktreeStatus, renamed);
83
- const stat = stats.get(path) || stats.get(oldPath) || { additions: 0, deletions: 0 };
84
- return {
85
- path,
86
- oldPath: oldPath || undefined,
87
- status,
88
- indexStatus: indexStatus.trim() || undefined,
89
- worktreeStatus: worktreeStatus.trim() || undefined,
90
- additions: stat.additions,
91
- deletions: stat.deletions,
92
- staged: indexStatus !== " " && indexStatus !== "?",
93
- unstaged: worktreeStatus !== " " && worktreeStatus !== "?",
94
- untracked: indexStatus === "?" && worktreeStatus === "?",
95
- diff: diffs.get(path) || diffs.get(oldPath)
96
- };
68
+ const changes = [];
69
+ const nulSeparated = statusOutput.includes("\0");
70
+ const entries = nulSeparated ? statusOutput.split("\0") : statusOutput.split("\n");
71
+ for (let index = 0; index < entries.length; index++) {
72
+ const entry = entries[index];
73
+ if (entry.length < 4)
74
+ continue;
75
+ const indexStatus = entry[0];
76
+ const worktreeStatus = entry[1];
77
+ let path = entry.slice(3);
78
+ let oldPath;
79
+ if (indexStatus === "R" || indexStatus === "C" || worktreeStatus === "R" || worktreeStatus === "C") {
80
+ if (nulSeparated) {
81
+ oldPath = entries[++index];
82
+ }
83
+ else if (path.includes(" -> ")) {
84
+ [oldPath, path] = path.split(" -> ", 2);
85
+ }
86
+ }
87
+ const stat = stats.get(path) || (oldPath ? stats.get(oldPath) : undefined) || { additions: 0, deletions: 0 };
88
+ changes.push({
89
+ path,
90
+ oldPath,
91
+ status: mapStatus(indexStatus, worktreeStatus, Boolean(oldPath)),
92
+ indexStatus: indexStatus.trim() || undefined,
93
+ worktreeStatus: worktreeStatus.trim() || undefined,
94
+ additions: stat.additions,
95
+ deletions: stat.deletions,
96
+ staged: indexStatus !== " " && indexStatus !== "?",
97
+ unstaged: worktreeStatus !== " " && worktreeStatus !== "?",
98
+ untracked: indexStatus === "?" && worktreeStatus === "?"
99
+ });
100
+ }
101
+ return changes;
97
102
  }
98
103
  function mapStatus(indexStatus, worktreeStatus, renamed) {
99
104
  if (indexStatus === "?" && worktreeStatus === "?")
@@ -106,77 +111,58 @@ function mapStatus(indexStatus, worktreeStatus, renamed) {
106
111
  return "added";
107
112
  if (indexStatus === "D" || worktreeStatus === "D")
108
113
  return "deleted";
109
- if (indexStatus === "M" || worktreeStatus === "M")
114
+ if (indexStatus === "M" || worktreeStatus === "M" || indexStatus === "T" || worktreeStatus === "T")
110
115
  return "modified";
111
116
  return "unknown";
112
117
  }
113
118
  function parseNumstat(output) {
114
119
  const stats = new Map();
120
+ const count = (value) => (value === "-" ? 0 : Number(value) || 0);
121
+ if (output.includes("\0")) {
122
+ const parts = output.split("\0");
123
+ for (let index = 0; index < parts.length; index++) {
124
+ const fields = parts[index].replace(/^\n/, "").split("\t");
125
+ if (fields.length < 3)
126
+ continue;
127
+ const stat = { additions: count(fields[0]), deletions: count(fields[1]) };
128
+ // Renames: "added\tdeleted\t" followed by the old and new paths as separate entries.
129
+ const path = fields[2] || (index += 2, parts[index]);
130
+ if (path)
131
+ stats.set(path, stat);
132
+ }
133
+ return stats;
134
+ }
115
135
  for (const line of output.split("\n")) {
116
- const parts = line.split("\t");
117
- if (parts.length < 3)
136
+ const fields = line.split("\t");
137
+ if (fields.length < 3)
118
138
  continue;
119
- const additions = parts[0] === "-" ? 0 : Number(parts[0]) || 0;
120
- const deletions = parts[1] === "-" ? 0 : Number(parts[1]) || 0;
121
- const path = normalizeNumstatPath(parts.slice(2).join("\t"));
139
+ const path = expandRenamePath(fields.slice(2).join("\t"));
122
140
  if (path)
123
- stats.set(path, { additions, deletions });
141
+ stats.set(path, { additions: count(fields[0]), deletions: count(fields[1]) });
124
142
  }
125
143
  return stats;
126
144
  }
127
- function normalizeNumstatPath(path) {
128
- const renameMatch = path.match(/^(.*) => (.*)$/);
129
- if (!renameMatch)
130
- return path.trim();
131
- const [, before, after] = renameMatch;
132
- const prefix = before.includes("{") && after.includes("}") ? "" : "";
133
- return `${prefix}${after.replace(/[{}]/g, "")}`.trim();
134
- }
135
- function splitPatchByFile(patch) {
136
- const chunks = new Map();
137
- const lines = patch.split("\n");
138
- let currentPath = "";
139
- let current = [];
140
- const flush = () => {
141
- if (currentPath && current.length)
142
- chunks.set(currentPath, current.join("\n"));
143
- };
144
- for (const line of lines) {
145
- if (line.startsWith("diff --git ")) {
146
- flush();
147
- current = [line];
148
- currentPath = parseDiffPath(line);
149
- }
150
- else if (current.length) {
151
- current.push(line);
152
- }
153
- }
154
- flush();
155
- return chunks;
156
- }
157
- function parseDiffPath(line) {
158
- const match = line.match(/^diff --git a\/(.+) b\/(.+)$/);
159
- return match?.[2] || "";
145
+ /** `src/{old => new}/file.ts` -> `src/new/file.ts`; `old.ts => new.ts` -> `new.ts`. */
146
+ function expandRenamePath(path) {
147
+ const braced = /^(.*)\{(.*) => (.*)\}(.*)$/.exec(path);
148
+ if (braced)
149
+ return `${braced[1]}${braced[3]}${braced[4]}`.replace(/\/\//g, "/").trim();
150
+ const plain = /^(.*) => (.*)$/.exec(path);
151
+ return (plain ? plain[2] : path).trim();
160
152
  }
161
- function truncatePatch(value) {
162
- const truncated = value.length > MAX_DIFF_PATCH_CHARS;
153
+ /** Redact secrets and drop the bodies of sensitive files (.env, keys) before the patch is stored. */
154
+ function buildPatch(raw) {
155
+ const sections = raw.split(/(?=^diff --git )/m).map((section) => {
156
+ const path = /^diff --git a\/.+ b\/(.+)$/m.exec(section)?.[1] || "";
157
+ if (!isSensitivePath(path))
158
+ return section;
159
+ return `${section.split("\n")[0]}\n[ContextPool: diff omitted for sensitive file]\n`;
160
+ });
161
+ const patch = redactSecrets(sections.join(""));
162
+ const truncated = patch.length > MAX_DIFF_PATCH_CHARS;
163
163
  return {
164
- patch: truncated ? `${value.slice(0, MAX_DIFF_PATCH_CHARS)}\n\n[ContextPool: diff truncated at ${MAX_DIFF_PATCH_CHARS} characters]` : value,
164
+ patch: truncated ? `${patch.slice(0, MAX_DIFF_PATCH_CHARS)}\n\n[ContextPool: diff truncated at ${MAX_DIFF_PATCH_CHARS} characters]` : patch,
165
165
  truncated
166
166
  };
167
167
  }
168
- function limitFileChanges(changes, count) {
169
- const seen = new Set();
170
- const result = [];
171
- for (const change of changes) {
172
- const key = `${change.status}:${change.path}`;
173
- if (seen.has(key))
174
- continue;
175
- seen.add(key);
176
- result.push(change);
177
- if (result.length >= count)
178
- break;
179
- }
180
- return result;
181
- }
182
168
  //# sourceMappingURL=repo.js.map