ctxpool 0.1.1 → 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.
- package/README.md +94 -103
- package/dist/cli.js +328 -184
- package/dist/cli.js.map +1 -1
- package/dist/clipboard.js +8 -10
- package/dist/clipboard.js.map +1 -1
- package/dist/format.d.ts +32 -0
- package/dist/format.js +110 -0
- package/dist/format.js.map +1 -0
- package/dist/handoff.d.ts +16 -13
- package/dist/handoff.js +441 -706
- package/dist/handoff.js.map +1 -1
- package/dist/mcp.js +99 -221
- package/dist/mcp.js.map +1 -1
- package/dist/pack.d.ts +14 -0
- package/dist/pack.js +121 -0
- package/dist/pack.js.map +1 -0
- package/dist/paths.d.ts +15 -0
- package/dist/paths.js +35 -5
- package/dist/paths.js.map +1 -1
- package/dist/redact.d.ts +6 -0
- package/dist/redact.js +85 -0
- package/dist/redact.js.map +1 -0
- package/dist/repo.d.ts +6 -1
- package/dist/repo.js +109 -123
- package/dist/repo.js.map +1 -1
- package/dist/session.d.ts +40 -1
- package/dist/session.js +1295 -302
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +13 -6
- package/dist/store.js +88 -115
- package/dist/store.js.map +1 -1
- package/dist/text.d.ts +6 -0
- package/dist/text.js +25 -0
- package/dist/text.js.map +1 -0
- package/dist/types.d.ts +44 -7
- package/dist/ui.d.ts +10 -1
- package/dist/ui.js +329 -383
- package/dist/ui.js.map +1 -1
- package/dist/util.d.ts +10 -1
- package/dist/util.js +40 -10
- package/dist/util.js.map +1 -1
- package/dist/verify.d.ts +16 -0
- package/dist/verify.js +74 -0
- package/dist/verify.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist-web/assets/contextpool-mark-4vSRiUeZ.svg +1 -0
- package/dist-web/assets/index-BBjy_S2c.css +2 -0
- package/dist-web/assets/index-QwGbBEu2.js +17 -0
- package/dist-web/index.html +4 -3
- package/package.json +18 -17
- package/dist/logs.d.ts +0 -9
- package/dist/logs.js +0 -82
- package/dist/logs.js.map +0 -1
- package/dist-web/assets/contextpool-mark-Bu_00i5t.svg +0 -16
- package/dist-web/assets/index-BfHRocKl.js +0 -14
- 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
|
package/dist/pack.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pack.js","sourceRoot":"","sources":["../src/pack.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEnE,MAAM,CAAC,MAAM,cAAc,GAA2B;IACpD,QAAQ;IACR,aAAa;IACb,aAAa;IACb,UAAU;IACV,YAAY;IACZ,cAAc;IACd,MAAM;IACN,cAAc;IACd,YAAY;CACb,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAyC;IACnE,MAAM,EAAE,QAAQ;IAChB,WAAW,EAAE,cAAc;IAC3B,aAAa,EAAE,cAAc;IAC7B,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE,aAAa;IACzB,YAAY,EAAE,cAAc;IAC5B,IAAI,EAAE,OAAO;IACb,cAAc,EAAE,eAAe;IAC/B,UAAU,EAAE,YAAY;CACzB,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,SAAyB,EAAE,EAAE,UAA4D,EAAE;IACzI,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpD,MAAM,EAAE,GAAG,QAAQ,SAAS,CAAC,GAAG,IAAI,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;IAClE,OAAO;QACL,EAAE;QACF,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,eAAe;QACpC,SAAS,EAAE,GAAG;QACd,SAAS,EAAE,GAAG;QACd,oBAAoB,EAAE,OAAO,CAAC,oBAAoB,KAAK,IAAI;QAC3D,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;KAChE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAmB,EAAE,KAAa,EAAE,IAAI,GAAG,EAAE;IACpE,OAAO,EAAE,EAAE,EAAE,QAAQ,SAAS,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,EAAE,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACtI,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAiB;IACpD,OAAO;QACL,GAAG,IAAI;QACP,oBAAoB,EAAE,IAAI,CAAC,oBAAoB,KAAK,IAAI;QACxD,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,eAAe;QACzC,KAAK,EAAE,IAAI,CAAC,KAAK;aACd,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;YACrB,GAAG,IAAI;YACP,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK;YACvD,OAAO,EAAE,IAAI,CAAC,OAAO,KAAK,KAAK;YAC/B,KAAK,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE;SACvG,CAAC,CAAC;aACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;aACjC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;KACrD,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAiB;IACvD,MAAM,UAAU,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,oBAAoB,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC;IACnH,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAElD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,QAAQ;QAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,QAAQ,eAAe,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,qDAAqD,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IACxL,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAAE,QAAQ,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;IAC9G,IAAI,CAAC,QAAQ,CAAC,MAAM;QAAE,QAAQ,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;IAEpF,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;IACnC,MAAM,eAAe,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG;QACb,cAAc,UAAU,CAAC,IAAI,EAAE;QAC/B,EAAE;QACF,GAAG,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,OAAO,YAAY,CAAC,eAAe,CAAC,wBAAwB,UAAU,CAAC,oBAAoB,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,EAAE;QACvL,EAAE;QACF,qGAAqG;KACtG,CAAC;IACF,MAAM,QAAQ,GAAG,CAAC,GAAG,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAErE,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,QAAQ;QACR,IAAI,EAAE;YACJ,EAAE,EAAE,UAAU,CAAC,EAAE;YACjB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,SAAS,EAAE,UAAU,CAAC,SAAS;YAC/B,SAAS,EAAE,UAAU,CAAC,SAAS;YAC/B,oBAAoB,EAAE,UAAU,CAAC,oBAAoB;YACrD,cAAc;YACd,eAAe;YACf,QAAQ;YACR,KAAK,EAAE,QAAQ;SAChB;QACD,cAAc;QACd,eAAe;QACf,QAAQ;KACT,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,KAAwB;IAC1C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,QAAQ,IAAI,cAAc,EAAE,CAAC;QACtC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;QAC5E,IAAI,CAAC,UAAU,CAAC,MAAM;YAAE,SAAS;QACjC,QAAQ,CAAC,IAAI,CACX,CAAC,MAAM,eAAe,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CACtL,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,oFAAoF;AACpF,SAAS,SAAS,CAAC,KAAa;IAC9B,IAAI,IAAI,GAAG,UAAU,CAAC;IACtB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QAClD,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAC9E,CAAC"}
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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;
|
|
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"}
|
package/dist/redact.d.ts
ADDED
|
@@ -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
|
-
|
|
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 =
|
|
6
|
-
|
|
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
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
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
|
|
43
|
-
diffStat,
|
|
44
|
-
diffPatch:
|
|
45
|
-
diffPatchTruncated:
|
|
46
|
-
|
|
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 && !
|
|
58
|
-
return slugify(branch.replace(
|
|
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
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
117
|
-
if (
|
|
136
|
+
const fields = line.split("\t");
|
|
137
|
+
if (fields.length < 3)
|
|
118
138
|
continue;
|
|
119
|
-
const
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
return
|
|
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
|
-
|
|
162
|
-
|
|
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 ? `${
|
|
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
|