shakers-cli 0.0.0-stage → 0.14.1
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/LICENSE +59 -0
- package/README.md +1233 -2
- package/bin/add-agent.js +88 -0
- package/bin/add-project.js +88 -0
- package/bin/add-role.js +61 -0
- package/bin/add-skill.js +88 -0
- package/bin/ai-usage.js +99 -0
- package/bin/alma.js +99 -0
- package/bin/applications.js +69 -0
- package/bin/availability.js +70 -0
- package/bin/certifications.js +69 -0
- package/bin/certify.js +73 -0
- package/bin/change-role.js +61 -0
- package/bin/config.js +124 -0
- package/bin/experiences.js +69 -0
- package/bin/find-projects.js +89 -0
- package/bin/invitations.js +69 -0
- package/bin/lang.js +70 -0
- package/bin/login.js +216 -0
- package/bin/mcp.js +124 -0
- package/bin/mcpb.js +5 -0
- package/bin/me.js +69 -0
- package/bin/onboarding.js +134 -0
- package/bin/portfolios.js +69 -0
- package/bin/rate.js +70 -0
- package/bin/register.js +337 -0
- package/bin/report-html.js +134 -0
- package/bin/save.js +77 -0
- package/bin/shakers.js +363 -0
- package/bin/share.js +72 -0
- package/bin/show-project.js +72 -0
- package/bin/socials.js +70 -0
- package/bin/superadmin.js +351 -0
- package/bin/update-check-worker.js +70 -0
- package/bin/update.js +111 -0
- package/package.json +40 -4
- package/src/agent-category.js +13 -0
- package/src/agent-evaluation.js +177 -0
- package/src/agent-org-chart.js +355 -0
- package/src/agent-synthesis.js +132 -0
- package/src/agent-usage.js +118 -0
- package/src/agents-client.js +165 -0
- package/src/ai-fluency-cell.js +72 -0
- package/src/ai-profile-client.js +45 -0
- package/src/ai-profile-preview.js +154 -0
- package/src/ai.js +66 -0
- package/src/alma-client.js +123 -0
- package/src/alma-flow.js +32 -0
- package/src/alma-repo-context.js +47 -0
- package/src/alma-shell.js +161 -0
- package/src/ansi.js +62 -0
- package/src/applications-client.js +79 -0
- package/src/applications-flow.js +58 -0
- package/src/auth-client.js +144 -0
- package/src/auth-device-client.js +118 -0
- package/src/auth-method-picker.js +44 -0
- package/src/auth-methods.js +35 -0
- package/src/auth-session-store.js +215 -0
- package/src/authorship-mismatch.js +62 -0
- package/src/authorship.js +204 -0
- package/src/automations-detector.js +170 -0
- package/src/availability-client.js +93 -0
- package/src/availability-flow.js +124 -0
- package/src/backend-request.js +191 -0
- package/src/blind-approval-extract.js +48 -0
- package/src/brand-ansi.js +14 -0
- package/src/brand-svg.js +9 -0
- package/src/browser-tools-detector.js +21 -0
- package/src/build-next-level.js +56 -0
- package/src/certifications-client.js +89 -0
- package/src/certifications-flow.js +75 -0
- package/src/certify-dimension-client.js +163 -0
- package/src/certify-dimension-flow.js +135 -0
- package/src/certify-disclaimer.js +51 -0
- package/src/certify-remediation-prompt.js +23 -0
- package/src/cli-args.js +83 -0
- package/src/command-audit.js +105 -0
- package/src/config-dir.js +120 -0
- package/src/config.js +877 -0
- package/src/consent-flow.js +115 -0
- package/src/consent-skip.js +27 -0
- package/src/cv-file.js +62 -0
- package/src/cv-text.js +92 -0
- package/src/decisions-extract.js +43 -0
- package/src/detect-repos.js +145 -0
- package/src/detected-tools.js +15 -0
- package/src/detection-evidence.js +105 -0
- package/src/detection-noise.js +132 -0
- package/src/detectors.js +221 -0
- package/src/device-auth-store.js +100 -0
- package/src/device-login.js +120 -0
- package/src/email-verification.js +152 -0
- package/src/env-paths.js +30 -0
- package/src/find-projects-cache.js +89 -0
- package/src/find-projects-client.js +148 -0
- package/src/find-projects-flow.js +164 -0
- package/src/git-activity.js +124 -0
- package/src/git-log-prefetch.js +53 -0
- package/src/graph-certs.js +58 -0
- package/src/graph-generator.js +230 -0
- package/src/graph-scan.js +239 -0
- package/src/i18n.js +3341 -0
- package/src/inter-fonts.js +6 -0
- package/src/interactive-select.js +183 -0
- package/src/interview-client.js +226 -0
- package/src/interview-transcript.js +56 -0
- package/src/inventory-client.js +90 -0
- package/src/invitations-client.js +66 -0
- package/src/invitations-flow.js +50 -0
- package/src/lang-client.js +147 -0
- package/src/lang-codes.js +14 -0
- package/src/lang-flow.js +85 -0
- package/src/legal-notice.js +53 -0
- package/src/locale.js +72 -0
- package/src/markdown-ansi.js +50 -0
- package/src/maturity.js +111 -0
- package/src/mcp-ai-usage-result-tool.js +111 -0
- package/src/mcp-ai-usage-tool.js +401 -0
- package/src/mcp-alma-tools.js +95 -0
- package/src/mcp-applications-tools.js +44 -0
- package/src/mcp-auth-form.js +480 -0
- package/src/mcp-availability-tools.js +45 -0
- package/src/mcp-certifications-tools.js +44 -0
- package/src/mcp-certify-dimension-tools.js +66 -0
- package/src/mcp-choice.js +95 -0
- package/src/mcp-detector.js +223 -0
- package/src/mcp-device-login-tools.js +177 -0
- package/src/mcp-find-projects-tools.js +69 -0
- package/src/mcp-install.js +275 -0
- package/src/mcp-instructions.js +27 -0
- package/src/mcp-invitations-tools.js +44 -0
- package/src/mcp-lang-tools.js +41 -0
- package/src/mcp-onboarding-tools.js +379 -0
- package/src/mcp-portfolios-tools.js +50 -0
- package/src/mcp-profile-tools.js +48 -0
- package/src/mcp-prompts.js +73 -0
- package/src/mcp-rate-tools.js +44 -0
- package/src/mcp-register-tools.js +441 -0
- package/src/mcp-report-tools.js +105 -0
- package/src/mcp-role-tools.js +372 -0
- package/src/mcp-saved-positions-tools.js +59 -0
- package/src/mcp-server.js +290 -0
- package/src/mcp-show-project-tools.js +52 -0
- package/src/mcp-signup-client.js +118 -0
- package/src/mcp-signup-tools.js +1171 -0
- package/src/mcp-socials-tools.js +41 -0
- package/src/memory-structure-detector.js +108 -0
- package/src/model-call-record.js +198 -0
- package/src/onboarding-availability.js +26 -0
- package/src/onboarding-client.js +374 -0
- package/src/onboarding-flow.js +1162 -0
- package/src/onboarding-funnel.js +272 -0
- package/src/onboarding-livekit-client.js +102 -0
- package/src/onboarding-status-client.js +57 -0
- package/src/open-file.js +32 -0
- package/src/osc-link.js +16 -0
- package/src/portfolio-client.js +164 -0
- package/src/portfolio-git-info.js +61 -0
- package/src/portfolios-client.js +85 -0
- package/src/portfolios-flow.js +79 -0
- package/src/profile-client.js +133 -0
- package/src/profile-flow.js +115 -0
- package/src/profile-switch.js +46 -0
- package/src/prompt-select.js +66 -0
- package/src/rate-client.js +81 -0
- package/src/rate-flow.js +137 -0
- package/src/register-context.js +145 -0
- package/src/render-ai-fluency.js +266 -0
- package/src/render-certification.js +177 -0
- package/src/render-html.js +140 -0
- package/src/render-report-text.js +27 -0
- package/src/render-sheet.js +261 -0
- package/src/render-terminal.js +109 -0
- package/src/render-usage-report-text.js +155 -0
- package/src/repo-context.js +215 -0
- package/src/report-gating.js +49 -0
- package/src/report-store.js +226 -0
- package/src/report-theme.js +177 -0
- package/src/report-view-client.js +50 -0
- package/src/reveal-output.js +29 -0
- package/src/roadmap-content.js +453 -0
- package/src/roadmap-personalization.js +132 -0
- package/src/roadmap-prompt.js +154 -0
- package/src/roles-client.js +112 -0
- package/src/roles-flow.js +192 -0
- package/src/sanitize-network-text.js +36 -0
- package/src/saved-positions-client.js +71 -0
- package/src/saved-positions-flow.js +50 -0
- package/src/scan-exclusions.js +50 -0
- package/src/scanner.js +624 -0
- package/src/scope-signals.js +148 -0
- package/src/secret-prompt.js +72 -0
- package/src/session-gate.js +30 -0
- package/src/session-normalizers.js +174 -0
- package/src/session-refresh.js +51 -0
- package/src/session-scan.js +475 -0
- package/src/session-signals.js +36 -0
- package/src/shakers-wordmark.js +17 -0
- package/src/share-card.js +362 -0
- package/src/share-certification.js +111 -0
- package/src/share-consent-state.js +157 -0
- package/src/share-payload.js +300 -0
- package/src/share.js +185 -0
- package/src/show-project-client.js +58 -0
- package/src/show-project-flow.js +119 -0
- package/src/signup-client.js +103 -0
- package/src/signup-copy.js +450 -0
- package/src/signup-language.js +21 -0
- package/src/skills-client.js +102 -0
- package/src/socials-client.js +91 -0
- package/src/socials-flow.js +83 -0
- package/src/specificity-extract.js +41 -0
- package/src/start-add-agents.js +306 -0
- package/src/start-add-portfolio.js +388 -0
- package/src/start-add-skills.js +260 -0
- package/src/stdin-ask.js +129 -0
- package/src/steering-extract.js +43 -0
- package/src/strip-internal-comments.js +45 -0
- package/src/tech-detector.js +295 -0
- package/src/templates/report-sheet.html +572 -0
- package/src/terminal-agents.js +207 -0
- package/src/terminal-format.js +85 -0
- package/src/terminal-progress.js +106 -0
- package/src/terminal-sections.js +219 -0
- package/src/tier-analysis.js +126 -0
- package/src/tier-engine.js +114 -0
- package/src/tier-projection.js +117 -0
- package/src/transcript-turns.js +49 -0
- package/src/translations-client.js +84 -0
- package/src/typewriter.js +154 -0
- package/src/update-flow.js +223 -0
- package/src/update-notifier.js +113 -0
- package/src/usage-model-calls.js +119 -0
- package/src/usage-oneshot-commands.js +180 -0
- package/src/usage-repo-scope.js +162 -0
- package/src/usage-run-steps.js +354 -0
- package/src/work-streams.js +79 -0
package/src/rate-flow.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `rate` flow: view the talent's per-project pricing, and `--set` to change it
|
|
4
|
+
// (interactive, explicit confirmation — it changes what clients pay).
|
|
5
|
+
|
|
6
|
+
const { styleLabelPrefix } = require('./ansi');
|
|
7
|
+
|
|
8
|
+
const CURRENCIES = ['EUR', 'USD', 'GBP'];
|
|
9
|
+
const MAX_AMOUNT = 999999.99;
|
|
10
|
+
|
|
11
|
+
function makeRateDeps(overrides = {}) {
|
|
12
|
+
const client = require('./rate-client');
|
|
13
|
+
return {
|
|
14
|
+
fetchPricingRate: (opts) => client.fetchPricingRate({}, opts),
|
|
15
|
+
savePricingRate: (opts) => client.savePricingRate({}, opts),
|
|
16
|
+
promptSelect: (opts) => require('./prompt-select').promptSelect(opts),
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function priceLine(fp, money) {
|
|
22
|
+
if (!money || money.amount == null) return fp.notSet;
|
|
23
|
+
return `${money.amount}${money.currency ? ` ${money.currency}` : ''}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Empty pricing when the talent has none yet (upsert still works).
|
|
27
|
+
const EMPTY_PRICING = { fullTimeSelected: false, fullTimePrice: null, partTimeSelected: false, partTimePrice: null };
|
|
28
|
+
|
|
29
|
+
function parseAmount(raw) {
|
|
30
|
+
const s = String(raw == null ? '' : raw).trim().replace(',', '.');
|
|
31
|
+
if (!/^\d+(\.\d+)?$/.test(s)) return null;
|
|
32
|
+
const n = Number.parseFloat(s);
|
|
33
|
+
if (!Number.isFinite(n) || n < 0 || n > MAX_AMOUNT) return null;
|
|
34
|
+
return n;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Merge the chosen modality's new price into the current pricing → the PUT body
|
|
38
|
+
// (full object, so the untouched modality is preserved, never cleared).
|
|
39
|
+
function buildBody(current, modality, amount, currency) {
|
|
40
|
+
const body = {
|
|
41
|
+
fullTimeProjectSelected: current.fullTimeSelected === true,
|
|
42
|
+
fullTimeProjectPrice: current.fullTimePrice ? { amount: current.fullTimePrice.amount, currency: current.fullTimePrice.currency } : null,
|
|
43
|
+
partTimeProjectSelected: current.partTimeSelected === true,
|
|
44
|
+
partTimeProjectPrice: current.partTimePrice ? { amount: current.partTimePrice.amount, currency: current.partTimePrice.currency } : null,
|
|
45
|
+
};
|
|
46
|
+
if (modality === 'full') { body.fullTimeProjectSelected = true; body.fullTimeProjectPrice = { amount, currency }; }
|
|
47
|
+
else { body.partTimeProjectSelected = true; body.partTimeProjectPrice = { amount, currency }; }
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderView(io, fp, p) {
|
|
52
|
+
io.section(fp.title);
|
|
53
|
+
io.notify(styleLabelPrefix(fp.fullTime(priceLine(fp, p.fullTimeSelected ? p.fullTimePrice : null))));
|
|
54
|
+
if (p.partTimeSelected) io.notify(styleLabelPrefix(fp.partTime(priceLine(fp, p.partTimePrice))));
|
|
55
|
+
io.success(fp.done);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runSet({ io, ask, session, catalog, opts, deps }) {
|
|
59
|
+
const fp = catalog.rate;
|
|
60
|
+
const hubAccessToken = session ? session.hubAccessToken : null;
|
|
61
|
+
if (!opts.stdinIsTTY) {
|
|
62
|
+
io.error(fp.setNeedsInteractive);
|
|
63
|
+
return { ok: false, reason: 'non-interactive' };
|
|
64
|
+
}
|
|
65
|
+
const selOut = (s) => io.notify(s);
|
|
66
|
+
|
|
67
|
+
// Read current so we can show it, preserve the other modality, and diff.
|
|
68
|
+
const read = await io.withProgress(fp.loading, () => deps.fetchPricingRate({ hubAccessToken }));
|
|
69
|
+
if (!read.ok && read.reason !== 'not-found') {
|
|
70
|
+
io.error(fp.fetchFailed(read.reason));
|
|
71
|
+
return { ok: false, reason: read.reason };
|
|
72
|
+
}
|
|
73
|
+
const current = read.ok ? read.pricing : EMPTY_PRICING;
|
|
74
|
+
|
|
75
|
+
io.section(fp.setTitle);
|
|
76
|
+
io.notify(styleLabelPrefix(fp.fullTime(priceLine(fp, current.fullTimeSelected ? current.fullTimePrice : null))));
|
|
77
|
+
io.notify(styleLabelPrefix(fp.partTime(priceLine(fp, current.partTimeSelected ? current.partTimePrice : null))));
|
|
78
|
+
|
|
79
|
+
// Arrow-key pickers for every discrete choice (modality, currency).
|
|
80
|
+
const modItem = await deps.promptSelect({
|
|
81
|
+
ask, stdinIsTTY: opts.stdinIsTTY, out: selOut, header: fp.setWhich,
|
|
82
|
+
items: [{ label: fp.modalityFull, value: 'full' }, { label: fp.modalityPart, value: 'part' }],
|
|
83
|
+
labelFor: (x) => x.label, input: opts.input, output: opts.output,
|
|
84
|
+
});
|
|
85
|
+
if (!modItem) { io.notify(fp.setCancelled); return { ok: true, cancelled: true }; }
|
|
86
|
+
const modality = modItem.value;
|
|
87
|
+
const modalityLabel = modality === 'full' ? fp.modalityFull : fp.modalityPart;
|
|
88
|
+
|
|
89
|
+
const amount = parseAmount(await io.ask(fp.setAmount(modalityLabel)));
|
|
90
|
+
if (amount === null) { io.error(fp.setAmountInvalid); return { ok: false, reason: 'bad-amount' }; }
|
|
91
|
+
|
|
92
|
+
const currency = await deps.promptSelect({
|
|
93
|
+
ask, stdinIsTTY: opts.stdinIsTTY, out: selOut, header: fp.setCurrency,
|
|
94
|
+
items: CURRENCIES, labelFor: (x) => x, input: opts.input, output: opts.output,
|
|
95
|
+
});
|
|
96
|
+
if (!currency) { io.notify(fp.setCancelled); return { ok: true, cancelled: true }; }
|
|
97
|
+
|
|
98
|
+
const oldMoney = modality === 'full' ? (current.fullTimeSelected ? current.fullTimePrice : null) : (current.partTimeSelected ? current.partTimePrice : null);
|
|
99
|
+
const oldStr = priceLine(fp, oldMoney);
|
|
100
|
+
const newStr = `${amount} ${currency}`;
|
|
101
|
+
io.notify(fp.setDiff(modalityLabel, oldStr, newStr));
|
|
102
|
+
const confirmed = await io.confirm(fp.setConfirm);
|
|
103
|
+
if (!confirmed) { io.notify(fp.setCancelled); return { ok: true, cancelled: true }; }
|
|
104
|
+
|
|
105
|
+
const body = buildBody(current, modality, amount, currency);
|
|
106
|
+
const res = await io.withProgress(fp.setSaving, () => deps.savePricingRate({ hubAccessToken, pricing: body }));
|
|
107
|
+
if (!res.ok) { io.error(fp.setFailed(res.reason)); return { ok: false, reason: res.reason }; }
|
|
108
|
+
io.success(fp.setDone(modalityLabel, newStr));
|
|
109
|
+
return { ok: true, set: { modality, amount, currency } };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function runRate({ io, rawOut, ask, session, catalog, opts = {}, deps = makeRateDeps() }) {
|
|
113
|
+
const fp = catalog.rate;
|
|
114
|
+
if (opts.set) return runSet({ io, ask, session, catalog, opts, deps });
|
|
115
|
+
|
|
116
|
+
const hubAccessToken = session ? session.hubAccessToken : null;
|
|
117
|
+
const doFetch = () => deps.fetchPricingRate({ hubAccessToken });
|
|
118
|
+
const res = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
io.error(res.reason === 'not-found' ? fp.notFound : fp.fetchFailed(res.reason));
|
|
121
|
+
return { ok: false, reason: res.reason };
|
|
122
|
+
}
|
|
123
|
+
if (opts.json) {
|
|
124
|
+
(rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, pricing: res.pricing }, null, 2)}\n`);
|
|
125
|
+
return { ok: true, pricing: res.pricing };
|
|
126
|
+
}
|
|
127
|
+
renderView(io, fp, res.pricing);
|
|
128
|
+
return { ok: true, pricing: res.pricing };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
makeRateDeps,
|
|
133
|
+
runRate,
|
|
134
|
+
parseAmount,
|
|
135
|
+
buildBody,
|
|
136
|
+
CURRENCIES,
|
|
137
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Local context SUGGESTIONS for the register flow: the environment timezone, the
|
|
4
|
+
// local git identity, the GitHub login and CV candidates. These are proposals the
|
|
5
|
+
// model offers the talent to confirm — they are NEVER auto-written. No file
|
|
6
|
+
// content, no commit scanning, no network, no session: CV candidates are file
|
|
7
|
+
// NAMES only. This is deliberately NOT the authorship/evidence path.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { execFileSync } = require('child_process');
|
|
12
|
+
const { getHomeDir, tildify } = require('./env-paths');
|
|
13
|
+
|
|
14
|
+
// Where a talent keeps a CV, and how deep we look under each folder. iCloud
|
|
15
|
+
// Drive holds Desktop/Documents for many Mac users.
|
|
16
|
+
const CV_DIRS = ['Desktop', 'Documents', 'Downloads', 'Escritorio', 'Documentos', 'Descargas', 'Library/Mobile Documents/com~apple~CloudDocs'];
|
|
17
|
+
const CV_MAX_DEPTH = 3;
|
|
18
|
+
const CV_MAX_RESULTS = 8;
|
|
19
|
+
const CV_EXTENSIONS = /\.(pdf|docx?)$/i;
|
|
20
|
+
const CV_NAME = /(^|[^a-z])(cv|curriculum|curr[ií]culum|resume|r[ée]sum[ée])([^a-z]|$)/i;
|
|
21
|
+
|
|
22
|
+
function detectTimezone() {
|
|
23
|
+
try {
|
|
24
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function gitConfig(key, cwd) {
|
|
31
|
+
try {
|
|
32
|
+
const out = execFileSync('git', ['config', '--get', key], {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
timeout: 5000,
|
|
35
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
36
|
+
cwd: cwd || process.cwd(),
|
|
37
|
+
});
|
|
38
|
+
const value = String(out || '').trim();
|
|
39
|
+
return value || null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The login the GitHub CLI is signed in with, read from its local hosts file
|
|
46
|
+
// (no network), else the conventional `github.user` git key.
|
|
47
|
+
function detectGithubUser(home, cwd) {
|
|
48
|
+
try {
|
|
49
|
+
const hosts = fs.readFileSync(path.join(home, '.config', 'gh', 'hosts.yml'), 'utf8');
|
|
50
|
+
const block = hosts.split(/^github\.com:\s*$/m)[1];
|
|
51
|
+
const match = block && block.match(/^\s+user:\s*(\S+)\s*$/m);
|
|
52
|
+
if (match) return match[1];
|
|
53
|
+
} catch {
|
|
54
|
+
// no gh CLI config
|
|
55
|
+
}
|
|
56
|
+
return gitConfig('github.user', cwd);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fold = (text) => String(text || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
|
60
|
+
|
|
61
|
+
const nameTokens = (personName) => fold(personName).split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
|
|
62
|
+
|
|
63
|
+
// Whether a file name carries a token of the person's name. People who hire keep
|
|
64
|
+
// other people's CVs in Downloads; only a name match makes a candidate theirs.
|
|
65
|
+
function matchesName(fileName, personName) {
|
|
66
|
+
const file = fold(fileName);
|
|
67
|
+
return nameTokens(personName).some((t) => file.includes(t));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A file named after the person but without a CV word (`Alvaro Torro.pdf`) is a
|
|
71
|
+
// candidate only when it carries their full name, so one common first name does
|
|
72
|
+
// not pull in every contract and invoice.
|
|
73
|
+
function carriesFullName(fileName, personName) {
|
|
74
|
+
const tokens = nameTokens(personName);
|
|
75
|
+
const file = fold(fileName);
|
|
76
|
+
return tokens.length >= 2 && tokens.every((t) => file.includes(t));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Words that say nothing about whose CV it is: `cv.pdf`, `Curriculum_2024_ES.pdf`,
|
|
80
|
+
// `resume-final-v2.pdf`. Such a file may be the talent's own, so it is worth asking.
|
|
81
|
+
const GENERIC_WORDS = /\b(cv|curriculum|curriculo|resume|vitae|mi|my|el|the|de|of|final|actualizado|updated|nuevo|new|latest|es|en|esp|eng|spanish|english|espanol|ingles|v\d+|copia|copy)\b/g;
|
|
82
|
+
|
|
83
|
+
function isGenericCvName(fileName) {
|
|
84
|
+
const rest = fold(fileName.replace(/\.pdf$/i, '')).replace(/[^a-z0-9]+/g, ' ').replace(GENERIC_WORDS, ' ').replace(/\d+/g, ' ');
|
|
85
|
+
return rest.trim().length === 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// PDF/Word files whose NAME looks like a CV or carries the person's full name:
|
|
89
|
+
// name matches first, then generic names, then newest. Never opens a file.
|
|
90
|
+
function findCvCandidates(home, personName) {
|
|
91
|
+
// Without a name there is nothing to match: null says unknown, never a mismatch.
|
|
92
|
+
const known = nameTokens(personName).length > 0;
|
|
93
|
+
const found = [];
|
|
94
|
+
const walk = (dir, depth) => {
|
|
95
|
+
let entries;
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (entry.name.startsWith('.')) continue;
|
|
103
|
+
const full = path.join(dir, entry.name);
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
if (depth < CV_MAX_DEPTH) walk(full, depth + 1);
|
|
106
|
+
} else if (entry.isFile() && CV_EXTENSIONS.test(entry.name)
|
|
107
|
+
&& (CV_NAME.test(entry.name.replace(CV_EXTENSIONS, '')) || carriesFullName(entry.name, personName))) {
|
|
108
|
+
try {
|
|
109
|
+
found.push({
|
|
110
|
+
path: full,
|
|
111
|
+
nameMatch: known ? matchesName(entry.name, personName) : null,
|
|
112
|
+
generic: isGenericCvName(entry.name),
|
|
113
|
+
mtimeMs: fs.statSync(full).mtimeMs,
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
// vanished between readdir and stat
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
for (const name of CV_DIRS) walk(path.join(home, name), 1);
|
|
122
|
+
return found
|
|
123
|
+
.sort((a, b) => Number(b.nameMatch) - Number(a.nameMatch) || Number(b.generic) - Number(a.generic) || b.mtimeMs - a.mtimeMs)
|
|
124
|
+
.slice(0, CV_MAX_RESULTS)
|
|
125
|
+
.map(({ path: cvPath, nameMatch, generic }) => ({ path: tildify(cvPath, { SHAKERS_CLI_HOME_DIR: home }), nameMatch, generic }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Returns the signals found, each clearly a SUGGESTION to confirm. `talentName`
|
|
129
|
+
// (what the AI already knows) sharpens the CV match beyond the git name.
|
|
130
|
+
function readRegisterContextSuggestions({ cwd, env = process.env, talentName } = {}) {
|
|
131
|
+
const home = getHomeDir(env);
|
|
132
|
+
const githubUser = detectGithubUser(home, cwd);
|
|
133
|
+
const gitName = gitConfig('user.name', cwd);
|
|
134
|
+
const personName = [talentName, gitName].filter((n) => typeof n === 'string' && n.trim()).join(' ');
|
|
135
|
+
return {
|
|
136
|
+
timezone: detectTimezone(),
|
|
137
|
+
gitName,
|
|
138
|
+
gitEmail: gitConfig('user.email', cwd),
|
|
139
|
+
githubUser,
|
|
140
|
+
githubUrl: githubUser ? `https://github.com/${githubUser}` : null,
|
|
141
|
+
cvCandidates: findCvCandidates(home, personName),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { readRegisterContextSuggestions, detectTimezone, findCvCandidates, detectGithubUser, isGenericCvName, nameTokens, fold };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Renders the "My work with AI" preview (3x3 matrix + vision E + howIWork F +
|
|
4
|
+
// traction), terminal + HTML. Copy is inlined for the spike; promote to i18n later.
|
|
5
|
+
|
|
6
|
+
const { SETUP_CODE, CELL_NAMES } = require('./ai-fluency-cell');
|
|
7
|
+
const { detectedToolNames } = require('./detected-tools');
|
|
8
|
+
|
|
9
|
+
const SETUP_ORDER = ['ASSISTED', 'EXTENDED', 'ORCHESTRATED'];
|
|
10
|
+
const USAGE_ORDER = ['REACTIVE', 'DELIBERATE', 'RIGOROUS'];
|
|
11
|
+
|
|
12
|
+
const COPY = {
|
|
13
|
+
es: {
|
|
14
|
+
title: 'Tu trabajo con IA (Setup x Uso)',
|
|
15
|
+
usage: { REACTIVE: 'Reactivo', DELIBERATE: 'Deliberado', RIGOROUS: 'Riguroso' },
|
|
16
|
+
setup: { ASSISTED: 'Asistido', EXTENDED: 'Extendido', ORCHESTRATED: 'Orquestado' },
|
|
17
|
+
aiNative: 'AI Native',
|
|
18
|
+
pending: 'Tu perfil se está evaluando; míralo en un momento en "Mi trabajo con IA".',
|
|
19
|
+
matrixUpdating: 'Tu matriz Setup x Uso se está actualizando; aparecerá en breve.',
|
|
20
|
+
setupOnly: 'Disponible en tu perfil tras iniciar sesión y compartir el informe.',
|
|
21
|
+
visionLabel: 'Mi visión sobre la IA',
|
|
22
|
+
howIWorkLabel: 'Cómo trabajo con IA',
|
|
23
|
+
howIWorkMissing: 'Se genera tras la entrevista de onboarding.',
|
|
24
|
+
tractionLabel: 'Actividad',
|
|
25
|
+
sessions90d: 'sesiones (90d)',
|
|
26
|
+
daysPerWeek: 'días/semana',
|
|
27
|
+
agents: (d, p) => `${d} agentes detectados${p != null ? ` · ${p} en tu perfil` : ''}`,
|
|
28
|
+
toolsLabel: 'herramientas',
|
|
29
|
+
axisSetup: 'Setup',
|
|
30
|
+
axisUsage: 'Uso',
|
|
31
|
+
agentsLabel: 'Agentes',
|
|
32
|
+
},
|
|
33
|
+
en: {
|
|
34
|
+
title: 'Your work with AI (Setup x Usage)',
|
|
35
|
+
usage: { REACTIVE: 'Reactive', DELIBERATE: 'Deliberate', RIGOROUS: 'Rigorous' },
|
|
36
|
+
setup: { ASSISTED: 'Assisted', EXTENDED: 'Extended', ORCHESTRATED: 'Orchestrated' },
|
|
37
|
+
aiNative: 'AI Native',
|
|
38
|
+
pending: 'Your profile is being evaluated; check "My work with AI" shortly.',
|
|
39
|
+
matrixUpdating: 'Your Setup x Usage matrix is updating; it will appear shortly.',
|
|
40
|
+
setupOnly: 'Available on your profile after you log in and share the report.',
|
|
41
|
+
visionLabel: 'My vision on AI',
|
|
42
|
+
howIWorkLabel: 'How I work with AI',
|
|
43
|
+
howIWorkMissing: 'Generated after the onboarding interview.',
|
|
44
|
+
tractionLabel: 'Traction',
|
|
45
|
+
sessions90d: 'sessions (90d)',
|
|
46
|
+
daysPerWeek: 'days/week',
|
|
47
|
+
agents: (d, p) => `${d} agents detected${p != null ? ` · ${p} on your profile` : ''}`,
|
|
48
|
+
toolsLabel: 'tools',
|
|
49
|
+
axisSetup: 'Setup',
|
|
50
|
+
axisUsage: 'Usage',
|
|
51
|
+
agentsLabel: 'Agents',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function copyFor(lang) {
|
|
56
|
+
return COPY[lang === 'es' ? 'es' : 'en'];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The matrix band can lag behind E/F (independent projection). When absent, render
|
|
60
|
+
// E/F/traction anyway and swap the 3x3 grid for a short "updating" note.
|
|
61
|
+
function matrixReady(matrix) {
|
|
62
|
+
return !!(matrix && (matrix.setupLevel || matrix.usageLevel || matrix.cell));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const noColor = new Proxy({}, { get: () => '' });
|
|
66
|
+
|
|
67
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (ch) =>
|
|
68
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"' }[ch]));
|
|
69
|
+
|
|
70
|
+
// Traction metrics as compact "label: value" fragments, omitting null fields.
|
|
71
|
+
function tractionParts(traction, t) {
|
|
72
|
+
if (!traction) return [];
|
|
73
|
+
const parts = [];
|
|
74
|
+
if (traction.sessions90d != null) parts.push(`${traction.sessions90d} ${t.sessions90d}`);
|
|
75
|
+
if (traction.activeDaysPerWeek != null) parts.push(`${traction.activeDaysPerWeek} ${t.daysPerWeek}`);
|
|
76
|
+
if (traction.agentsDetected != null) parts.push(t.agents(traction.agentsDetected, traction.agentsOnProfile));
|
|
77
|
+
const tools = detectedToolNames(traction.tools);
|
|
78
|
+
if (tools.length) parts.push(`${t.toolsLabel}: ${tools.join(', ')}`);
|
|
79
|
+
return parts;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function matrixTerminalLines(matrix, t, c) {
|
|
83
|
+
// Column widths from the longest label (incl. current-cell brackets). Pad BEFORE
|
|
84
|
+
// colorizing: padEnd counts ANSI escape bytes, so padding a colored string misaligns.
|
|
85
|
+
const rowLabels = SETUP_ORDER.map((s) => `${SETUP_CODE[s]} ${t.setup[s]}`);
|
|
86
|
+
const rowW = Math.max(...rowLabels.map((s) => s.length)) + 2;
|
|
87
|
+
const cellLabels = SETUP_ORDER.flatMap((s) => USAGE_ORDER.map((u) => CELL_NAMES[s][u]));
|
|
88
|
+
const headerLabels = USAGE_ORDER.map((u) => t.usage[u]);
|
|
89
|
+
const colW = Math.max(...cellLabels.map((x) => x.length), ...headerLabels.map((x) => x.length)) + 4;
|
|
90
|
+
|
|
91
|
+
const lines = [` ${c.bold}${t.title}${c.reset}`];
|
|
92
|
+
lines.push(` ${c.dim}${' '.repeat(rowW) + headerLabels.map((u) => u.padEnd(colW)).join('')}${c.reset}`);
|
|
93
|
+
for (const s of SETUP_ORDER) {
|
|
94
|
+
const rowLabel = `${SETUP_CODE[s]} ${t.setup[s]}`.padEnd(rowW);
|
|
95
|
+
const cells = USAGE_ORDER.map((u) => {
|
|
96
|
+
const name = CELL_NAMES[s][u];
|
|
97
|
+
const isCurrent = matrix.setupLevel === s && matrix.usageLevel === u;
|
|
98
|
+
if (isCurrent) return `${c.accent}${c.bold}${`[${name}]`.padEnd(colW)}${c.reset}`;
|
|
99
|
+
return `${c.muted}${name.padEnd(colW)}${c.reset}`;
|
|
100
|
+
}).join('');
|
|
101
|
+
lines.push(` ${c.dim}${rowLabel}${c.reset}${cells}`);
|
|
102
|
+
}
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildProfileSections(preview, t) {
|
|
107
|
+
const sections = [];
|
|
108
|
+
const m = (preview && preview.matrix) || {};
|
|
109
|
+
const setupExp = preview.setup && preview.setup.explanation ? preview.setup.explanation : null;
|
|
110
|
+
if (m.setupLevel) {
|
|
111
|
+
const value = [m.setupCode, t.setup[m.setupLevel]].filter(Boolean).join(' ');
|
|
112
|
+
sections.push({ kind: 'axis', label: t.axisSetup, value, explanation: setupExp });
|
|
113
|
+
}
|
|
114
|
+
const usageExp = preview.usage && preview.usage.explanation ? preview.usage.explanation : null;
|
|
115
|
+
if (m.usageLevel) {
|
|
116
|
+
sections.push({ kind: 'axis', label: t.axisUsage, value: t.usage[m.usageLevel] || m.usageLevel, explanation: usageExp });
|
|
117
|
+
}
|
|
118
|
+
if (preview.vision) sections.push({ kind: 'narrative', label: t.visionLabel, body: preview.vision });
|
|
119
|
+
if (preview.howIWork) sections.push({ kind: 'narrative', label: t.howIWorkLabel, body: preview.howIWork });
|
|
120
|
+
return sections;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const axisBody = (s) => (s.explanation ? `${s.value} — ${s.explanation}` : s.value);
|
|
124
|
+
|
|
125
|
+
// Full "My work with AI" preview, terminal. `preview` is the normalized shape from
|
|
126
|
+
// resolveAiProfilePreview; `status` drives the fallbacks.
|
|
127
|
+
function renderAiProfileTerminal({ status, preview } = {}, { lang = 'en', c = noColor } = {}) {
|
|
128
|
+
const t = copyFor(lang);
|
|
129
|
+
// Non-fatal notices (not-yet-judged / setup-only) → yellow.
|
|
130
|
+
if (status === 'unavailable') return ` ${c.warning || c.muted}${t.setupOnly}${c.reset}`;
|
|
131
|
+
if (status !== 'ready' || !preview) return ` ${c.warning || c.muted}${t.pending}${c.reset}`;
|
|
132
|
+
|
|
133
|
+
const lines = matrixReady(preview.matrix)
|
|
134
|
+
? matrixTerminalLines(preview.matrix, t, c)
|
|
135
|
+
: [` ${c.bold}${t.title}${c.reset}`, ` ${c.warning || c.muted}${t.matrixUpdating}${c.reset}`];
|
|
136
|
+
const sections = buildProfileSections(preview, t);
|
|
137
|
+
const axes = sections.filter((s) => s.kind === 'axis');
|
|
138
|
+
if (axes.length) {
|
|
139
|
+
lines.push('');
|
|
140
|
+
for (const a of axes) lines.push(` ${c.bold}${a.label}:${c.reset} ${c.muted}${axisBody(a)}${c.reset}`);
|
|
141
|
+
}
|
|
142
|
+
for (const n of sections.filter((s) => s.kind === 'narrative')) {
|
|
143
|
+
lines.push('', ` ${c.bold}${n.label}${c.reset}`, ` ${c.muted}${n.body}${c.reset}`);
|
|
144
|
+
}
|
|
145
|
+
// Traction now lives in the Activity section (terminal-agents#printActivity).
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function plainAgentLines(agents) {
|
|
150
|
+
if (!Array.isArray(agents) || agents.length === 0) return [];
|
|
151
|
+
return agents.map((a) => {
|
|
152
|
+
const bits = [a.category, a.role].filter(Boolean);
|
|
153
|
+
return bits.length ? `- ${a.name} — ${bits.join(' · ')}` : `- ${a.name}`;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Full "My work with AI" report as plain markdown (NO ANSI), for the MCP tool so a chat client shows the same report the CLI terminal prints.
|
|
158
|
+
function renderAiProfilePlain({ status, preview } = {}, { lang = 'en', agents = [] } = {}) {
|
|
159
|
+
const t = copyFor(lang);
|
|
160
|
+
const out = [];
|
|
161
|
+
if (status === 'unavailable') {
|
|
162
|
+
out.push(t.setupOnly);
|
|
163
|
+
} else if (status !== 'ready' || !preview) {
|
|
164
|
+
out.push(t.pending);
|
|
165
|
+
} else {
|
|
166
|
+
if (matrixReady(preview.matrix)) {
|
|
167
|
+
const gridLines = matrixTerminalLines(preview.matrix, t, noColor).slice(1);
|
|
168
|
+
out.push(`## ${t.title}`, '', '```', ...gridLines, '```');
|
|
169
|
+
} else {
|
|
170
|
+
out.push(`## ${t.title}`, '', t.matrixUpdating);
|
|
171
|
+
}
|
|
172
|
+
const sections = buildProfileSections(preview, t);
|
|
173
|
+
const axes = sections.filter((s) => s.kind === 'axis');
|
|
174
|
+
if (axes.length) {
|
|
175
|
+
out.push('');
|
|
176
|
+
for (const a of axes) out.push(`${a.label}: ${axisBody(a)}`);
|
|
177
|
+
}
|
|
178
|
+
for (const n of sections.filter((s) => s.kind === 'narrative')) {
|
|
179
|
+
out.push('', `**${n.label}**`, n.body);
|
|
180
|
+
}
|
|
181
|
+
const tp = tractionParts(preview.traction, t);
|
|
182
|
+
if (tp.length) out.push('', `**${t.tractionLabel}:** ${tp.join(' · ')}`);
|
|
183
|
+
}
|
|
184
|
+
const agentLines = plainAgentLines(agents);
|
|
185
|
+
if (agentLines.length) out.push('', `**${t.agentsLabel}**`, ...agentLines);
|
|
186
|
+
return out.join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Full "My work with AI" preview, HTML. '' when there is nothing to show inline.
|
|
190
|
+
function renderAiProfileHtml({ status, preview } = {}, { lang = 'en' } = {}) {
|
|
191
|
+
const t = copyFor(lang);
|
|
192
|
+
if (status === 'unavailable') return `<p class="ai-fluency-note">${esc(t.setupOnly)}</p>`;
|
|
193
|
+
if (status !== 'ready' || !preview) return `<p class="ai-fluency-note">${esc(t.pending)}</p>`;
|
|
194
|
+
|
|
195
|
+
const m = preview.matrix;
|
|
196
|
+
const out = [
|
|
197
|
+
`<section class="ai-fluency" aria-label="${esc(t.title)}">`,
|
|
198
|
+
`<h3>${esc(t.title)}</h3>`,
|
|
199
|
+
];
|
|
200
|
+
if (matrixReady(m)) {
|
|
201
|
+
const rows = SETUP_ORDER.map((s) => {
|
|
202
|
+
const cells = USAGE_ORDER.map((u) => {
|
|
203
|
+
const current = m.setupLevel === s && m.usageLevel === u;
|
|
204
|
+
return `<td class="ai-fluency-cell${current ? ' is-current' : ''}">${esc(CELL_NAMES[s][u])}</td>`;
|
|
205
|
+
}).join('');
|
|
206
|
+
return `<tr><th scope="row">${esc(SETUP_CODE[s])} ${esc(t.setup[s])}</th>${cells}</tr>`;
|
|
207
|
+
}).join('');
|
|
208
|
+
const head = USAGE_ORDER.map((u) => `<th scope="col">${esc(t.usage[u])}</th>`).join('');
|
|
209
|
+
out.push(
|
|
210
|
+
'<table class="ai-fluency-matrix">',
|
|
211
|
+
`<thead><tr><th></th>${head}</tr></thead>`,
|
|
212
|
+
`<tbody>${rows}</tbody>`,
|
|
213
|
+
'</table>',
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
out.push(`<p class="ai-fluency-note">${esc(t.matrixUpdating)}</p>`);
|
|
217
|
+
}
|
|
218
|
+
if (m.cell) {
|
|
219
|
+
out.push(`<p class="ai-fluency-current">${esc(m.setupCode)}·${esc(t.usage[m.usageLevel] || m.usageLevel)} → <strong>${esc(m.cell)}</strong>${m.isAiNative ? ` <span class="ai-native">★ ${esc(t.aiNative)}</span>` : ''}</p>`);
|
|
220
|
+
}
|
|
221
|
+
if (preview.vision) {
|
|
222
|
+
out.push(`<div class="ai-vision"><h4>${esc(t.visionLabel)}</h4><p>${esc(preview.vision)}</p></div>`);
|
|
223
|
+
}
|
|
224
|
+
out.push(`<div class="ai-howiwork"><h4>${esc(t.howIWorkLabel)}</h4><p>${preview.howIWork ? esc(preview.howIWork) : `<em>${esc(t.howIWorkMissing)}</em>`}</p></div>`);
|
|
225
|
+
const parts = tractionParts(preview.traction, t);
|
|
226
|
+
if (parts.length) {
|
|
227
|
+
out.push(`<p class="ai-traction"><strong>${esc(t.tractionLabel)}:</strong> ${esc(parts.join(' · '))}</p>`);
|
|
228
|
+
}
|
|
229
|
+
out.push('</section>');
|
|
230
|
+
return out.join('');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Matrix-only HTML (kept for render-sheet.js; the HTML report is a separate iteration).
|
|
234
|
+
function renderAiFluencyMatrixHtml({ status, usage } = {}, { lang = 'en' } = {}) {
|
|
235
|
+
const t = copyFor(lang);
|
|
236
|
+
if (status === 'unavailable') return `<p class="ai-fluency-note">${esc(t.setupOnly)}</p>`;
|
|
237
|
+
if (status !== 'ready' || !usage || !usage.cell) return `<p class="ai-fluency-note">${esc(t.pending)}</p>`;
|
|
238
|
+
const rows = SETUP_ORDER.map((s) => {
|
|
239
|
+
const cells = USAGE_ORDER.map((u) => {
|
|
240
|
+
const current = usage.setupLevel === s && usage.usageLevel === u;
|
|
241
|
+
return `<td class="ai-fluency-cell${current ? ' is-current' : ''}">${esc(CELL_NAMES[s][u])}</td>`;
|
|
242
|
+
}).join('');
|
|
243
|
+
return `<tr><th scope="row">${esc(SETUP_CODE[s])} ${esc(t.setup[s])}</th>${cells}</tr>`;
|
|
244
|
+
}).join('');
|
|
245
|
+
const head = USAGE_ORDER.map((u) => `<th scope="col">${esc(t.usage[u])}</th>`).join('');
|
|
246
|
+
return [
|
|
247
|
+
`<section class="ai-fluency" aria-label="${esc(t.title)}">`,
|
|
248
|
+
`<h3>${esc(t.title)}</h3>`,
|
|
249
|
+
'<table class="ai-fluency-matrix">',
|
|
250
|
+
`<thead><tr><th></th>${head}</tr></thead>`,
|
|
251
|
+
`<tbody>${rows}</tbody></table>`,
|
|
252
|
+
`<p class="ai-fluency-current">${esc(usage.setupCode)}·${esc(t.usage[usage.usageLevel] || usage.usageLevel)} → <strong>${esc(usage.cell)}</strong>${usage.isAiNative ? ` <span class="ai-native">★ ${esc(t.aiNative)}</span>` : ''}</p>`,
|
|
253
|
+
'</section>',
|
|
254
|
+
].join('');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
renderAiProfileTerminal,
|
|
259
|
+
renderAiProfilePlain,
|
|
260
|
+
renderAiProfileHtml,
|
|
261
|
+
renderAiFluencyMatrixHtml,
|
|
262
|
+
copyFor,
|
|
263
|
+
tractionParts,
|
|
264
|
+
SETUP_ORDER,
|
|
265
|
+
USAGE_ORDER,
|
|
266
|
+
};
|