auxilo-mcp 0.9.10 → 0.9.12
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/bin/auxilo-cli.js +271 -1
- package/lib/extraction-index.js +14 -0
- package/lib/installer.js +62 -2
- package/mcp-server.js +3 -3
- package/package.json +1 -1
- package/scripts/extract-local.js +76 -16
- package/scripts/review-notice.js +73 -3
package/bin/auxilo-cli.js
CHANGED
|
@@ -493,6 +493,10 @@ async function cmdStatus() {
|
|
|
493
493
|
console.log(`Account mode: ${s.accountMode}`);
|
|
494
494
|
console.log(`Kill-switch sentinel: ${s.sentinel ? 'present (extraction enabled)' : 'absent (extraction disabled)'}`);
|
|
495
495
|
console.log(`Runner installed: ${s.runnerInstalled ? 'yes (~/.auxilo/bin)' : 'no'}`);
|
|
496
|
+
if (s.runnerInstalled) {
|
|
497
|
+
const line = runnerSkewLine(installer.runnerVersionSkew(HOME));
|
|
498
|
+
if (line) console.log(line);
|
|
499
|
+
}
|
|
496
500
|
console.log(`SessionEnd hook: ${s.hookInstalled ? 'installed' : 'not installed'}${s.hookRegistered ? ', registered in Claude Code settings' : ''}`);
|
|
497
501
|
for (const c of s.clients.filter((c) => c.captureHook)) {
|
|
498
502
|
console.log(`Capture hooks: ${c.name} (${c.captureEvent}, ${c.captureRegistered ? 'registered' : 'not registered'})`);
|
|
@@ -501,6 +505,17 @@ async function cmdStatus() {
|
|
|
501
505
|
console.log(`Pending upload queue: ${s.pendingCount} file(s)\n`);
|
|
502
506
|
}
|
|
503
507
|
|
|
508
|
+
/**
|
|
509
|
+
* CLEAN-LANE-FLIP Phase B: ONE line when ~/.auxilo/bin/VERSION is missing or
|
|
510
|
+
* differs from this CLI's package version; null when the stack is current.
|
|
511
|
+
* `setup` is idempotent and re-copies the stack, so that is the remedy.
|
|
512
|
+
*/
|
|
513
|
+
function runnerSkewLine(skew) {
|
|
514
|
+
if (!skew || !skew.skew) return null;
|
|
515
|
+
const installed = skew.installed ? `v${skew.installed}` : 'unstamped (pre-0.9.12)';
|
|
516
|
+
return ` ⚠ Installed runner is ${installed} (package v${skew.package}) — run: npx auxilo setup`;
|
|
517
|
+
}
|
|
518
|
+
|
|
504
519
|
// ─── auxilo disable ─────────────────────────────────────────────────────────
|
|
505
520
|
|
|
506
521
|
async function cmdDisable(flags) {
|
|
@@ -860,6 +875,238 @@ async function cmdReview(flags) {
|
|
|
860
875
|
console.log(`\nReview complete: approved ${approved}, kept private ${keptPrivate}, rejected ${rejected}, skipped ${skipped} of ${ordered.length}.`);
|
|
861
876
|
}
|
|
862
877
|
|
|
878
|
+
// ─── auxilo clean-lane (SPEC3-C1 standing consent; CLEAN-LANE-FLIP Phase A) ──
|
|
879
|
+
//
|
|
880
|
+
// GOV-3 (ratified language, Gate-A 2026-09-05): `grant` runs ONLY on a TTY and
|
|
881
|
+
// requires the human to TYPE the affirmation sentence verbatim — no --yes, no
|
|
882
|
+
// flag. The TTY gate + verbatim affirmation prevent ACCIDENTAL enrollment and
|
|
883
|
+
// create a hash-chained record of a DELIBERATE act by the credential holder.
|
|
884
|
+
// That record is EVIDENTIARY, not preventive: it is not a defense against a
|
|
885
|
+
// holder of the account's contribute-scoped key, who can reach the same routes
|
|
886
|
+
// directly. `status` / `revoke` are non-interactive.
|
|
887
|
+
// While the server flag is off the routes answer the catch-all 404 and every
|
|
888
|
+
// subcommand prints "not yet available" (exit 0).
|
|
889
|
+
//
|
|
890
|
+
// The sentence below MIRRORS lib/clean-lane.js CLEAN_LANE_AFFIRMATION. It is
|
|
891
|
+
// a literal here only because lib/clean-lane.js is server-side and not in the
|
|
892
|
+
// published package's files[]; test/clean-lane-phase-a.test.js pins the two
|
|
893
|
+
// byte-equal. The consent VERSION is never a client literal — it always comes
|
|
894
|
+
// from GET /account/clean-lane (consent_version_current).
|
|
895
|
+
const CLEAN_LANE_AFFIRMATION = 'I understand and choose auto-publish for qualifying extracted learnings.';
|
|
896
|
+
const CLEAN_LANE_UNAVAILABLE = 'Auto-publish for clean learnings is not yet available on this account.';
|
|
897
|
+
// CLEAN-LANE-FLIP Phase B (legal; DRAFT pending Tyler): the full text of ToS
|
|
898
|
+
// §5.9.3(g) (plus its ratchet paragraph) prints ABOVE the affirmation prompt —
|
|
899
|
+
// counsel condition: the enrollment surface must show what "qualifying",
|
|
900
|
+
// revocation and the 7-day retraction mean, on both the dashboard and CLI
|
|
901
|
+
// paths. Same package-boundary reason as the affirmation: these literals are
|
|
902
|
+
// pinned byte-equal to docs/TERMS-OF-SERVICE.md and public/dashboard.html by
|
|
903
|
+
// test/clean-lane-phase-b-legal.test.js. Edit the Terms first, then mirror.
|
|
904
|
+
const CLEAN_LANE_TERMS_G = '(g) Standing publication consent (optional). Standing publication consent is off by default. A Builder may turn it on by an affirmative act — a dashboard setting, or a terminal command that requires typing the affirmation sentence shown on that screen. Auxilo records that act, the affirmation, and the consent-text version in a durable, hash-chained consent log, retained for the life of the account plus three (3) years under subsection (b). While it is on, a Learning submitted through Autonomous Extraction is published without separate per-item approval only if it passes every Platform screen and the quality threshold the Builder chose at activation. An account\'s first public Learning is never published this way; it is held for operator review under Section 4.1. Auxilo records each such publication in the Builder\'s dashboard and returns a notice in the response to the submission that produced it; each is retractable for seven (7) days under Section 5.9.4. If more than five percent (5%) of a Builder\'s Learnings published this way in any thirty (30) day period are retracted, Auxilo freezes the feature for that account until the Builder turns it on again. A Builder may turn it off at any time, effective immediately for later submissions; doing so does not affect Learnings already published. Subsection (c) applies in full to every Learning so published.';
|
|
905
|
+
const CLEAN_LANE_TERMS_G2 = 'The quality threshold in effect for a Builder is the one that Builder selected, and Auxilo will not broaden the conditions under which a Learning qualifies for publication under this subsection without recording a new consent; Auxilo may make those conditions stricter at any time.';
|
|
906
|
+
// CLEAN-LANE-FLIP Phase B (notice hardening): the no-email enrollment line —
|
|
907
|
+
// GOV-2 counsel draft §6 read #2 "move 3" — printed verbatim before the
|
|
908
|
+
// affirmation prompt on every enrollment surface. Byte-equal to the dashboard's
|
|
909
|
+
// #clean-lane-no-email-line (test/clean-lane-phase-b-notice.test.js).
|
|
910
|
+
const CLEAN_LANE_NO_EMAIL_LINE = 'You will not receive an email for these. Publications appear in your dashboard and in the response to the session that submitted them. The 7-day retraction window runs from publication.';
|
|
911
|
+
const CLEAN_LANE_MIN_QUALITY_MIN = 14;
|
|
912
|
+
const CLEAN_LANE_MIN_QUALITY_MAX = 20;
|
|
913
|
+
const CLEAN_LANE_MIN_QUALITY_DEFAULT = 16;
|
|
914
|
+
|
|
915
|
+
const CLEAN_LANE_EXPLAINER = `
|
|
916
|
+
Auto-publish clean learnings
|
|
917
|
+
|
|
918
|
+
When this is on, a learning is published without waiting for your review
|
|
919
|
+
only when all three hold: it was extracted by your own model, every server
|
|
920
|
+
screen came back clean, and its quality score is at or above the threshold
|
|
921
|
+
you set.
|
|
922
|
+
|
|
923
|
+
What is never auto-published: your first public learning (it waits for
|
|
924
|
+
operator review), anything a screen flags, anything below your threshold,
|
|
925
|
+
and anything after an auto-freeze. Every auto-published learning can be
|
|
926
|
+
retracted for 7 days (\`npx auxilo review\` or your dashboard).
|
|
927
|
+
`;
|
|
928
|
+
|
|
929
|
+
/** Word-wrap a single paragraph at `width` columns (whitespace only; no word is altered). */
|
|
930
|
+
function wrapForTerminal(paragraph, width = 78) {
|
|
931
|
+
const lines = [];
|
|
932
|
+
let line = '';
|
|
933
|
+
for (const word of paragraph.split(' ')) {
|
|
934
|
+
if (line && (line.length + 1 + word.length) > width) { lines.push(line); line = word; }
|
|
935
|
+
else line = line ? `${line} ${word}` : word;
|
|
936
|
+
}
|
|
937
|
+
if (line) lines.push(line);
|
|
938
|
+
return lines.map((l) => ` ${l}`).join('\n');
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async function cleanLaneRequest({ apiKey, baseUrl, method, route, body }) {
|
|
942
|
+
const url = `${String(baseUrl).replace(/\/+$/, '')}${route}`;
|
|
943
|
+
const headers = { 'X-API-Key': apiKey };
|
|
944
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
945
|
+
const res = await fetch(url, {
|
|
946
|
+
method,
|
|
947
|
+
headers,
|
|
948
|
+
...(body !== undefined && { body: JSON.stringify(body) }),
|
|
949
|
+
});
|
|
950
|
+
let data = {};
|
|
951
|
+
try { data = await res.json(); } catch { /* non-JSON body */ }
|
|
952
|
+
return { status: res.status, ok: res.ok, data: data || {} };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function printCleanLaneStatus(data) {
|
|
956
|
+
const active = data.clean_lane_active === true;
|
|
957
|
+
console.log(`Auto-publish clean learnings: ${active ? 'ON' : 'OFF'}`);
|
|
958
|
+
if (active) {
|
|
959
|
+
console.log(` since: ${data.last_action_at || 'unknown'}`);
|
|
960
|
+
console.log(` quality at least: ${data.min_auto_publish_quality}`);
|
|
961
|
+
console.log(` consent version: ${data.consent_version_recorded || data.consent_version_current}`);
|
|
962
|
+
} else if (data.last_action === 'freeze') {
|
|
963
|
+
console.log(` FROZEN: ${data.freeze_reason || 'unknown reason'} (${data.last_action_at || 'unknown time'})`);
|
|
964
|
+
console.log(' Nothing auto-publishes until you grant consent again: npx auxilo clean-lane grant');
|
|
965
|
+
} else if (data.last_action === 'revoke') {
|
|
966
|
+
console.log(` revoked at: ${data.last_action_at || 'unknown'}`);
|
|
967
|
+
} else if (data.last_action === 'grant') {
|
|
968
|
+
console.log(` a grant exists under consent version ${data.consent_version_recorded} but the current version is ${data.consent_version_current}; re-grant to re-activate.`);
|
|
969
|
+
}
|
|
970
|
+
console.log(` current consent version: ${data.consent_version_current}`);
|
|
971
|
+
// CLEAN-LANE-FLIP Phase B (notice hardening): the unread count, printed only
|
|
972
|
+
// when > 0. Nothing here acknowledges it — only the dashboard button does.
|
|
973
|
+
const unread = data.unacknowledged_publications;
|
|
974
|
+
if (Number.isInteger(unread) && unread > 0) {
|
|
975
|
+
console.log(` auto-published since you last checked: ${unread} (review and acknowledge them in your dashboard)`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function cmdCleanLane(flags) {
|
|
980
|
+
const sub = process.argv[3];
|
|
981
|
+
if (!['status', 'grant', 'revoke'].includes(sub)) {
|
|
982
|
+
if (sub) console.error(`Unknown clean-lane subcommand: ${sub}`);
|
|
983
|
+
usage('clean-lane');
|
|
984
|
+
process.exit(sub ? 1 : 0);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// The TTY gate runs BEFORE credentials and BEFORE any network call: a
|
|
988
|
+
// piped or scripted stdin can never reach the grant.
|
|
989
|
+
if (sub === 'grant' && !process.stdin.isTTY) {
|
|
990
|
+
console.error('auxilo clean-lane grant must be run by a person in an interactive terminal. It does not accept piped input, and there is no flag that skips typing the consent sentence.');
|
|
991
|
+
process.exit(1);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const creds = installer.readCredentials(HOME);
|
|
995
|
+
if (!creds || !creds.api_key) {
|
|
996
|
+
console.error('Not logged in. Run `npx auxilo setup` first.');
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
const baseUrl = resolveBaseUrl(flags);
|
|
1000
|
+
const apiKey = creds.api_key;
|
|
1001
|
+
|
|
1002
|
+
let status;
|
|
1003
|
+
try {
|
|
1004
|
+
status = await cleanLaneRequest({ apiKey, baseUrl, method: 'GET', route: '/account/clean-lane' });
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
console.error(`Could not reach the server: ${err.message}`);
|
|
1007
|
+
process.exit(1);
|
|
1008
|
+
}
|
|
1009
|
+
if (status.status === 404) {
|
|
1010
|
+
console.log(CLEAN_LANE_UNAVAILABLE);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (!status.ok) {
|
|
1014
|
+
console.error(`Could not read auto-publish status (HTTP ${status.status}): ${status.data.error || 'unknown error'}`);
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (sub === 'status') {
|
|
1019
|
+
printCleanLaneStatus(status.data);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (sub === 'revoke') {
|
|
1024
|
+
let res;
|
|
1025
|
+
try {
|
|
1026
|
+
res = await cleanLaneRequest({ apiKey, baseUrl, method: 'POST', route: '/account/clean-lane/revoke', body: {} });
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
console.error(`Could not reach the server: ${err.message}`);
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
if (res.status === 404) { console.log(CLEAN_LANE_UNAVAILABLE); return; }
|
|
1032
|
+
if (!res.ok) {
|
|
1033
|
+
console.error(`Revoke failed (HTTP ${res.status}): ${res.data.error || 'unknown error'}`);
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
console.log(res.data.message || 'Auto-publish is now OFF.');
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// ── grant: explainer → threshold → the sentence, typed verbatim ──────────
|
|
1041
|
+
console.log(CLEAN_LANE_EXPLAINER);
|
|
1042
|
+
if (status.data.clean_lane_active === true) {
|
|
1043
|
+
console.log(`Auto-publish is already ON (quality at least ${status.data.min_auto_publish_quality}, since ${status.data.last_action_at}). Granting again records a fresh consent row with the threshold you choose now.\n`);
|
|
1044
|
+
} else if (status.data.last_action === 'freeze') {
|
|
1045
|
+
console.log(`Auto-publish is FROZEN: ${status.data.freeze_reason || 'unknown reason'}. Granting again re-activates it.\n`);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
let minQuality = CLEAN_LANE_MIN_QUALITY_DEFAULT;
|
|
1049
|
+
for (;;) {
|
|
1050
|
+
const answer = await ask(`Publish only when the quality score is at least [${CLEAN_LANE_MIN_QUALITY_MIN}-${CLEAN_LANE_MIN_QUALITY_MAX}, default ${CLEAN_LANE_MIN_QUALITY_DEFAULT}]: `);
|
|
1051
|
+
if (answer === '') break;
|
|
1052
|
+
const n = parseInt(answer, 10);
|
|
1053
|
+
if (Number.isInteger(n) && String(n) === answer && n >= CLEAN_LANE_MIN_QUALITY_MIN && n <= CLEAN_LANE_MIN_QUALITY_MAX) {
|
|
1054
|
+
minQuality = n;
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
if (readlineEnded) { console.log('Aborted. Nothing changed.'); return; }
|
|
1058
|
+
console.log(`Enter a whole number from ${CLEAN_LANE_MIN_QUALITY_MIN} to ${CLEAN_LANE_MIN_QUALITY_MAX}.`);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// The consent text itself, verbatim (word-wrapped for the terminal only), before the sentence.
|
|
1062
|
+
console.log('\nTerms of Service, Section 5.9.3(g): the consent you are giving\n');
|
|
1063
|
+
console.log(wrapForTerminal(CLEAN_LANE_TERMS_G));
|
|
1064
|
+
console.log('');
|
|
1065
|
+
console.log(wrapForTerminal(CLEAN_LANE_TERMS_G2));
|
|
1066
|
+
console.log(`\nFull Terms: ${baseUrl}/terms`);
|
|
1067
|
+
// The no-email line, verbatim, directly before the affirmation prompt.
|
|
1068
|
+
console.log(`\n${wrapForTerminal(CLEAN_LANE_NO_EMAIL_LINE)}`);
|
|
1069
|
+
console.log('\nTo turn on auto-publish, type this sentence exactly as written, then press Enter:');
|
|
1070
|
+
console.log(`\n ${CLEAN_LANE_AFFIRMATION}\n`);
|
|
1071
|
+
const typed = await ask('> ');
|
|
1072
|
+
if (typed !== CLEAN_LANE_AFFIRMATION) {
|
|
1073
|
+
console.log('The sentence did not match. Aborted. Nothing changed.');
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
let res;
|
|
1078
|
+
try {
|
|
1079
|
+
res = await cleanLaneRequest({
|
|
1080
|
+
apiKey,
|
|
1081
|
+
baseUrl,
|
|
1082
|
+
method: 'POST',
|
|
1083
|
+
route: '/account/clean-lane/grant',
|
|
1084
|
+
body: {
|
|
1085
|
+
consent_version: status.data.consent_version_current,
|
|
1086
|
+
agree: true,
|
|
1087
|
+
affirmation: typed, // what the human typed, transmitted verbatim
|
|
1088
|
+
min_auto_publish_quality: minQuality,
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
console.error(`Could not reach the server: ${err.message}`);
|
|
1093
|
+
process.exit(1);
|
|
1094
|
+
}
|
|
1095
|
+
if (res.status === 404) { console.log(CLEAN_LANE_UNAVAILABLE); return; }
|
|
1096
|
+
if (res.status === 409) {
|
|
1097
|
+
console.error('The consent version changed on the server while you were reading. Run `npx auxilo clean-lane grant` again.');
|
|
1098
|
+
process.exit(1);
|
|
1099
|
+
}
|
|
1100
|
+
if (!res.ok) {
|
|
1101
|
+
console.error(`Grant failed (HTTP ${res.status}): ${res.data.error || 'unknown error'}`);
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
}
|
|
1104
|
+
console.log(`\n${res.data.message || 'Auto-publish is now ON.'}`);
|
|
1105
|
+
console.log(` quality at least: ${res.data.min_auto_publish_quality}`);
|
|
1106
|
+
console.log(` consent version: ${res.data.consent_version}`);
|
|
1107
|
+
console.log(' Turn it off any time: npx auxilo clean-lane revoke');
|
|
1108
|
+
}
|
|
1109
|
+
|
|
863
1110
|
// ─── Entry point ────────────────────────────────────────────────────────────
|
|
864
1111
|
|
|
865
1112
|
function usage(command) {
|
|
@@ -903,6 +1150,18 @@ them private or sanitize-promote a corrected replacement.`,
|
|
|
903
1150
|
disable: `Usage: auxilo disable [--base-url <url>]
|
|
904
1151
|
|
|
905
1152
|
Disable background extraction locally and optionally revoke server consent.`,
|
|
1153
|
+
'clean-lane': `Usage: auxilo clean-lane <status|grant|revoke> [--base-url <url>]
|
|
1154
|
+
|
|
1155
|
+
Auto-publish clean learnings (standing consent). While the feature is not yet
|
|
1156
|
+
available on your account every subcommand says so and changes nothing.
|
|
1157
|
+
|
|
1158
|
+
status Show whether auto-publish is on, the quality threshold, and the
|
|
1159
|
+
consent version.
|
|
1160
|
+
grant Turn it on. Interactive ONLY: you choose the threshold and then
|
|
1161
|
+
TYPE the consent sentence exactly. No flag or piped input can do
|
|
1162
|
+
this for you.
|
|
1163
|
+
revoke Turn it off (one step, no confirmation). Already-published
|
|
1164
|
+
learnings keep their 7-day retraction window.`,
|
|
906
1165
|
};
|
|
907
1166
|
if (command && blocks[command]) {
|
|
908
1167
|
console.log(`\n${blocks[command]}\n`);
|
|
@@ -951,6 +1210,9 @@ Commands:
|
|
|
951
1210
|
skips that step.
|
|
952
1211
|
disable Turn off background extraction (local kill-switch; optional
|
|
953
1212
|
server-side consent revoke).
|
|
1213
|
+
clean-lane <status|grant|revoke>
|
|
1214
|
+
Auto-publish clean learnings (standing consent). grant is
|
|
1215
|
+
interactive only: you type the consent sentence yourself.
|
|
954
1216
|
|
|
955
1217
|
Docs: https://auxilo.io · API: ${installer.DEFAULT_BASE_URL}
|
|
956
1218
|
`);
|
|
@@ -959,7 +1221,7 @@ Docs: https://auxilo.io · API: ${installer.DEFAULT_BASE_URL}
|
|
|
959
1221
|
async function main() {
|
|
960
1222
|
const cmd = process.argv[2];
|
|
961
1223
|
const subcommandHelp = ['help', '--help', '-h'].includes(process.argv[3]);
|
|
962
|
-
if (['setup', 'init', 'status', 'review', 'disable'].includes(cmd) && subcommandHelp) {
|
|
1224
|
+
if (['setup', 'init', 'status', 'review', 'disable', 'clean-lane'].includes(cmd) && subcommandHelp) {
|
|
963
1225
|
return usage(cmd);
|
|
964
1226
|
}
|
|
965
1227
|
const flags = parseFlags(process.argv);
|
|
@@ -969,6 +1231,7 @@ async function main() {
|
|
|
969
1231
|
case 'status': return cmdStatus(flags);
|
|
970
1232
|
case 'review': return cmdReview(flags);
|
|
971
1233
|
case 'disable': return cmdDisable(flags);
|
|
1234
|
+
case 'clean-lane': return cmdCleanLane(flags);
|
|
972
1235
|
case 'help': case '--help': case '-h': case undefined: return usage();
|
|
973
1236
|
default:
|
|
974
1237
|
console.error(`Unknown command: ${cmd}`);
|
|
@@ -996,10 +1259,17 @@ if (require.main === module) {
|
|
|
996
1259
|
|
|
997
1260
|
module.exports = {
|
|
998
1261
|
parseFlags,
|
|
1262
|
+
runnerSkewLine,
|
|
999
1263
|
resolveBaseUrl,
|
|
1000
1264
|
shortFlags,
|
|
1001
1265
|
groupSummaryRows,
|
|
1002
1266
|
printSummaryTable,
|
|
1003
1267
|
usage,
|
|
1004
1268
|
run,
|
|
1269
|
+
CLEAN_LANE_AFFIRMATION,
|
|
1270
|
+
CLEAN_LANE_UNAVAILABLE,
|
|
1271
|
+
CLEAN_LANE_TERMS_G,
|
|
1272
|
+
CLEAN_LANE_TERMS_G2,
|
|
1273
|
+
CLEAN_LANE_NO_EMAIL_LINE,
|
|
1274
|
+
wrapForTerminal,
|
|
1005
1275
|
};
|
package/lib/extraction-index.js
CHANGED
|
@@ -140,6 +140,20 @@ function localIndexRow(learning, response = {}, opts = {}) {
|
|
|
140
140
|
...(typeof response.status === 'string' && VALID_STATUSES.has(response.status) && {
|
|
141
141
|
status: response.status,
|
|
142
142
|
}),
|
|
143
|
+
// CLEAN-LANE-FLIP Phase A2: persist the standing-consent publish stamps the
|
|
144
|
+
// /learn response carries (published_via / standing_consent_version /
|
|
145
|
+
// retractable_until) so the SessionStart rollup (scripts/review-notice.js)
|
|
146
|
+
// can count clean-lane publishes from the local index. Spread only when
|
|
147
|
+
// present as non-empty strings; absent on every non-clean-lane response.
|
|
148
|
+
...(typeof response.published_via === 'string' && response.published_via && {
|
|
149
|
+
published_via: response.published_via,
|
|
150
|
+
}),
|
|
151
|
+
...(typeof response.standing_consent_version === 'string' && response.standing_consent_version && {
|
|
152
|
+
standing_consent_version: response.standing_consent_version,
|
|
153
|
+
}),
|
|
154
|
+
...(typeof response.retractable_until === 'string' && response.retractable_until && {
|
|
155
|
+
retractable_until: response.retractable_until,
|
|
156
|
+
}),
|
|
143
157
|
};
|
|
144
158
|
}
|
|
145
159
|
|
package/lib/installer.js
CHANGED
|
@@ -792,10 +792,18 @@ exit 0
|
|
|
792
792
|
* relative layout runner.js requires. Missing package files throw — the lib
|
|
793
793
|
* never calls process.exit.
|
|
794
794
|
*
|
|
795
|
+
* CLEAN-LANE-FLIP Phase B: the install also stamps <bin>/VERSION with the
|
|
796
|
+
* package version read from <packageRoot>/package.json at install time, so
|
|
797
|
+
* `auxilo status` can tell a stale copied stack from the CLI that is running
|
|
798
|
+
* (npx resolves a fresh auxilo-mcp; ~/.auxilo/bin keeps whatever setup last
|
|
799
|
+
* copied). VERSION is a stamp, not a stack row — it is deliberately NOT in
|
|
800
|
+
* RUNNER_STACK (test/runner-packaging-closure.test.js walks the require
|
|
801
|
+
* closure against the stack, and a version file is not code).
|
|
802
|
+
*
|
|
795
803
|
* @param {string} homeDir
|
|
796
804
|
* @param {object} [opts]
|
|
797
805
|
* @param {string} [opts.packageRoot=PACKAGE_ROOT]
|
|
798
|
-
* @returns {{ binRoot: string, hookPath: string, installed: string[] }}
|
|
806
|
+
* @returns {{ binRoot: string, hookPath: string, versionPath: string, version: string, installed: string[] }}
|
|
799
807
|
*/
|
|
800
808
|
function installRunner(homeDir, opts = {}) {
|
|
801
809
|
if (!homeDir) throw new Error('installRunner: homeDir is required');
|
|
@@ -820,7 +828,55 @@ function installRunner(homeDir, opts = {}) {
|
|
|
820
828
|
fs.chmodSync(hookPath, 0o755);
|
|
821
829
|
installed.push(hookPath);
|
|
822
830
|
|
|
823
|
-
|
|
831
|
+
const version = packageVersion(packageRoot);
|
|
832
|
+
const versionPath = runnerVersionPath(homeDir);
|
|
833
|
+
fs.writeFileSync(versionPath, `${version}\n`);
|
|
834
|
+
fs.chmodSync(versionPath, 0o644);
|
|
835
|
+
installed.push(versionPath);
|
|
836
|
+
|
|
837
|
+
return { binRoot, hookPath, versionPath, version, installed };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// ─── Runner-stack freshness (CLEAN-LANE-FLIP Phase B) ───────────────────────
|
|
841
|
+
|
|
842
|
+
/** <home>/.auxilo/bin/VERSION — the package version installRunner last copied. */
|
|
843
|
+
function runnerVersionPath(homeDir) {
|
|
844
|
+
return path.join(binRootFor(homeDir), 'VERSION');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Package version as shipped in <packageRoot>/package.json (throws if unreadable). */
|
|
848
|
+
function packageVersion(packageRoot = PACKAGE_ROOT) {
|
|
849
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
850
|
+
if (!pkg || typeof pkg.version !== 'string' || !pkg.version) {
|
|
851
|
+
throw new Error(`packageVersion: no version in ${path.join(packageRoot, 'package.json')}`);
|
|
852
|
+
}
|
|
853
|
+
return pkg.version;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/** Installed runner stamp, or null when the stamp is missing/unreadable (pre-0.9.12 installs). */
|
|
857
|
+
function installedRunnerVersion(homeDir) {
|
|
858
|
+
try {
|
|
859
|
+
const v = fs.readFileSync(runnerVersionPath(homeDir), 'utf-8').trim();
|
|
860
|
+
return v || null;
|
|
861
|
+
} catch {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Compare the copied runner stack against the running package.
|
|
868
|
+
*
|
|
869
|
+
* @param {string} homeDir
|
|
870
|
+
* @param {object} [opts]
|
|
871
|
+
* @param {string} [opts.packageRoot=PACKAGE_ROOT]
|
|
872
|
+
* @returns {{ installed: string|null, package: string, skew: boolean }}
|
|
873
|
+
* skew is true when the stamp is missing OR differs from the package version.
|
|
874
|
+
*/
|
|
875
|
+
function runnerVersionSkew(homeDir, opts = {}) {
|
|
876
|
+
if (!homeDir) throw new Error('runnerVersionSkew: homeDir is required');
|
|
877
|
+
const installed = installedRunnerVersion(homeDir);
|
|
878
|
+
const pkg = packageVersion(opts.packageRoot || PACKAGE_ROOT);
|
|
879
|
+
return { installed, package: pkg, skew: installed !== pkg };
|
|
824
880
|
}
|
|
825
881
|
|
|
826
882
|
// ─── Claude Code hook registration (spec §LW-12 step 4) ─────────────────────
|
|
@@ -1704,6 +1760,10 @@ module.exports = {
|
|
|
1704
1760
|
hookScriptPathFor,
|
|
1705
1761
|
renderHookScript,
|
|
1706
1762
|
installRunner,
|
|
1763
|
+
runnerVersionPath,
|
|
1764
|
+
packageVersion,
|
|
1765
|
+
installedRunnerVersion,
|
|
1766
|
+
runnerVersionSkew,
|
|
1707
1767
|
registerClaudeCodeHook,
|
|
1708
1768
|
removeClaudeCodeHook,
|
|
1709
1769
|
captureShimPath,
|
package/mcp-server.js
CHANGED
|
@@ -198,7 +198,7 @@ async function postBulkChunks(headers, decisions) {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
const server = new Server(
|
|
201
|
-
{ name: 'auxilo', version: '0.9.
|
|
201
|
+
{ name: 'auxilo', version: '0.9.12' },
|
|
202
202
|
{
|
|
203
203
|
capabilities: { tools: {} },
|
|
204
204
|
instructions: `You are connected to Auxilo, a knowledge marketplace where AI agents buy and sell operational learnings.
|
|
@@ -384,7 +384,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
384
384
|
},
|
|
385
385
|
{
|
|
386
386
|
name: 'auxilo_contributor',
|
|
387
|
-
description: 'Check earnings for a contributor wallet.
|
|
387
|
+
description: 'Check aggregate earnings totals for a contributor wallet. Per-learning earnings require the authenticated auxilo_account_earnings tool. Free.',
|
|
388
388
|
inputSchema: {
|
|
389
389
|
type: 'object',
|
|
390
390
|
properties: {
|
|
@@ -408,7 +408,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
408
408
|
},
|
|
409
409
|
{
|
|
410
410
|
name: 'auxilo_account_earnings',
|
|
411
|
-
description: 'View earnings for your authenticated Auxilo account. Authenticates with your configured API key automatically, or pass a session_token (JWT). Returns total gross, contributor share, pending balance, total withdrawn, whether withdrawal is available (can_withdraw), and held_pending_assent — undisbursable receipts recorded before you accepted the current Terms, released to your withdrawable balance when you accept via auxilo_accept_terms. Earnings from on-chain-settled sales are paid to your wallet at sale time and appear in settlement history, not in pending balance. Free.',
|
|
411
|
+
description: 'View aggregate and per-learning earnings for your authenticated Auxilo account. Authenticates with your configured API key automatically, or pass a session_token (JWT). Returns total gross, contributor share, pending balance, total withdrawn, whether withdrawal is available (can_withdraw), and held_pending_assent — undisbursable receipts recorded before you accepted the current Terms, released to your withdrawable balance when you accept via auxilo_accept_terms. Earnings from on-chain-settled sales are paid to your wallet at sale time and appear in settlement history, not in pending balance. Free.',
|
|
412
412
|
inputSchema: {
|
|
413
413
|
type: 'object',
|
|
414
414
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auxilo-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.12",
|
|
4
4
|
"mcpName": "io.github.silent-architects/auxilo",
|
|
5
5
|
"description": "MCP server for Auxilo. Your agent stops solving the same problem twice: auto-extracted learnings, free self-unlocks, and earnings when other agents unlock yours.",
|
|
6
6
|
"main": "mcp-server.js",
|
package/scripts/extract-local.js
CHANGED
|
@@ -36,20 +36,36 @@ const PRIVATE_CATEGORIES = [...CATEGORIES, 'non-technical'];
|
|
|
36
36
|
const RETIRED_CATEGORIES = ['communication', 'content-generation'];
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* SPEC3 slice A1 gate — score-at-extraction,
|
|
39
|
+
* SPEC3 slice A1 gate — score-at-extraction, ON BY DEFAULT since 0.9.12
|
|
40
|
+
* (CLEAN-LANE-FLIP Phase B). Explicit opt-out only: AUXILO_SCORE_EXTRACTION=0
|
|
41
|
+
* (or 'false') disables scoring; any other value, including unset, scores.
|
|
40
42
|
*
|
|
41
|
-
* ┌─
|
|
42
|
-
* │
|
|
43
|
-
* │
|
|
44
|
-
* │
|
|
45
|
-
* │
|
|
46
|
-
* │
|
|
47
|
-
* │
|
|
48
|
-
* │
|
|
49
|
-
*
|
|
43
|
+
* ┌─ SEQUENCING CONSTRAINT — SATISFIED (SPEC3-BUILDER-REVIEW-LOOP §3.1/§8) ──┐
|
|
44
|
+
* │ This gate shipped dark (opt-in via AUXILO_SCORE_EXTRACTION=1) because │
|
|
45
|
+
* │ under a server WITHOUT the B1 extraction-channel hold, a clean /learn │
|
|
46
|
+
* │ submission carrying a floor-passing quality_self_assessment published │
|
|
47
|
+
* │ IMMEDIATELY (seamlessEligible) — arming scoring would have silently │
|
|
48
|
+
* │ flipped hook extraction from "everything held" to "clean items │
|
|
49
|
+
* │ auto-publish", an unrecorded consent change (2026-06-10 class). │
|
|
50
|
+
* │ │
|
|
51
|
+
* │ B1 SHIPPED (lib/clean-lane.js; server.js /learn): every submission │
|
|
52
|
+
* │ carrying submission_channel:'extraction' (runner.js stamps it │
|
|
53
|
+
* │ unconditionally) is HELD with reason 'standing_consent_off' unless the │
|
|
54
|
+
* │ account holds an active standing-consent grant — regardless of the │
|
|
55
|
+
* │ score. The server brake, not the absence of a score, is what keeps │
|
|
56
|
+
* │ unconsented items out of the catalog, so default-on scoring is safe: │
|
|
57
|
+
* │ the score can only move a held item into the ready_to_publish lane (one │
|
|
58
|
+
* │ counted approve away) or, under an active grant, into the clean lane the │
|
|
59
|
+
* │ contributor explicitly opted into. Without a score every extraction │
|
|
60
|
+
* │ submission held awaiting_quality forever, so a stock install could never │
|
|
61
|
+
* │ enter the clean lane at all — that was the Phase B defect this closes. │
|
|
62
|
+
* └──────────────────────────────────────────────────────────────────────────┘
|
|
50
63
|
*/
|
|
51
64
|
function scoreExtractionEnabled(env = process.env) {
|
|
52
|
-
|
|
65
|
+
const raw = env.AUXILO_SCORE_EXTRACTION;
|
|
66
|
+
if (raw === undefined || raw === null) return true;
|
|
67
|
+
const v = String(raw).trim().toLowerCase();
|
|
68
|
+
return !(v === '0' || v === 'false');
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
const EXTRACTION_PROMPT_BASE = `You are extracting reusable OPERATIONAL LEARNINGS from an AI agent's session transcript, to publish to a PUBLIC knowledge marketplace read by other AI agents.
|
|
@@ -662,11 +678,55 @@ async function runAnchoredJudge(candidates, indexState, opts = {}) {
|
|
|
662
678
|
|
|
663
679
|
/**
|
|
664
680
|
* Extract learnings locally. Returns { learnings: [...] } or { learnings: [], skipped }.
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
* (
|
|
681
|
+
*
|
|
682
|
+
* EXT-GATE: every capture source id runs the client-local extractor. The
|
|
683
|
+
* extractor is transcript-text based (buildExtractionPrompt carries no
|
|
684
|
+
* per-source branch), so nothing here depends on WHICH client captured.
|
|
685
|
+
* Unknown ids still short-circuit: a `--source` value the registry does not
|
|
686
|
+
* know is a misconfigured shim, not a client, and the skip message below is
|
|
687
|
+
* matched by runner.js and test/uc6-codex-capture.test.js — do not change it.
|
|
688
|
+
*
|
|
689
|
+
* The list is static on purpose: lib/installer.js is not in RUNNER_STACK, so
|
|
690
|
+
* this file cannot enumerate the registry at runtime. The closure test
|
|
691
|
+
* (test/ext-gate-closure.test.js) derives the expected set from the two live
|
|
692
|
+
* enumerations — scripts/sources/*.js adapter ids ∪ installer hook-client
|
|
693
|
+
* source ids — and is the authority; a new adapter or hook client that is not
|
|
694
|
+
* added here turns CI red.
|
|
668
695
|
*/
|
|
669
|
-
const
|
|
696
|
+
const EXTRACTABLE_SOURCE_IDS = Object.freeze([
|
|
697
|
+
'antigravity',
|
|
698
|
+
'claude-code',
|
|
699
|
+
'cline',
|
|
700
|
+
'codex-cli',
|
|
701
|
+
'continue',
|
|
702
|
+
'copilot',
|
|
703
|
+
'cursor',
|
|
704
|
+
'factory',
|
|
705
|
+
'gemini-cli',
|
|
706
|
+
'openclaw',
|
|
707
|
+
'roo-code',
|
|
708
|
+
'windsurf',
|
|
709
|
+
]);
|
|
710
|
+
|
|
711
|
+
// Gate-A 2026-09-05: the exported set is IMMUTABLE. It stays a real Set (same
|
|
712
|
+
// name, `.has()` / iteration / `instanceof Set` unchanged) but its own
|
|
713
|
+
// add/delete/clear shadow the prototype's and throw, so no importer can widen
|
|
714
|
+
// or narrow the allowlist at runtime — the frozen id array above is the only
|
|
715
|
+
// source and the closure test is the only authority.
|
|
716
|
+
function immutableSet(ids) {
|
|
717
|
+
const set = new Set(ids);
|
|
718
|
+
const refuse = (op) => function () {
|
|
719
|
+
throw new TypeError(`EXTRACTABLE_SOURCES is immutable (${op} refused)`);
|
|
720
|
+
};
|
|
721
|
+
Object.defineProperties(set, {
|
|
722
|
+
add: { value: refuse('add'), writable: false, configurable: false, enumerable: false },
|
|
723
|
+
delete: { value: refuse('delete'), writable: false, configurable: false, enumerable: false },
|
|
724
|
+
clear: { value: refuse('clear'), writable: false, configurable: false, enumerable: false },
|
|
725
|
+
});
|
|
726
|
+
return Object.freeze(set);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const EXTRACTABLE_SOURCES = immutableSet(EXTRACTABLE_SOURCE_IDS);
|
|
670
730
|
|
|
671
731
|
async function extractLocally(transcript, sourceType, opts = {}) {
|
|
672
732
|
if (sourceType && !EXTRACTABLE_SOURCES.has(sourceType)) {
|
|
@@ -780,7 +840,7 @@ async function extractLocally(transcript, sourceType, opts = {}) {
|
|
|
780
840
|
}
|
|
781
841
|
|
|
782
842
|
module.exports = {
|
|
783
|
-
extractLocally, extractWithClaudeCode, checkClaudeAuthStatus,
|
|
843
|
+
extractLocally, extractWithClaudeCode, checkClaudeAuthStatus, EXTRACTABLE_SOURCES, EXTRACTABLE_SOURCE_IDS,
|
|
784
844
|
parseLearnings, parseExtractionOutput, resolveClaudeBin,
|
|
785
845
|
CATEGORIES, PRIVATE_CATEGORIES, RETIRED_CATEGORIES,
|
|
786
846
|
EXTRACTION_PROMPT, buildExtractionPrompt, scoreExtractionEnabled,
|
package/scripts/review-notice.js
CHANGED
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
* - Count source: GET /account/pending/summary with the credentials from
|
|
25
25
|
* ~/.auxilo/credentials.json; 3.5s abort so session start is never held
|
|
26
26
|
* hostage by a slow network.
|
|
27
|
+
* - Standing-consent rollup (CLEAN-LANE-FLIP Phase A, SPEC3-C1 §4.3): ONE
|
|
28
|
+
* more count-only line when the LOCAL submitted-learnings log
|
|
29
|
+
* (~/.auxilo/extracted-index.jsonl, lib/extraction-index.js) holds rows
|
|
30
|
+
* stamped published_via = clean_lane_standing_consent since the last
|
|
31
|
+
* notice. Zero platform cost; reaches the human in their own client.
|
|
32
|
+
* Suppression and the last-notice stamp are shared with the held-count
|
|
33
|
+
* line (one state file, one 4h window).
|
|
27
34
|
*
|
|
28
35
|
* Self-contained (fs/path/os + global fetch) — ships in RUNNER_STACK to
|
|
29
36
|
* ~/.auxilo/bin/scripts/ and must not require anything outside that layout
|
|
@@ -86,6 +93,61 @@ function renderNotice(count) {
|
|
|
86
93
|
return `Auxilo: ${count} learning(s) held for your review — run auxilo_review (MCP) or \`npx auxilo review\`.`;
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Stamp lib/clean-lane.js writes on lane publishes (PUBLISHED_VIA_CLEAN_LANE).
|
|
98
|
+
* A literal here because this script is self-contained (RUNNER_STACK layout);
|
|
99
|
+
* test/clean-lane-phase-a.test.js pins the two byte-equal.
|
|
100
|
+
*/
|
|
101
|
+
const PUBLISHED_VIA_CLEAN_LANE = 'clean_lane_standing_consent';
|
|
102
|
+
|
|
103
|
+
/** Local submitted-learnings log (lib/extraction-index.js DEFAULT_INDEX_PATH). */
|
|
104
|
+
function submittedIndexPath(homeDir) {
|
|
105
|
+
return path.join(auxiloDir(homeDir), 'extracted-index.jsonl');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Read the local log; absent/unreadable → []; malformed lines skipped. */
|
|
109
|
+
function readSubmittedRows(homeDir) {
|
|
110
|
+
let raw;
|
|
111
|
+
try {
|
|
112
|
+
raw = fs.readFileSync(submittedIndexPath(homeDir), 'utf-8');
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const rows = [];
|
|
117
|
+
for (const line of String(raw).split(/\r?\n/)) {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (!trimmed) continue;
|
|
120
|
+
try {
|
|
121
|
+
const row = JSON.parse(trimmed);
|
|
122
|
+
if (row && typeof row === 'object') rows.push(row);
|
|
123
|
+
} catch { /* skip */ }
|
|
124
|
+
}
|
|
125
|
+
return rows;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Pure: how many local rows were published under standing consent AFTER
|
|
130
|
+
* `sinceIso` (the last notice stamp). No stamp → every such row counts.
|
|
131
|
+
*/
|
|
132
|
+
function countStandingConsentPublishes(rows, sinceIso) {
|
|
133
|
+
const since = sinceIso ? Date.parse(sinceIso) : NaN;
|
|
134
|
+
let n = 0;
|
|
135
|
+
for (const row of rows || []) {
|
|
136
|
+
if (!row || row.published_via !== PUBLISHED_VIA_CLEAN_LANE) continue;
|
|
137
|
+
if (Number.isFinite(since)) {
|
|
138
|
+
const t = Date.parse(row.submitted_at);
|
|
139
|
+
if (!Number.isFinite(t) || t <= since) continue;
|
|
140
|
+
}
|
|
141
|
+
n += 1;
|
|
142
|
+
}
|
|
143
|
+
return n;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The rollup line. Count only — same contract as renderNotice. */
|
|
147
|
+
function renderStandingConsentNotice(count) {
|
|
148
|
+
return `Auxilo: ${count} learning(s) auto-published under your standing consent (retract within 7 days: npx auxilo review).`;
|
|
149
|
+
}
|
|
150
|
+
|
|
89
151
|
/** Load credentials; null when absent/malformed/keyless. */
|
|
90
152
|
function readCredentials(homeDir) {
|
|
91
153
|
try {
|
|
@@ -125,18 +187,26 @@ async function main() {
|
|
|
125
187
|
const creds = readCredentials(homeDir);
|
|
126
188
|
if (!creds) return;
|
|
127
189
|
|
|
128
|
-
|
|
190
|
+
const state = readState(homeDir);
|
|
191
|
+
if (!shouldNotify(state)) return;
|
|
129
192
|
|
|
130
193
|
const count = await fetchPendingCount(creds);
|
|
131
|
-
|
|
194
|
+
const autoPublished = countStandingConsentPublishes(readSubmittedRows(homeDir), state.last_notice_at);
|
|
195
|
+
|
|
196
|
+
const lines = [];
|
|
197
|
+
if (count != null && count > 0) lines.push(renderNotice(count));
|
|
198
|
+
if (autoPublished > 0) lines.push(renderStandingConsentNotice(autoPublished));
|
|
199
|
+
if (lines.length === 0) return;
|
|
132
200
|
|
|
133
|
-
process.stdout.write(
|
|
201
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
134
202
|
writeState(homeDir);
|
|
135
203
|
}
|
|
136
204
|
|
|
137
205
|
module.exports = {
|
|
138
206
|
shouldNotify, renderNotice, readState, writeState, readCredentials,
|
|
139
207
|
fetchPendingCount, NOTICE_SUPPRESSION_MS, FETCH_TIMEOUT_MS,
|
|
208
|
+
PUBLISHED_VIA_CLEAN_LANE, submittedIndexPath, readSubmittedRows,
|
|
209
|
+
countStandingConsentPublishes, renderStandingConsentNotice,
|
|
140
210
|
};
|
|
141
211
|
|
|
142
212
|
if (require.main === module) {
|