@senad-d/branchme 0.3.15 → 0.3.17

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.
@@ -0,0 +1,119 @@
1
+ import type { TruncatedText, TruncationMetadata, TruncationStrategy } from "./types.ts";
2
+
3
+ export interface TruncateTextOptions {
4
+ maxLines?: number;
5
+ maxBytes?: number;
6
+ strategy?: TruncationStrategy;
7
+ label?: string;
8
+ }
9
+
10
+ export function countLines(text: string): number {
11
+ if (text.length === 0) return 0;
12
+ return text.split(/\r?\n/).length;
13
+ }
14
+
15
+ export function byteLength(text: string): number {
16
+ return Buffer.byteLength(text, "utf8");
17
+ }
18
+
19
+ function sliceToBytesFromHead(text: string, maxBytes: number): string {
20
+ let bytes = 0;
21
+ let output = "";
22
+ for (const char of text) {
23
+ const nextBytes = byteLength(char);
24
+ if (bytes + nextBytes > maxBytes) break;
25
+ output += char;
26
+ bytes += nextBytes;
27
+ }
28
+ return output;
29
+ }
30
+
31
+ function sliceToBytesFromTail(text: string, maxBytes: number): string {
32
+ let bytes = 0;
33
+ const chars = Array.from(text);
34
+ const kept: string[] = [];
35
+ for (let index = chars.length - 1; index >= 0; index -= 1) {
36
+ const char = chars[index];
37
+ if (char === undefined) continue;
38
+ const nextBytes = byteLength(char);
39
+ if (bytes + nextBytes > maxBytes) break;
40
+ kept.push(char);
41
+ bytes += nextBytes;
42
+ }
43
+ kept.reverse();
44
+ return kept.join("");
45
+ }
46
+
47
+ function truncateLines(text: string, maxLines: number, strategy: TruncationStrategy): string {
48
+ if (maxLines <= 0) return "";
49
+ const lines = text.split(/\r?\n/);
50
+ if (lines.length <= maxLines) return text;
51
+ return strategy === "tail" ? lines.slice(-maxLines).join("\n") : lines.slice(0, maxLines).join("\n");
52
+ }
53
+
54
+ function truncateBytes(text: string, maxBytes: number, strategy: TruncationStrategy): string {
55
+ if (byteLength(text) <= maxBytes) return text;
56
+ return strategy === "tail" ? sliceToBytesFromTail(text, maxBytes) : sliceToBytesFromHead(text, maxBytes);
57
+ }
58
+
59
+ export function formatTruncationNotice(metadata: TruncationMetadata): string | undefined {
60
+ if (!metadata.truncated) return undefined;
61
+ const label = metadata.label ? `${metadata.label}: ` : "";
62
+ return `[Truncated ${label}showing ${metadata.outputLines} of ${metadata.originalLines} lines, ${metadata.outputBytes} of ${metadata.originalBytes} bytes.]`;
63
+ }
64
+
65
+ export function truncateText(text: string, options: TruncateTextOptions): TruncatedText {
66
+ const strategy = options.strategy ?? "head";
67
+ const originalBytes = byteLength(text);
68
+ const originalLines = countLines(text);
69
+
70
+ let output = text;
71
+ if (typeof options.maxLines === "number" && options.maxLines >= 0) {
72
+ output = truncateLines(output, options.maxLines, strategy);
73
+ }
74
+ if (typeof options.maxBytes === "number" && options.maxBytes >= 0) {
75
+ output = truncateBytes(output, options.maxBytes, strategy);
76
+ }
77
+
78
+ const metadata: TruncationMetadata = {
79
+ truncated: output !== text,
80
+ strategy,
81
+ originalBytes,
82
+ outputBytes: byteLength(output),
83
+ originalLines,
84
+ outputLines: countLines(output),
85
+ label: options.label,
86
+ };
87
+
88
+ return {
89
+ text: output,
90
+ metadata,
91
+ notice: formatTruncationNotice(metadata),
92
+ };
93
+ }
94
+
95
+ export function appendTruncationNotice(truncated: TruncatedText): string {
96
+ if (!truncated.notice) return truncated.text;
97
+ return `${truncated.text}\n\n${truncated.notice}`;
98
+ }
99
+
100
+ export function collectTruncationMetadata(items: Array<{ truncation?: TruncationMetadata }>): TruncationMetadata[] {
101
+ return items.flatMap((item) => (item.truncation ? [item.truncation] : []));
102
+ }
103
+
104
+ function escapedControlCharacter(character: string, codePoint: number): string {
105
+ if (character === "\r") return String.raw`\r`;
106
+ if (character === "\n") return String.raw`\n`;
107
+ if (character === "\t") return String.raw`\t`;
108
+ return String.raw`\x${codePoint.toString(16).padStart(2, "0")}`;
109
+ }
110
+
111
+ export function formatDisplayPath(path: string): string {
112
+ let output = "";
113
+ for (const character of path) {
114
+ const codePoint = character.codePointAt(0);
115
+ if (codePoint === undefined) continue;
116
+ output += codePoint <= 0x1f || codePoint === 0x7f ? escapedControlCharacter(character, codePoint) : character;
117
+ }
118
+ return output;
119
+ }
package/src/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { CONVENTIONAL_COMMIT_TYPES } from "./constants.ts";
2
+
1
3
  export interface GitHubRepository {
2
4
  owner: string;
3
5
  repo: string;
@@ -35,6 +37,80 @@ export interface ListBranchesToolInput {
35
37
  patterns?: string[];
36
38
  }
37
39
 
40
+ export interface GitStatusToolInput {
41
+ paths?: string[];
42
+ worktreePath?: string;
43
+ after?: string;
44
+ }
45
+
46
+ export interface GitStatusEntry {
47
+ kind: "tracked" | "untracked" | "conflict";
48
+ /** Display only unless reusablePath is true. */
49
+ path: string;
50
+ originalPath?: string;
51
+ reusablePath: boolean;
52
+ indexStatus: string;
53
+ worktreeStatus: string;
54
+ submodule: string;
55
+ /** Present only for explicitly filtered, reusable paths. No file content is returned. */
56
+ worktreeExpectation?: { fingerprint: string | null; reason?: string };
57
+ }
58
+
59
+ export interface GitStatusDetails {
60
+ action: "git_status";
61
+ repoRoot: string;
62
+ head: string | null;
63
+ branch: string | null;
64
+ detached: boolean;
65
+ index: string;
66
+ snapshot: string;
67
+ operations: string[];
68
+ filtered: boolean;
69
+ pathFilters: string[];
70
+ hasChanges: boolean;
71
+ total: number;
72
+ entries: GitStatusEntry[];
73
+ offset: number;
74
+ complete: boolean;
75
+ next: string | null;
76
+ }
77
+
78
+ export type GitDiffMode = "unstaged" | "staged" | "range";
79
+ export type GitDiffOutput = "patch" | "stat" | "name-status";
80
+
81
+ export interface GitDiffToolInput {
82
+ mode?: GitDiffMode;
83
+ baseRef?: string;
84
+ targetRef?: string;
85
+ mergeBase?: boolean;
86
+ output?: GitDiffOutput;
87
+ paths?: string[];
88
+ contextLines?: number;
89
+ worktreePath?: string;
90
+ }
91
+
92
+ export interface GitDiffDetails {
93
+ action: "git_diff";
94
+ repoRoot: string;
95
+ mode: GitDiffMode;
96
+ output: GitDiffOutput;
97
+ /** Redacted/abbreviated literal filter previews, not reusable path identities. */
98
+ pathFilters: string[];
99
+ comparison?: {
100
+ baseHead: string;
101
+ targetHead: string;
102
+ /** The captured base commit, or the unique merge base when requested. */
103
+ diffBaseHead: string;
104
+ };
105
+ hasDifferences: boolean;
106
+ /** Redacted, control-escaped, bounded display text, not an apply-ready patch. */
107
+ diff: string;
108
+ truncated: boolean;
109
+ /** Counts after redaction/control escaping, before truncation. */
110
+ totalBytes: number;
111
+ totalLines: number;
112
+ }
113
+
38
114
  export interface FetchRemoteToolInput {
39
115
  remote?: string;
40
116
  prune?: boolean;
@@ -726,3 +802,200 @@ export interface GitExecResult {
726
802
  code: number;
727
803
  killed: boolean;
728
804
  }
805
+
806
+ export type ConventionalCommitType = (typeof CONVENTIONAL_COMMIT_TYPES)[number];
807
+ export type GitChangeScope = "staged" | "unstaged";
808
+ export type TruncationStrategy = "head" | "tail";
809
+ export type ProjectContextEntryKind = "metadata" | "changed-file-snippet";
810
+ export type SkippedContextReason =
811
+ | "sensitive"
812
+ | "generated"
813
+ | "binary"
814
+ | "symlink"
815
+ | "missing"
816
+ | "too-large"
817
+ | "unreadable"
818
+ | "outside-repository";
819
+
820
+ export interface CommitGitOptions {
821
+ cwd: string;
822
+ timeout?: number;
823
+ signal?: AbortSignal;
824
+ }
825
+
826
+ export interface TruncationMetadata {
827
+ truncated: boolean;
828
+ strategy: TruncationStrategy;
829
+ originalBytes: number;
830
+ outputBytes: number;
831
+ originalLines: number;
832
+ outputLines: number;
833
+ label?: string;
834
+ }
835
+
836
+ export interface TruncatedText {
837
+ text: string;
838
+ metadata: TruncationMetadata;
839
+ notice?: string;
840
+ }
841
+
842
+ export interface PromptSectionBudget {
843
+ maxBytes: number;
844
+ maxLines: number;
845
+ }
846
+
847
+ export interface CommitPromptBudget {
848
+ profile: "compact" | "default" | "large";
849
+ maxBytes: number;
850
+ maxLines: number;
851
+ sections: Record<
852
+ | "steeringPrompt"
853
+ | "repositorySummary"
854
+ | "changedFiles"
855
+ | "diffStats"
856
+ | "changedFileSnippets"
857
+ | "diffExcerpts"
858
+ | "omittedContext"
859
+ | "projectMetadata",
860
+ PromptSectionBudget
861
+ >;
862
+ }
863
+
864
+ export interface CommitPromptDiagnostics {
865
+ budgetProfile: CommitPromptBudget["profile"];
866
+ maxBytes: number;
867
+ maxLines: number;
868
+ systemPromptBytes: number;
869
+ userPromptBytes: number;
870
+ textBytes: number;
871
+ truncationCount: number;
872
+ }
873
+
874
+ export interface CommitPromptPayload {
875
+ systemPrompt: string;
876
+ userPrompt: string;
877
+ summaryPrompt: string;
878
+ text: string;
879
+ truncation: TruncationMetadata[];
880
+ diagnostics: CommitPromptDiagnostics;
881
+ }
882
+
883
+ export interface DraftUsageDiagnostics {
884
+ input?: number;
885
+ output?: number;
886
+ cacheRead?: number;
887
+ cacheWrite?: number;
888
+ totalTokens?: number;
889
+ }
890
+
891
+ export interface DraftResponseDiagnostics {
892
+ stopReason?: string;
893
+ contentTypeCounts: Record<string, number>;
894
+ contentTypes: string[];
895
+ textCharacterCount: number;
896
+ usableTextCharacterCount: number;
897
+ empty: boolean;
898
+ thinkingOnly: boolean;
899
+ lengthStopped: boolean;
900
+ usage?: DraftUsageDiagnostics;
901
+ }
902
+
903
+ export interface DraftAttemptDiagnostics {
904
+ attempt: number;
905
+ purpose: "draft" | "retry" | "repair";
906
+ maxTokens: number;
907
+ response?: DraftResponseDiagnostics;
908
+ validationError?: string;
909
+ }
910
+
911
+ export interface ChangedFile {
912
+ path: string;
913
+ status: string;
914
+ scope: GitChangeScope;
915
+ sensitive: boolean;
916
+ generated: boolean;
917
+ binary: boolean;
918
+ relatedPaths?: string[];
919
+ secretContent?: boolean;
920
+ unreadable?: boolean;
921
+ }
922
+
923
+ export interface GitDiffSummary {
924
+ scope: GitChangeScope;
925
+ stat: string;
926
+ excerpt: string;
927
+ truncation: TruncationMetadata;
928
+ }
929
+
930
+ export interface ProjectContextEntry {
931
+ path: string;
932
+ kind: ProjectContextEntryKind;
933
+ content: string;
934
+ truncation: TruncationMetadata;
935
+ }
936
+
937
+ export interface SkippedProjectContextEntry {
938
+ path: string;
939
+ reason: SkippedContextReason;
940
+ }
941
+
942
+ export interface ProjectContext {
943
+ root: string;
944
+ metadata: ProjectContextEntry[];
945
+ changedFileSnippets: ProjectContextEntry[];
946
+ skipped: SkippedProjectContextEntry[];
947
+ }
948
+
949
+ export interface GitContext {
950
+ repositoryRoot: string;
951
+ branch: string;
952
+ isDetachedHead: boolean;
953
+ statusPorcelain: string;
954
+ staged: GitDiffSummary;
955
+ unstaged: GitDiffSummary;
956
+ changedFiles: ChangedFile[];
957
+ project: ProjectContext;
958
+ hasChanges: boolean;
959
+ warnings: string[];
960
+ }
961
+
962
+ export interface CommitMessageValidationOk {
963
+ ok: true;
964
+ subject: string;
965
+ body: string;
966
+ message: string;
967
+ }
968
+
969
+ export type CommitMessageValidationResult =
970
+ | CommitMessageValidationOk
971
+ | { ok: false; error: string };
972
+
973
+ export interface CommitResult {
974
+ commitHash: string;
975
+ subject: string;
976
+ body: string;
977
+ stdout: string;
978
+ stderr: string;
979
+ }
980
+
981
+ export interface UnsafeCommitFileApprovalRequest {
982
+ files: ChangedFile[];
983
+ context: GitContext;
984
+ }
985
+
986
+ export type UnsafeCommitFileApproval = (request: UnsafeCommitFileApprovalRequest) => Promise<boolean>;
987
+
988
+ export interface CommitChangesDetails {
989
+ action: "gather" | "commit" | "help";
990
+ steeringPrompt?: string;
991
+ repositoryRoot?: string;
992
+ branch?: string;
993
+ statusPorcelain?: string;
994
+ hasChanges?: boolean;
995
+ changedFiles?: ChangedFile[];
996
+ truncation?: TruncationMetadata[];
997
+ prompt?: CommitPromptDiagnostics;
998
+ draft?: DraftAttemptDiagnostics[];
999
+ warnings?: string[];
1000
+ committed?: CommitResult;
1001
+ }