@hifullmoon/aicommit 1.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/.aicommit.config.example.json +118 -0
- package/CHANGELOG.md +131 -0
- package/LICENSE +21 -0
- package/README.md +406 -0
- package/README.zh-CN.md +408 -0
- package/SECURITY.md +33 -0
- package/bin/aicommit.js +47 -0
- package/docs/distribution.md +104 -0
- package/docs/examples/aicommit-policy.yml +24 -0
- package/docs/examples/commit-msg +4 -0
- package/docs/examples/extension/aicommit-extension.json +9 -0
- package/docs/examples/extension/index.mjs +32 -0
- package/docs/extensions.md +93 -0
- package/docs/privacy.md +58 -0
- package/docs/provider-compatibility.md +37 -0
- package/docs/provider-presets.md +100 -0
- package/docs/team-policy.md +86 -0
- package/docs/troubleshooting.md +37 -0
- package/package.json +100 -0
- package/presets/provider-presets.json +60 -0
- package/schemas/aicommit-extension.schema.json +27 -0
- package/schemas/aicommit-output.schema.json +86 -0
- package/schemas/aicommit-provider-presets.schema.json +56 -0
- package/schemas/aicommit-split-checkpoint.schema.json +92 -0
- package/schemas/aicommit-split-plan.schema.json +108 -0
- package/schemas/aicommit-team-policy.schema.json +59 -0
- package/src/api.js +740 -0
- package/src/cli.js +558 -0
- package/src/completion.js +136 -0
- package/src/config-command.js +59 -0
- package/src/config.js +524 -0
- package/src/context.js +537 -0
- package/src/credentials.js +123 -0
- package/src/doctor.js +131 -0
- package/src/errors.js +96 -0
- package/src/extension-runner.mjs +36 -0
- package/src/extensions.js +426 -0
- package/src/generation-ui.js +53 -0
- package/src/git.js +458 -0
- package/src/main.js +768 -0
- package/src/metrics.js +375 -0
- package/src/output.js +87 -0
- package/src/policy-command.js +172 -0
- package/src/policy.js +421 -0
- package/src/preset-command.js +91 -0
- package/src/provider-presets.js +361 -0
- package/src/providers.js +306 -0
- package/src/setup.js +268 -0
- package/src/split-checkpoint.js +252 -0
- package/src/split-hunks.js +263 -0
- package/src/split-plan.js +339 -0
- package/src/split.js +2285 -0
- package/src/team-policy.js +95 -0
- package/src/trust.js +31 -0
- package/src/ui.js +488 -0
- package/src/utils.js +165 -0
- package/templates/.aicommit.policy.json +22 -0
package/src/utils.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export async function fileExists(p) {
|
|
4
|
+
try {
|
|
5
|
+
await access(p);
|
|
6
|
+
return true;
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Treat model output, provider errors, Git paths, and config values as
|
|
13
|
+
// untrusted terminal text. OSC/CSI and other escape/control sequences can
|
|
14
|
+
// clear the screen, forge prompts, or interact with terminal features such as
|
|
15
|
+
// clipboard integration. Preserve only ordinary text, tabs, and newlines.
|
|
16
|
+
export function sanitizeTerminalText(value) {
|
|
17
|
+
return String(value ?? '')
|
|
18
|
+
.replace(/\r\n?/g, '\n')
|
|
19
|
+
.replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
|
|
20
|
+
.replace(/\x1B[P^_][\s\S]*?\x1B\\/g, '')
|
|
21
|
+
.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
|
|
22
|
+
.replace(/\x1B./g, '')
|
|
23
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Plain-object keys merge recursively; scalars are overwritten; arrays are
|
|
27
|
+
// concatenated and deduped rather than replaced, so a project-level
|
|
28
|
+
// "stripFiles" extends the user-level list instead of wiping it.
|
|
29
|
+
export function deepMerge(a, b) {
|
|
30
|
+
const result = { ...a };
|
|
31
|
+
for (const key of Object.keys(b)) {
|
|
32
|
+
if (key === '__proto__' || key === 'prototype' || key === 'constructor') continue;
|
|
33
|
+
if (Array.isArray(a[key]) && Array.isArray(b[key])) {
|
|
34
|
+
result[key] = [...new Set([...a[key], ...b[key]])];
|
|
35
|
+
} else if (
|
|
36
|
+
b[key] &&
|
|
37
|
+
typeof b[key] === 'object' &&
|
|
38
|
+
!Array.isArray(b[key]) &&
|
|
39
|
+
a[key] &&
|
|
40
|
+
typeof a[key] === 'object' &&
|
|
41
|
+
!Array.isArray(a[key])
|
|
42
|
+
) {
|
|
43
|
+
result[key] = deepMerge(a[key], b[key]);
|
|
44
|
+
} else {
|
|
45
|
+
result[key] = b[key];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Strip markdown code fences the model sometimes wraps around the message
|
|
52
|
+
// (e.g. "```\nfix: ...\n```" or "```text\n...\n```"), plus stray backticks.
|
|
53
|
+
// The fence may share a line with the message ("``` feat: ..."), so strip a
|
|
54
|
+
// leading/trailing fence prefix/suffix instead of only dropping whole lines.
|
|
55
|
+
// Also strips inline <think>...</think> blocks — MiniMax embeds thinking
|
|
56
|
+
// this way in `content` when thinking is enabled (M2.x can't disable it).
|
|
57
|
+
export function cleanCommitMessage(msg) {
|
|
58
|
+
const cleaned = sanitizeTerminalText(msg).replace(/<think>[\s\S]*?<\/think>/g, '');
|
|
59
|
+
return cleaned
|
|
60
|
+
.trim()
|
|
61
|
+
.replace(/^\s*```[a-zA-Z]*(?:\s+|$)/, '') // opening fence, even with content on the same line
|
|
62
|
+
.replace(/\s*```\s*$/, '') // closing fence, even with content on the same line
|
|
63
|
+
.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const CONVENTIONAL_COMMIT_RE =
|
|
67
|
+
/^(?:feat|fix|chore|docs|refactor|test|style|perf|ci|build)(?:\([\w./-]+\))?!?: \S/i;
|
|
68
|
+
|
|
69
|
+
export function isValidCommitMessage(message, maxSubjectLength = 100) {
|
|
70
|
+
const cleaned = cleanCommitMessage(message);
|
|
71
|
+
const subject = cleaned.split('\n', 1)[0];
|
|
72
|
+
return (
|
|
73
|
+
Boolean(cleaned) && subject.length <= maxSubjectLength && CONVENTIONAL_COMMIT_RE.test(subject)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function formatMs(ms) {
|
|
78
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Round a raw token count to the precision it's displayed at, expressed in k
|
|
82
|
+
// (M-scale counts stay in k here — 1.2M = 1200k — so the parts can be summed).
|
|
83
|
+
function roundedK(n) {
|
|
84
|
+
const k = n / 1_000;
|
|
85
|
+
return +k.toFixed(k < 0.1 ? 2 : 1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Render a k value with units — "0.9k", "6.8k", or "1.2M" once >= 1000k.
|
|
89
|
+
// The leading `+` strips a trailing ".0" so 7k renders as "7k", not "7.0k".
|
|
90
|
+
function renderK(k) {
|
|
91
|
+
if (k >= 1_000) return `${+(k / 1_000).toFixed(1)}M`;
|
|
92
|
+
return `${+k.toFixed(k < 0.1 ? 2 : 1)}k`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Abbreviate a token count with k/M units — k is the smallest unit shown,
|
|
96
|
+
// so even sub-thousand counts render as 0.xk. Sub-0.1k counts keep a second
|
|
97
|
+
// decimal so tiny values (e.g. the ping check) don't collapse to "0k".
|
|
98
|
+
function formatTokens(n) {
|
|
99
|
+
return renderK(roundedK(n));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// "4k in + 0.5k out (total 4.5k)" — labeled prompt/input and completion/output
|
|
103
|
+
// split, with the total appended. The total is the sum of the *displayed*
|
|
104
|
+
// parts, not the raw total rounded on its own, so in + out always equals it
|
|
105
|
+
// (1.6k + 0.3k = 1.9k, never a mismatch like "1.6k+0.3k (total 2k)").
|
|
106
|
+
// Accepts the internal camelCase contract plus legacy provider field names.
|
|
107
|
+
export function formatUsage(usage) {
|
|
108
|
+
const prompt = usage.inputTokens ?? usage.prompt_tokens ?? usage.input_tokens;
|
|
109
|
+
const completion = usage.outputTokens ?? usage.completion_tokens ?? usage.output_tokens;
|
|
110
|
+
if (typeof prompt === 'number' && typeof completion === 'number') {
|
|
111
|
+
const p = roundedK(prompt);
|
|
112
|
+
const c = roundedK(completion);
|
|
113
|
+
return `${renderK(p)} in + ${renderK(c)} out (total ${renderK(p + c)})`;
|
|
114
|
+
}
|
|
115
|
+
const total = usage.totalTokens ?? usage.total_tokens;
|
|
116
|
+
if (typeof total === 'number') return `${formatTokens(total)}`;
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Indent every line of an error message by two spaces, so multi-line API
|
|
121
|
+
// errors align under the surrounding CLI indentation.
|
|
122
|
+
export function indentError(err) {
|
|
123
|
+
return sanitizeTerminalText(err?.message || err)
|
|
124
|
+
.split('\n')
|
|
125
|
+
.join('\n ');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function maskApiKey(key) {
|
|
129
|
+
if (!key) return '(not set)';
|
|
130
|
+
if (key.length <= 8) return '****';
|
|
131
|
+
const safe = sanitizeTerminalText(key);
|
|
132
|
+
return `${safe.slice(0, 4)}…${safe.slice(-4)} (${key.length} chars)`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const SENSITIVE_CONFIG_KEY_RE =
|
|
136
|
+
/^(?:api[_-]?key|access[_-]?token|authorization|client[_-]?secret|password|passwd|secret|token)$/i;
|
|
137
|
+
const SENSITIVE_URL_PARAMETER_RE =
|
|
138
|
+
/^(?:api[-_]?key|key|x[-_]?api[-_]?key|access[-_]?token|refresh[-_]?token|bearer[-_]?token|token|authorization|auth|password|passwd|secret(?:[-_]?key)?|client[-_]?secret|credential|signature|sig|code|subscription[-_]?key|x-amz-(?:credential|signature|security-token)|x-goog-signature)$/i;
|
|
139
|
+
|
|
140
|
+
// Provider endpoints occasionally need non-secret query parameters such as
|
|
141
|
+
// `api-version`, but credentials embedded in userinfo or well-known secret
|
|
142
|
+
// parameters must never be echoed to terminals, JSON inspection output, or
|
|
143
|
+
// extension processes. Fragments are omitted because HTTP never sends them.
|
|
144
|
+
export function redactSensitiveUrl(value) {
|
|
145
|
+
try {
|
|
146
|
+
const url = new URL(String(value));
|
|
147
|
+
if (url.username) url.username = 'redacted';
|
|
148
|
+
if (url.password) url.password = 'redacted';
|
|
149
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
150
|
+
if (SENSITIVE_URL_PARAMETER_RE.test(key)) url.searchParams.set(key, 'redacted');
|
|
151
|
+
}
|
|
152
|
+
url.hash = '';
|
|
153
|
+
return url.toString();
|
|
154
|
+
} catch {
|
|
155
|
+
return sanitizeTerminalText(value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function stringifyConfigRedacted(value) {
|
|
160
|
+
return JSON.stringify(value, (key, item) => {
|
|
161
|
+
if (/^apiUrl$/i.test(key) && typeof item === 'string') return redactSensitiveUrl(item);
|
|
162
|
+
if (!SENSITIVE_CONFIG_KEY_RE.test(key)) return item;
|
|
163
|
+
return typeof item === 'string' ? maskApiKey(item) : '[REDACTED]';
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "aicommit-team-policy",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"language": "en",
|
|
5
|
+
"commitPolicy": {
|
|
6
|
+
"version": 1,
|
|
7
|
+
"types": ["feat", "fix", "docs", "refactor", "test", "chore"],
|
|
8
|
+
"scope": {
|
|
9
|
+
"mode": "optional",
|
|
10
|
+
"values": []
|
|
11
|
+
},
|
|
12
|
+
"subject": {
|
|
13
|
+
"maxLength": 72
|
|
14
|
+
},
|
|
15
|
+
"body": {
|
|
16
|
+
"mode": "optional",
|
|
17
|
+
"maxLines": 8
|
|
18
|
+
},
|
|
19
|
+
"breakingChange": "allow",
|
|
20
|
+
"language": "inherit"
|
|
21
|
+
}
|
|
22
|
+
}
|