maiass 5.15.3 → 5.15.8
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/lib/account-info.js +21 -5
- package/lib/changelog-cleanup.js +7 -4
- package/lib/client-info.js +53 -0
- package/lib/commit.js +122 -13
- package/package.json +1 -1
- package/templates/ci/github-version-bump.yml +6 -1
package/lib/account-info.js
CHANGED
|
@@ -6,7 +6,7 @@ import { SYMBOLS } from './symbols.js';
|
|
|
6
6
|
import { loadEnvironmentConfig } from './config.js';
|
|
7
7
|
import { generateMachineFingerprint } from './machine-fingerprint.js';
|
|
8
8
|
import { retrieveSecureVariable, storeSecureVariable, removeSecureVariable } from './secure-storage.js';
|
|
9
|
-
import { getClientName, getClientVersion } from './client-info.js';
|
|
9
|
+
import { getClientName, getClientVersion, isAIModeOff, isCI, ciHeaders } from './client-info.js';
|
|
10
10
|
import { getSingleCharInput } from './input-utils.js';
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
@@ -40,8 +40,24 @@ function maskToken(token) {
|
|
|
40
40
|
*/
|
|
41
41
|
async function createAnonymousSubscriptionIfNeeded() {
|
|
42
42
|
const debugMode = process.env.MAIASS_DEBUG === 'true';
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
try {
|
|
45
|
+
// MAI-98 defense-in-depth: don't mint a throwaway anonymous sub when AI is
|
|
46
|
+
// off, or on an ephemeral CI runner with no existing token (fresh
|
|
47
|
+
// fingerprint per run drains the global free-credit pool).
|
|
48
|
+
if (isAIModeOff()) {
|
|
49
|
+
if (debugMode) {
|
|
50
|
+
log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] AI mode is off (MAIASS_AI_MODE=${process.env.MAIASS_AI_MODE}); skipping anonymous subscription`);
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (isCI()) {
|
|
55
|
+
if (debugMode) {
|
|
56
|
+
log.debug(SYMBOLS.INFO, '[MAIASS DEBUG] CI environment detected with no existing token; skipping anonymous subscription (MAI-98)');
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
log.info(SYMBOLS.INFO, 'Generating machine fingerprint...');
|
|
46
62
|
const machineFingerprint = generateMachineFingerprint();
|
|
47
63
|
|
|
@@ -58,7 +74,8 @@ async function createAnonymousSubscriptionIfNeeded() {
|
|
|
58
74
|
headers: {
|
|
59
75
|
'Content-Type': 'application/json',
|
|
60
76
|
'X-Client-Name': getClientName(),
|
|
61
|
-
'X-Client-Version': getClientVersion()
|
|
77
|
+
'X-Client-Version': getClientVersion(),
|
|
78
|
+
...ciHeaders()
|
|
62
79
|
},
|
|
63
80
|
body: JSON.stringify({
|
|
64
81
|
machine_fingerprint: machineFingerprint
|
|
@@ -334,8 +351,7 @@ export async function handleAccountInfoCommand(options = {}) {
|
|
|
334
351
|
|
|
335
352
|
// If still no API key and AI mode is not off, create anonymous subscription
|
|
336
353
|
if (!apiKey || apiKey === 'DISABLED') {
|
|
337
|
-
|
|
338
|
-
if (aiMode !== 'off') {
|
|
354
|
+
if (!isAIModeOff()) {
|
|
339
355
|
if (debugMode) {
|
|
340
356
|
log.debug(SYMBOLS.INFO, '[MAIASS DEBUG] No AI token found, creating anonymous subscription...');
|
|
341
357
|
}
|
package/lib/changelog-cleanup.js
CHANGED
|
@@ -29,7 +29,7 @@ import { SYMBOLS } from './symbols.js';
|
|
|
29
29
|
import colors from './colors.js';
|
|
30
30
|
import { createAnonymousSubscriptionIfNeeded } from './commit.js';
|
|
31
31
|
import { generateMachineFingerprint } from './machine-fingerprint.js';
|
|
32
|
-
import { getClientName, getClientVersion } from './client-info.js';
|
|
32
|
+
import { getClientName, getClientVersion, isAIModeOff, ciHeaders } from './client-info.js';
|
|
33
33
|
import { getSingleCharInput } from './input-utils.js';
|
|
34
34
|
|
|
35
35
|
export const FLAGS = ['--cleanup-changelogs'];
|
|
@@ -48,8 +48,10 @@ const MAX_OUTPUT_TOKENS = 2000;
|
|
|
48
48
|
* anything other than 'off' as on; we mirror that here.
|
|
49
49
|
*/
|
|
50
50
|
function isAIModeActive() {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// MAI-98: delegate to the shared parser so "off"-ish spellings (off/false/no/
|
|
52
|
+
// 0/disabled, plus the YAML-boolean "false" produced by an unquoted GH Actions
|
|
53
|
+
// `MAIASS_AI_MODE: off`) are all treated as off consistently across the CLI.
|
|
54
|
+
return !isAIModeOff();
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
function executeGitCommand(command) {
|
|
@@ -548,7 +550,8 @@ async function callCleanupAI(systemContent, userContent) {
|
|
|
548
550
|
'X-Machine-Fingerprint': generateMachineFingerprint(),
|
|
549
551
|
'X-Client-Name': getClientName(),
|
|
550
552
|
'X-Client-Version': getClientVersion(),
|
|
551
|
-
'X-Subscription-ID': process.env.MAIASS_SUBSCRIPTION_ID || ''
|
|
553
|
+
'X-Subscription-ID': process.env.MAIASS_SUBSCRIPTION_ID || '',
|
|
554
|
+
...ciHeaders()
|
|
552
555
|
},
|
|
553
556
|
body: JSON.stringify(requestBody),
|
|
554
557
|
signal: controller.signal
|
package/lib/client-info.js
CHANGED
|
@@ -58,3 +58,56 @@ export function getClientInfo() {
|
|
|
58
58
|
version: getClientVersion()
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Robustly decide whether AI is effectively OFF based on MAIASS_AI_MODE.
|
|
64
|
+
*
|
|
65
|
+
* MAI-98: the GitHub Actions version-bump workflow set `MAIASS_AI_MODE: off`
|
|
66
|
+
* (an unquoted YAML boolean) which GitHub stringifies to "false". We also need
|
|
67
|
+
* to treat the various "off"-ish spellings consistently everywhere so no AI
|
|
68
|
+
* path ever fires when the operator meant to disable it. A null/undefined/empty
|
|
69
|
+
* value is NOT off (default behaviour is the interactive 'ask' mode).
|
|
70
|
+
*
|
|
71
|
+
* @param {string|undefined} [mode] - raw MAIASS_AI_MODE value (defaults to env)
|
|
72
|
+
* @returns {boolean} true when AI should be considered disabled
|
|
73
|
+
*/
|
|
74
|
+
export function isAIModeOff(mode = process.env.MAIASS_AI_MODE) {
|
|
75
|
+
if (mode === undefined || mode === null) return false;
|
|
76
|
+
const normalised = String(mode).trim().toLowerCase();
|
|
77
|
+
if (normalised === '') return false;
|
|
78
|
+
return ['off', 'false', 'no', '0', 'disabled'].includes(normalised);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Detect whether we are running inside a CI / automation environment.
|
|
83
|
+
*
|
|
84
|
+
* MAI-98: ephemeral CI runners produce a fresh machine fingerprint on every
|
|
85
|
+
* run, so any anonymous-subscription mint there creates a brand new sub +
|
|
86
|
+
* free-credit grant that is never reused — draining the global free pool.
|
|
87
|
+
* When in CI with no pre-existing token we must never mint an anonymous sub.
|
|
88
|
+
*
|
|
89
|
+
* @returns {boolean} true when a known CI environment is detected
|
|
90
|
+
*/
|
|
91
|
+
export function isCI() {
|
|
92
|
+
return (
|
|
93
|
+
process.env.CI === 'true' ||
|
|
94
|
+
!!process.env.GITHUB_ACTIONS ||
|
|
95
|
+
!!process.env.GITLAB_CI ||
|
|
96
|
+
!!process.env.CIRCLECI ||
|
|
97
|
+
!!process.env.BUILDKITE ||
|
|
98
|
+
!!process.env.TF_BUILD
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Headers to attach to MAIASS proxy / token requests so the server can
|
|
104
|
+
* independently detect CI and apply its own anti-abuse handling.
|
|
105
|
+
*
|
|
106
|
+
* MAI-98 SHARED CONTRACT with worker-api-specialist: the header name is
|
|
107
|
+
* `X-MAIASS-CI` and is only sent when {@link isCI} is true.
|
|
108
|
+
*
|
|
109
|
+
* @returns {Object} header object ({} when not in CI)
|
|
110
|
+
*/
|
|
111
|
+
export function ciHeaders() {
|
|
112
|
+
return isCI() ? { 'X-MAIASS-CI': 'true' } : {};
|
|
113
|
+
}
|
package/lib/commit.js
CHANGED
|
@@ -10,7 +10,7 @@ import readline from 'readline';
|
|
|
10
10
|
import { loadEnvironmentConfig } from './config.js';
|
|
11
11
|
import { generateMachineFingerprint } from './machine-fingerprint.js';
|
|
12
12
|
import { storeSecureVariable, retrieveSecureVariable } from './secure-storage.js';
|
|
13
|
-
import { getClientName, getClientVersion } from './client-info.js';
|
|
13
|
+
import { getClientName, getClientVersion, isAIModeOff, isCI, ciHeaders } from './client-info.js';
|
|
14
14
|
import { getSingleCharInput, getMultiLineInput } from './input-utils.js';
|
|
15
15
|
import { logCommit } from './devlog.js';
|
|
16
16
|
import colors from './colors.js';
|
|
@@ -206,6 +206,25 @@ async function createAnonymousSubscriptionIfNeeded() {
|
|
|
206
206
|
return existingToken;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// MAI-98 defense-in-depth: never mint an anonymous subscription when AI is
|
|
210
|
+
// effectively off, or when running on an ephemeral CI runner with no
|
|
211
|
+
// pre-existing token. Both conditions otherwise create a fresh anonymous
|
|
212
|
+
// sub + free-credit grant on every run (CI fingerprints are non-reusable),
|
|
213
|
+
// draining the global free-credit pool. The caller MUST treat a null return
|
|
214
|
+
// as "no AI" and proceed without it — the version bump must never block.
|
|
215
|
+
if (isAIModeOff()) {
|
|
216
|
+
if (debugMode) {
|
|
217
|
+
log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] AI mode is off (MAIASS_AI_MODE=${process.env.MAIASS_AI_MODE}); skipping anonymous subscription`);
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
if (isCI()) {
|
|
222
|
+
if (debugMode) {
|
|
223
|
+
log.debug(SYMBOLS.INFO, '[MAIASS DEBUG] CI environment detected with no existing token; skipping anonymous subscription to avoid pool drain (MAI-98)');
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
209
228
|
log.info(SYMBOLS.INFO, 'Generating machine fingerprint...');
|
|
210
229
|
const machineFingerprint = generateMachineFingerprint();
|
|
211
230
|
|
|
@@ -223,7 +242,8 @@ async function createAnonymousSubscriptionIfNeeded() {
|
|
|
223
242
|
headers: {
|
|
224
243
|
'Content-Type': 'application/json',
|
|
225
244
|
'X-Client-Name': getClientName(),
|
|
226
|
-
'X-Client-Version': getClientVersion()
|
|
245
|
+
'X-Client-Version': getClientVersion(),
|
|
246
|
+
...ciHeaders()
|
|
227
247
|
},
|
|
228
248
|
body: JSON.stringify({
|
|
229
249
|
machine_fingerprint: machineFingerprint
|
|
@@ -309,6 +329,77 @@ async function createAnonymousSubscriptionIfNeeded() {
|
|
|
309
329
|
}
|
|
310
330
|
}
|
|
311
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Normalize a bullet-style AI commit suggestion into a deterministic on-disk
|
|
334
|
+
* format. Pure function — no side effects. The normalizer is deliberately
|
|
335
|
+
* conservative and content-preserving: it reformats whitespace/markers but
|
|
336
|
+
* never fabricates a subject and never drops body content.
|
|
337
|
+
*
|
|
338
|
+
* Contract:
|
|
339
|
+
* - Input is coerced via String(text); non-string input cannot throw.
|
|
340
|
+
* - CRLF (`\r\n`) and CR line endings are tolerated (trailing \r stripped).
|
|
341
|
+
* - Early return (input returned trimmed, unchanged) when the input has zero
|
|
342
|
+
* or one non-empty line — a bare title with no bullets, or empty input.
|
|
343
|
+
* - No-title passthrough: if the first non-empty line already starts with a
|
|
344
|
+
* bullet marker (`-` or `*`), the model produced bullets with no summary
|
|
345
|
+
* line. A deterministic normalizer must not invent a subject, so the input
|
|
346
|
+
* is returned trimmed and otherwise unchanged.
|
|
347
|
+
* - Otherwise the first non-empty line becomes the title (trailing whitespace
|
|
348
|
+
* stripped; no marker stripping — it is not a marker line by definition).
|
|
349
|
+
* - Each subsequent non-empty line that starts with a bullet marker becomes
|
|
350
|
+
* " - <content>" (exactly two-space indent, single dash; the original
|
|
351
|
+
* marker and leading indentation are stripped).
|
|
352
|
+
* - Each subsequent non-empty line that does NOT start with a marker is a
|
|
353
|
+
* soft-wrapped continuation of the previous line: it is appended to the
|
|
354
|
+
* previous bullet (joined with a single space). If no bullet exists yet,
|
|
355
|
+
* it is appended to the title line instead.
|
|
356
|
+
* - Blank lines between the title and bullets (and trailing blanks) are
|
|
357
|
+
* dropped.
|
|
358
|
+
* @param {string} text - raw AI suggestion (already trimmed/quote-stripped)
|
|
359
|
+
* @returns {string} normalized bullet-style message
|
|
360
|
+
*/
|
|
361
|
+
function normalizeBulletMessage(text) {
|
|
362
|
+
const source = String(text);
|
|
363
|
+
// Normalize line endings so \r\n / \r inputs behave identically to \n.
|
|
364
|
+
const lines = source.split(/\r\n|\r|\n/);
|
|
365
|
+
|
|
366
|
+
// Guard: zero or one non-empty line (empty input or a bare title) is left
|
|
367
|
+
// untouched apart from outer trimming.
|
|
368
|
+
if (lines.filter(l => l.trim() !== '').length <= 1) {
|
|
369
|
+
return source.trim();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// No-title passthrough: the model emitted bullets with no summary line. Do
|
|
373
|
+
// not promote a bullet to a title — return the input trimmed, unchanged.
|
|
374
|
+
const firstNonEmpty = lines.find(l => l.trim() !== '');
|
|
375
|
+
if (/^\s*[-*]\s+/.test(firstNonEmpty)) {
|
|
376
|
+
return source.trim();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const isMarker = (line) => /^\s*[-*]\s+/.test(line);
|
|
380
|
+
const stripMarker = (line) => line.replace(/^\s*[-*]\s+/, '').trim();
|
|
381
|
+
|
|
382
|
+
const out = [];
|
|
383
|
+
let titleSet = false;
|
|
384
|
+
for (const raw of lines) {
|
|
385
|
+
const line = raw.replace(/\s+$/, '');
|
|
386
|
+
if (line.trim() === '') continue;
|
|
387
|
+
if (!titleSet) {
|
|
388
|
+
// First non-empty line is the title (guaranteed non-marker above).
|
|
389
|
+
out.push(line.trim());
|
|
390
|
+
titleSet = true;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (isMarker(line)) {
|
|
394
|
+
out.push(' - ' + stripMarker(line));
|
|
395
|
+
} else {
|
|
396
|
+
// Soft-wrapped continuation: append to the previous line.
|
|
397
|
+
out[out.length - 1] = out[out.length - 1] + ' ' + line.trim();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return out.join('\n');
|
|
401
|
+
}
|
|
402
|
+
|
|
312
403
|
/**
|
|
313
404
|
* Get AI commit message suggestion
|
|
314
405
|
* @param {Object} gitInfo - Git information object
|
|
@@ -446,20 +537,31 @@ async function getAICommitSuggestion(gitInfo) {
|
|
|
446
537
|
}
|
|
447
538
|
|
|
448
539
|
// Create AI prompt based on commit message style
|
|
540
|
+
//
|
|
541
|
+
// ⚠️ MAI-108 PROMPT CHANGE / ROLLBACK POINT: the bullet prompt below was
|
|
542
|
+
// rewritten (placeholders + anti-leak/anti-invent guard) because small/local
|
|
543
|
+
// models echoed the old few-shot example bullets verbatim. The PREVIOUS,
|
|
544
|
+
// long-standing prompt is production-proven against the 1min.ai chat path.
|
|
545
|
+
// If hosted-model commit-message QUALITY regresses, revert this block to its
|
|
546
|
+
// pre-change form: see git history of this file before commit dca4519 (PR #28).
|
|
449
547
|
let prompt;
|
|
450
548
|
if (commitMessageStyle === 'bullet') {
|
|
451
|
-
prompt = `
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
-
|
|
455
|
-
-
|
|
456
|
-
|
|
457
|
-
|
|
549
|
+
prompt = `Write a git commit message for the diff below.
|
|
550
|
+
Format exactly like this template:
|
|
551
|
+
<one-line summary>
|
|
552
|
+
- <type>: <description>
|
|
553
|
+
- <type>: <description>
|
|
554
|
+
Rules:
|
|
555
|
+
- First line: a brief past-tense summary with no leading dash.
|
|
556
|
+
- Then one or more bullet lines, each indented by exactly two spaces, starting with "- ", then a conventional-commit type (feat, fix, docs, refactor, chore) and a colon.
|
|
557
|
+
- No blank line between the summary line and the bullets.
|
|
558
|
+
- Describe ONLY the actual changes in the diff. Do not output the literal words type or description or any angle brackets, and do not invent changes that are not in the diff.
|
|
559
|
+
- Do not wrap the response in quotes.
|
|
458
560
|
|
|
459
561
|
Git diff:
|
|
460
562
|
${gitDiff}`;
|
|
461
563
|
} else {
|
|
462
|
-
prompt = `Analyze the following git diff and create a concise, descriptive commit message. Use conventional commit format when appropriate (feat:, fix:, docs:, etc.). Keep it under 72 characters for the first line.
|
|
564
|
+
prompt = `Analyze the following git diff and create a concise, descriptive commit message. Use conventional commit format when appropriate (feat:, fix:, docs:, etc.). Keep it under 72 characters for the first line. Describe only the actual changes in the diff; do not invent changes.
|
|
463
565
|
|
|
464
566
|
Git diff:
|
|
465
567
|
${gitDiff}`;
|
|
@@ -513,7 +615,8 @@ ${gitDiff}`;
|
|
|
513
615
|
'X-Machine-Fingerprint': generateMachineFingerprint(),
|
|
514
616
|
'X-Client-Name': getClientName(),
|
|
515
617
|
'X-Client-Version': getClientVersion(),
|
|
516
|
-
'X-Subscription-ID': process.env.MAIASS_SUBSCRIPTION_ID || ''
|
|
618
|
+
'X-Subscription-ID': process.env.MAIASS_SUBSCRIPTION_ID || '',
|
|
619
|
+
...ciHeaders()
|
|
517
620
|
},
|
|
518
621
|
body: JSON.stringify(requestBody),
|
|
519
622
|
signal: controller.signal
|
|
@@ -556,11 +659,16 @@ ${gitDiff}`;
|
|
|
556
659
|
}
|
|
557
660
|
|
|
558
661
|
// Clean up any quotes that might wrap the entire response
|
|
559
|
-
if ((suggestion.startsWith("'") && suggestion.endsWith("'")) ||
|
|
662
|
+
if ((suggestion.startsWith("'") && suggestion.endsWith("'")) ||
|
|
560
663
|
(suggestion.startsWith('"') && suggestion.endsWith('"'))) {
|
|
561
664
|
suggestion = suggestion.slice(1, -1).trim();
|
|
562
665
|
}
|
|
563
|
-
|
|
666
|
+
|
|
667
|
+
// Deterministically normalize bullet format so it's model-independent.
|
|
668
|
+
if (commitMessageStyle === 'bullet') {
|
|
669
|
+
suggestion = normalizeBulletMessage(suggestion);
|
|
670
|
+
}
|
|
671
|
+
|
|
564
672
|
// Extract credit information from billing data
|
|
565
673
|
let creditsUsed, creditsRemaining;
|
|
566
674
|
if (data.billing) {
|
|
@@ -1136,6 +1244,7 @@ export {
|
|
|
1136
1244
|
getCommitMessage,
|
|
1137
1245
|
handleStagedCommit,
|
|
1138
1246
|
getAICommitSuggestion,
|
|
1247
|
+
normalizeBulletMessage,
|
|
1139
1248
|
executeGitCommand,
|
|
1140
1249
|
remoteExists
|
|
1141
1250
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maiass",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.15.
|
|
4
|
+
"version": "5.15.8",
|
|
5
5
|
"description": "AI commit messages, version bumps, and changelogs from one command. Stages, commits, merges, tags. Anonymous on first run — no email, no card.",
|
|
6
6
|
"main": "maiass.mjs",
|
|
7
7
|
"bin": {
|
|
@@ -72,4 +72,9 @@ jobs:
|
|
|
72
72
|
- name: Bump version
|
|
73
73
|
run: maiass -a patch
|
|
74
74
|
env:
|
|
75
|
-
|
|
75
|
+
# Quoted: unquoted `off` is a YAML boolean that GitHub stringifies to
|
|
76
|
+
# "false", and a repo's tracked .env.maiass (loaded with override) can
|
|
77
|
+
# still flip AI back on — both previously minted a throwaway anonymous
|
|
78
|
+
# subscription on every CI run. The CLI now also self-guards via CI
|
|
79
|
+
# detection (see MAI-98), but keep this explicit and correct.
|
|
80
|
+
MAIASS_AI_MODE: "off" # Disable AI — no credits used in CI
|