@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.
Files changed (57) hide show
  1. package/.aicommit.config.example.json +118 -0
  2. package/CHANGELOG.md +131 -0
  3. package/LICENSE +21 -0
  4. package/README.md +406 -0
  5. package/README.zh-CN.md +408 -0
  6. package/SECURITY.md +33 -0
  7. package/bin/aicommit.js +47 -0
  8. package/docs/distribution.md +104 -0
  9. package/docs/examples/aicommit-policy.yml +24 -0
  10. package/docs/examples/commit-msg +4 -0
  11. package/docs/examples/extension/aicommit-extension.json +9 -0
  12. package/docs/examples/extension/index.mjs +32 -0
  13. package/docs/extensions.md +93 -0
  14. package/docs/privacy.md +58 -0
  15. package/docs/provider-compatibility.md +37 -0
  16. package/docs/provider-presets.md +100 -0
  17. package/docs/team-policy.md +86 -0
  18. package/docs/troubleshooting.md +37 -0
  19. package/package.json +100 -0
  20. package/presets/provider-presets.json +60 -0
  21. package/schemas/aicommit-extension.schema.json +27 -0
  22. package/schemas/aicommit-output.schema.json +86 -0
  23. package/schemas/aicommit-provider-presets.schema.json +56 -0
  24. package/schemas/aicommit-split-checkpoint.schema.json +92 -0
  25. package/schemas/aicommit-split-plan.schema.json +108 -0
  26. package/schemas/aicommit-team-policy.schema.json +59 -0
  27. package/src/api.js +740 -0
  28. package/src/cli.js +558 -0
  29. package/src/completion.js +136 -0
  30. package/src/config-command.js +59 -0
  31. package/src/config.js +524 -0
  32. package/src/context.js +537 -0
  33. package/src/credentials.js +123 -0
  34. package/src/doctor.js +131 -0
  35. package/src/errors.js +96 -0
  36. package/src/extension-runner.mjs +36 -0
  37. package/src/extensions.js +426 -0
  38. package/src/generation-ui.js +53 -0
  39. package/src/git.js +458 -0
  40. package/src/main.js +768 -0
  41. package/src/metrics.js +375 -0
  42. package/src/output.js +87 -0
  43. package/src/policy-command.js +172 -0
  44. package/src/policy.js +421 -0
  45. package/src/preset-command.js +91 -0
  46. package/src/provider-presets.js +361 -0
  47. package/src/providers.js +306 -0
  48. package/src/setup.js +268 -0
  49. package/src/split-checkpoint.js +252 -0
  50. package/src/split-hunks.js +263 -0
  51. package/src/split-plan.js +339 -0
  52. package/src/split.js +2285 -0
  53. package/src/team-policy.js +95 -0
  54. package/src/trust.js +31 -0
  55. package/src/ui.js +488 -0
  56. package/src/utils.js +165 -0
  57. 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
+ }