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.
Files changed (237) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1233 -2
  3. package/bin/add-agent.js +88 -0
  4. package/bin/add-project.js +88 -0
  5. package/bin/add-role.js +61 -0
  6. package/bin/add-skill.js +88 -0
  7. package/bin/ai-usage.js +99 -0
  8. package/bin/alma.js +99 -0
  9. package/bin/applications.js +69 -0
  10. package/bin/availability.js +70 -0
  11. package/bin/certifications.js +69 -0
  12. package/bin/certify.js +73 -0
  13. package/bin/change-role.js +61 -0
  14. package/bin/config.js +124 -0
  15. package/bin/experiences.js +69 -0
  16. package/bin/find-projects.js +89 -0
  17. package/bin/invitations.js +69 -0
  18. package/bin/lang.js +70 -0
  19. package/bin/login.js +216 -0
  20. package/bin/mcp.js +124 -0
  21. package/bin/mcpb.js +5 -0
  22. package/bin/me.js +69 -0
  23. package/bin/onboarding.js +134 -0
  24. package/bin/portfolios.js +69 -0
  25. package/bin/rate.js +70 -0
  26. package/bin/register.js +337 -0
  27. package/bin/report-html.js +134 -0
  28. package/bin/save.js +77 -0
  29. package/bin/shakers.js +363 -0
  30. package/bin/share.js +72 -0
  31. package/bin/show-project.js +72 -0
  32. package/bin/socials.js +70 -0
  33. package/bin/superadmin.js +351 -0
  34. package/bin/update-check-worker.js +70 -0
  35. package/bin/update.js +111 -0
  36. package/package.json +40 -4
  37. package/src/agent-category.js +13 -0
  38. package/src/agent-evaluation.js +177 -0
  39. package/src/agent-org-chart.js +355 -0
  40. package/src/agent-synthesis.js +132 -0
  41. package/src/agent-usage.js +118 -0
  42. package/src/agents-client.js +165 -0
  43. package/src/ai-fluency-cell.js +72 -0
  44. package/src/ai-profile-client.js +45 -0
  45. package/src/ai-profile-preview.js +154 -0
  46. package/src/ai.js +66 -0
  47. package/src/alma-client.js +123 -0
  48. package/src/alma-flow.js +32 -0
  49. package/src/alma-repo-context.js +47 -0
  50. package/src/alma-shell.js +161 -0
  51. package/src/ansi.js +62 -0
  52. package/src/applications-client.js +79 -0
  53. package/src/applications-flow.js +58 -0
  54. package/src/auth-client.js +144 -0
  55. package/src/auth-device-client.js +118 -0
  56. package/src/auth-method-picker.js +44 -0
  57. package/src/auth-methods.js +35 -0
  58. package/src/auth-session-store.js +215 -0
  59. package/src/authorship-mismatch.js +62 -0
  60. package/src/authorship.js +204 -0
  61. package/src/automations-detector.js +170 -0
  62. package/src/availability-client.js +93 -0
  63. package/src/availability-flow.js +124 -0
  64. package/src/backend-request.js +191 -0
  65. package/src/blind-approval-extract.js +48 -0
  66. package/src/brand-ansi.js +14 -0
  67. package/src/brand-svg.js +9 -0
  68. package/src/browser-tools-detector.js +21 -0
  69. package/src/build-next-level.js +56 -0
  70. package/src/certifications-client.js +89 -0
  71. package/src/certifications-flow.js +75 -0
  72. package/src/certify-dimension-client.js +163 -0
  73. package/src/certify-dimension-flow.js +135 -0
  74. package/src/certify-disclaimer.js +51 -0
  75. package/src/certify-remediation-prompt.js +23 -0
  76. package/src/cli-args.js +83 -0
  77. package/src/command-audit.js +105 -0
  78. package/src/config-dir.js +120 -0
  79. package/src/config.js +877 -0
  80. package/src/consent-flow.js +115 -0
  81. package/src/consent-skip.js +27 -0
  82. package/src/cv-file.js +62 -0
  83. package/src/cv-text.js +92 -0
  84. package/src/decisions-extract.js +43 -0
  85. package/src/detect-repos.js +145 -0
  86. package/src/detected-tools.js +15 -0
  87. package/src/detection-evidence.js +105 -0
  88. package/src/detection-noise.js +132 -0
  89. package/src/detectors.js +221 -0
  90. package/src/device-auth-store.js +100 -0
  91. package/src/device-login.js +120 -0
  92. package/src/email-verification.js +152 -0
  93. package/src/env-paths.js +30 -0
  94. package/src/find-projects-cache.js +89 -0
  95. package/src/find-projects-client.js +148 -0
  96. package/src/find-projects-flow.js +164 -0
  97. package/src/git-activity.js +124 -0
  98. package/src/git-log-prefetch.js +53 -0
  99. package/src/graph-certs.js +58 -0
  100. package/src/graph-generator.js +230 -0
  101. package/src/graph-scan.js +239 -0
  102. package/src/i18n.js +3341 -0
  103. package/src/inter-fonts.js +6 -0
  104. package/src/interactive-select.js +183 -0
  105. package/src/interview-client.js +226 -0
  106. package/src/interview-transcript.js +56 -0
  107. package/src/inventory-client.js +90 -0
  108. package/src/invitations-client.js +66 -0
  109. package/src/invitations-flow.js +50 -0
  110. package/src/lang-client.js +147 -0
  111. package/src/lang-codes.js +14 -0
  112. package/src/lang-flow.js +85 -0
  113. package/src/legal-notice.js +53 -0
  114. package/src/locale.js +72 -0
  115. package/src/markdown-ansi.js +50 -0
  116. package/src/maturity.js +111 -0
  117. package/src/mcp-ai-usage-result-tool.js +111 -0
  118. package/src/mcp-ai-usage-tool.js +401 -0
  119. package/src/mcp-alma-tools.js +95 -0
  120. package/src/mcp-applications-tools.js +44 -0
  121. package/src/mcp-auth-form.js +480 -0
  122. package/src/mcp-availability-tools.js +45 -0
  123. package/src/mcp-certifications-tools.js +44 -0
  124. package/src/mcp-certify-dimension-tools.js +66 -0
  125. package/src/mcp-choice.js +95 -0
  126. package/src/mcp-detector.js +223 -0
  127. package/src/mcp-device-login-tools.js +177 -0
  128. package/src/mcp-find-projects-tools.js +69 -0
  129. package/src/mcp-install.js +275 -0
  130. package/src/mcp-instructions.js +27 -0
  131. package/src/mcp-invitations-tools.js +44 -0
  132. package/src/mcp-lang-tools.js +41 -0
  133. package/src/mcp-onboarding-tools.js +379 -0
  134. package/src/mcp-portfolios-tools.js +50 -0
  135. package/src/mcp-profile-tools.js +48 -0
  136. package/src/mcp-prompts.js +73 -0
  137. package/src/mcp-rate-tools.js +44 -0
  138. package/src/mcp-register-tools.js +441 -0
  139. package/src/mcp-report-tools.js +105 -0
  140. package/src/mcp-role-tools.js +372 -0
  141. package/src/mcp-saved-positions-tools.js +59 -0
  142. package/src/mcp-server.js +290 -0
  143. package/src/mcp-show-project-tools.js +52 -0
  144. package/src/mcp-signup-client.js +118 -0
  145. package/src/mcp-signup-tools.js +1171 -0
  146. package/src/mcp-socials-tools.js +41 -0
  147. package/src/memory-structure-detector.js +108 -0
  148. package/src/model-call-record.js +198 -0
  149. package/src/onboarding-availability.js +26 -0
  150. package/src/onboarding-client.js +374 -0
  151. package/src/onboarding-flow.js +1162 -0
  152. package/src/onboarding-funnel.js +272 -0
  153. package/src/onboarding-livekit-client.js +102 -0
  154. package/src/onboarding-status-client.js +57 -0
  155. package/src/open-file.js +32 -0
  156. package/src/osc-link.js +16 -0
  157. package/src/portfolio-client.js +164 -0
  158. package/src/portfolio-git-info.js +61 -0
  159. package/src/portfolios-client.js +85 -0
  160. package/src/portfolios-flow.js +79 -0
  161. package/src/profile-client.js +133 -0
  162. package/src/profile-flow.js +115 -0
  163. package/src/profile-switch.js +46 -0
  164. package/src/prompt-select.js +66 -0
  165. package/src/rate-client.js +81 -0
  166. package/src/rate-flow.js +137 -0
  167. package/src/register-context.js +145 -0
  168. package/src/render-ai-fluency.js +266 -0
  169. package/src/render-certification.js +177 -0
  170. package/src/render-html.js +140 -0
  171. package/src/render-report-text.js +27 -0
  172. package/src/render-sheet.js +261 -0
  173. package/src/render-terminal.js +109 -0
  174. package/src/render-usage-report-text.js +155 -0
  175. package/src/repo-context.js +215 -0
  176. package/src/report-gating.js +49 -0
  177. package/src/report-store.js +226 -0
  178. package/src/report-theme.js +177 -0
  179. package/src/report-view-client.js +50 -0
  180. package/src/reveal-output.js +29 -0
  181. package/src/roadmap-content.js +453 -0
  182. package/src/roadmap-personalization.js +132 -0
  183. package/src/roadmap-prompt.js +154 -0
  184. package/src/roles-client.js +112 -0
  185. package/src/roles-flow.js +192 -0
  186. package/src/sanitize-network-text.js +36 -0
  187. package/src/saved-positions-client.js +71 -0
  188. package/src/saved-positions-flow.js +50 -0
  189. package/src/scan-exclusions.js +50 -0
  190. package/src/scanner.js +624 -0
  191. package/src/scope-signals.js +148 -0
  192. package/src/secret-prompt.js +72 -0
  193. package/src/session-gate.js +30 -0
  194. package/src/session-normalizers.js +174 -0
  195. package/src/session-refresh.js +51 -0
  196. package/src/session-scan.js +475 -0
  197. package/src/session-signals.js +36 -0
  198. package/src/shakers-wordmark.js +17 -0
  199. package/src/share-card.js +362 -0
  200. package/src/share-certification.js +111 -0
  201. package/src/share-consent-state.js +157 -0
  202. package/src/share-payload.js +300 -0
  203. package/src/share.js +185 -0
  204. package/src/show-project-client.js +58 -0
  205. package/src/show-project-flow.js +119 -0
  206. package/src/signup-client.js +103 -0
  207. package/src/signup-copy.js +450 -0
  208. package/src/signup-language.js +21 -0
  209. package/src/skills-client.js +102 -0
  210. package/src/socials-client.js +91 -0
  211. package/src/socials-flow.js +83 -0
  212. package/src/specificity-extract.js +41 -0
  213. package/src/start-add-agents.js +306 -0
  214. package/src/start-add-portfolio.js +388 -0
  215. package/src/start-add-skills.js +260 -0
  216. package/src/stdin-ask.js +129 -0
  217. package/src/steering-extract.js +43 -0
  218. package/src/strip-internal-comments.js +45 -0
  219. package/src/tech-detector.js +295 -0
  220. package/src/templates/report-sheet.html +572 -0
  221. package/src/terminal-agents.js +207 -0
  222. package/src/terminal-format.js +85 -0
  223. package/src/terminal-progress.js +106 -0
  224. package/src/terminal-sections.js +219 -0
  225. package/src/tier-analysis.js +126 -0
  226. package/src/tier-engine.js +114 -0
  227. package/src/tier-projection.js +117 -0
  228. package/src/transcript-turns.js +49 -0
  229. package/src/translations-client.js +84 -0
  230. package/src/typewriter.js +154 -0
  231. package/src/update-flow.js +223 -0
  232. package/src/update-notifier.js +113 -0
  233. package/src/usage-model-calls.js +119 -0
  234. package/src/usage-oneshot-commands.js +180 -0
  235. package/src/usage-repo-scope.js +162 -0
  236. package/src/usage-run-steps.js +354 -0
  237. package/src/work-streams.js +79 -0
@@ -0,0 +1,204 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+ const path = require('path');
5
+
6
+ // Git AUTHORSHIP + provenance for the certify phase (skill-code-certification, ADR-017).
7
+
8
+ const { takePrefetchedGitLog } = require('./git-log-prefetch');
9
+
10
+ const GIT_TIMEOUT_MS = 15000;
11
+ const SHORT_SHA_LEN = 10;
12
+
13
+ const AUTHORSHIP_LOG_ARGS = ['log', '--no-merges', '--pretty=format:\x01%ae\x1f%at', '--name-only'];
14
+
15
+ function runGit(root, args) {
16
+ try {
17
+ return execFileSync('git', ['-C', root, ...args], {
18
+ encoding: 'utf8',
19
+ timeout: GIT_TIMEOUT_MS,
20
+ stdio: ['ignore', 'pipe', 'ignore'],
21
+ maxBuffer: 64 * 1024 * 1024,
22
+ });
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ // True only inside a real work tree. Everything else (no git, bare repo, not a
29
+ // repo) is treated as "cannot attribute" upstream.
30
+ function isGitRepo(root) {
31
+ return runGit(root, ['rev-parse', '--is-inside-work-tree']) !== null;
32
+ }
33
+
34
+ // Repo identity for the evidence record.
35
+ function getRepository(root) {
36
+ const remote = runGit(root, ['config', '--get', 'remote.origin.url']);
37
+ if (remote && remote.trim()) return normalizeRemote(remote.trim());
38
+ const top = runGit(root, ['rev-parse', '--show-toplevel']);
39
+ if (top && top.trim()) return path.basename(top.trim());
40
+ return null;
41
+ }
42
+
43
+ function normalizeRemote(url) {
44
+ // `git@host:owner/repo.git` | `https://[token@]host/owner/repo.git` ->
45
+ // `host/owner/repo`. Any userinfo (a token/password) is dropped, never kept.
46
+ let s = url.replace(/\.git$/, '');
47
+ const scp = s.match(/^[^@]+@([^:]+):(.+)$/); // scp-like ssh
48
+ if (scp) return `${scp[1]}/${scp[2]}`;
49
+ try {
50
+ const u = new URL(s);
51
+ return `${u.host}${u.pathname}`.replace(/\/+$/, '');
52
+ } catch {
53
+ return s;
54
+ }
55
+ }
56
+
57
+ // `<root-commit>..<HEAD>` short shas over the current branch. `null` when the
58
+ // history can't be read (e.g. a repo with no commits yet).
59
+ function getCommitRange(root) {
60
+ const head = runGit(root, ['rev-parse', `--short=${SHORT_SHA_LEN}`, 'HEAD']);
61
+ if (!head || !head.trim()) return null;
62
+ const roots = runGit(root, ['rev-list', '--max-parents=0', 'HEAD']);
63
+ const firstFull = roots && roots.trim() ? roots.trim().split('\n').pop().trim() : null;
64
+ if (!firstFull) return head.trim();
65
+ const first = runGit(root, ['rev-parse', `--short=${SHORT_SHA_LEN}`, firstFull]);
66
+ const firstShort = first && first.trim() ? first.trim() : firstFull.slice(0, SHORT_SHA_LEN);
67
+ return `${firstShort}..${head.trim()}`;
68
+ }
69
+
70
+ // The path git sees for a file is repo-relative; the sampler's paths are relative to `root`, which may be a SUBDIR of the repo.
71
+ function getRootPrefix(root) {
72
+ const prefix = runGit(root, ['rev-parse', '--show-prefix']);
73
+ return prefix && prefix.trim() ? prefix.trim().replace(/\/+$/, '') : '';
74
+ }
75
+
76
+ function buildPathIndex(root) {
77
+ const authorsByPath = new Map();
78
+ const timeByPath = new Map();
79
+ const prefetched = takePrefetchedGitLog(root, AUTHORSHIP_LOG_ARGS);
80
+ const log = prefetched !== undefined ? prefetched : runGit(root, AUTHORSHIP_LOG_ARGS);
81
+ if (log === null) return { authorsByPath, timeByPath };
82
+
83
+ let currentEmail = null;
84
+ let currentTime = null;
85
+ for (const rawLine of log.split('\n')) {
86
+ if (rawLine.startsWith('\x01')) {
87
+ const [email, atRaw] = rawLine.slice(1).split('\x1f');
88
+ currentEmail = String(email || '').trim().toLowerCase();
89
+ const ts = Number.parseInt(atRaw, 10);
90
+ currentTime = Number.isFinite(ts) ? ts : null;
91
+ continue;
92
+ }
93
+ const file = rawLine.trim();
94
+ if (!file || currentEmail === null) continue;
95
+ let set = authorsByPath.get(file);
96
+ if (!set) {
97
+ set = new Set();
98
+ authorsByPath.set(file, set);
99
+ }
100
+ set.add(currentEmail);
101
+ if (currentTime !== null) {
102
+ const prev = timeByPath.get(file);
103
+ if (prev === undefined || currentTime > prev) {
104
+ timeByPath.set(file, currentTime);
105
+ }
106
+ }
107
+ }
108
+ return { authorsByPath, timeByPath };
109
+ }
110
+
111
+ // Full authorship context for `root`, collected ONCE per run.
112
+ function collectAuthorship(root) {
113
+ if (!isGitRepo(root)) {
114
+ return {
115
+ available: false,
116
+ repository: null,
117
+ commitRange: null,
118
+ authorsForPath: () => [],
119
+ mostRecentCommitTime: () => null,
120
+ };
121
+ }
122
+
123
+ const prefix = getRootPrefix(root);
124
+ const { authorsByPath, timeByPath } = buildPathIndex(root);
125
+
126
+ return {
127
+ available: true,
128
+ repository: getRepository(root),
129
+ commitRange: getCommitRange(root),
130
+ authorsForPath(relPathFromRoot) {
131
+ if (typeof relPathFromRoot !== 'string' || !relPathFromRoot) return [];
132
+ const repoRel = prefix ? `${prefix}/${relPathFromRoot}` : relPathFromRoot;
133
+ const set = authorsByPath.get(repoRel);
134
+ return set ? [...set] : [];
135
+ },
136
+ // UNIX seconds of the most recent commit touching `relPathFromRoot`, or `null` when the path is untracked / history is unreadable.
137
+ mostRecentCommitTime(relPathFromRoot) {
138
+ if (typeof relPathFromRoot !== 'string' || !relPathFromRoot) return null;
139
+ const repoRel = prefix ? `${prefix}/${relPathFromRoot}` : relPathFromRoot;
140
+ const t = timeByPath.get(repoRel);
141
+ return t === undefined ? null : t;
142
+ },
143
+ };
144
+ }
145
+
146
+ // Builds an "is this author attributable?" predicate for one certifying identity.
147
+ function buildAttributionPredicate(verifiedEmail, authorizedSet) {
148
+ const target = String(verifiedEmail || '').trim().toLowerCase();
149
+ const domainSuffix =
150
+ authorizedSet && authorizedSet.domain
151
+ ? `@${String(authorizedSet.domain).trim().toLowerCase()}`
152
+ : null;
153
+ const extra = new Set(
154
+ authorizedSet && Array.isArray(authorizedSet.extraEmails)
155
+ ? authorizedSet.extraEmails.map((e) => String(e).trim().toLowerCase())
156
+ : [],
157
+ );
158
+ return (author) => {
159
+ const a = String(author || '').toLowerCase();
160
+ if (target && a === target) return true;
161
+ if (domainSuffix && a.endsWith(domainSuffix)) return true;
162
+ if (extra.has(a)) return true;
163
+ return false;
164
+ };
165
+ }
166
+
167
+ // Applies the ADR-017 gate (with the ADR-023 widening for test identities) to ONE sampled Skill.
168
+ function attributeSample(sample, verifiedEmail, authorship, authorizedSet = null) {
169
+ const isAttributable = buildAttributionPredicate(verifiedEmail, authorizedSet);
170
+ const files = Array.isArray(sample.files) ? sample.files : [];
171
+
172
+ const consideredEmails = new Set();
173
+ const attributableFiles = [];
174
+ const fileAttribution = [];
175
+
176
+ for (const file of files) {
177
+ const authors = authorship.authorsForPath(file.path);
178
+ for (const a of authors) consideredEmails.add(a);
179
+ const attributed = authors.some((a) => isAttributable(a));
180
+ if (attributed) attributableFiles.push(file);
181
+ fileAttribution.push({ path: file.path, authors, attributed });
182
+ }
183
+
184
+ const authorEmails = [...consideredEmails].map((email) => ({
185
+ email,
186
+ matched: isAttributable(email),
187
+ }));
188
+
189
+ return {
190
+ attributableFiles,
191
+ authorEmails,
192
+ fileAttribution,
193
+ certifiable: attributableFiles.length > 0,
194
+ };
195
+ }
196
+
197
+ module.exports = {
198
+ collectAuthorship,
199
+ AUTHORSHIP_LOG_ARGS,
200
+ attributeSample,
201
+ // exported for unit tests
202
+ normalizeRemote,
203
+ isGitRepo,
204
+ };
@@ -0,0 +1,170 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execFileSync } = require('child_process');
6
+ const { getHomeDir } = require('./env-paths');
7
+
8
+ // Deterministic (no-LLM) automations detector (talents-ai-score, issue 017 / ADR-013-014).
9
+
10
+ const AI_CLI_NAMES = ['claude', 'aider', 'gemini', 'codex'];
11
+ const AI_CLI_RE = new RegExp(`\\b(${AI_CLI_NAMES.join('|')})\\b`, 'i');
12
+
13
+ function mentionsAiCli(text) {
14
+ return typeof text === 'string' && AI_CLI_RE.test(text);
15
+ }
16
+
17
+ function looksLikeJsonPiping(text) {
18
+ if (typeof text !== 'string') return false;
19
+ return mentionsAiCli(text) && /\|/.test(text) && /(--json\b|(^|\s)-p\b)/.test(text);
20
+ }
21
+
22
+ const MAX_SHELL_SCRIPT_BYTES = 200_000; // safety cap, never a product requirement
23
+ const MAX_SHELL_SCRIPTS = 50;
24
+
25
+ /* ---------- 1 & 2: scripts (npm + shell) ---------- */
26
+
27
+ function scanNpmScripts(root) {
28
+ let raw;
29
+ try {
30
+ raw = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
31
+ } catch {
32
+ return { aiMentions: 0, jsonPiping: 0 };
33
+ }
34
+ let pkg;
35
+ try {
36
+ pkg = JSON.parse(raw);
37
+ } catch {
38
+ return { aiMentions: 0, jsonPiping: 0 };
39
+ }
40
+ const scripts = pkg && typeof pkg.scripts === 'object' ? pkg.scripts : {};
41
+ let aiMentions = 0;
42
+ let jsonPiping = 0;
43
+ for (const command of Object.values(scripts)) {
44
+ if (mentionsAiCli(command)) aiMentions += 1;
45
+ if (looksLikeJsonPiping(command)) jsonPiping += 1;
46
+ }
47
+ return { aiMentions, jsonPiping };
48
+ }
49
+
50
+ function scanShellScripts(root) {
51
+ const dir = path.join(root, 'scripts');
52
+ let entries;
53
+ try {
54
+ entries = fs.readdirSync(dir, { withFileTypes: true });
55
+ } catch {
56
+ return { aiMentions: 0, jsonPiping: 0 };
57
+ }
58
+ let aiMentions = 0;
59
+ let jsonPiping = 0;
60
+ let scanned = 0;
61
+ for (const entry of entries) {
62
+ if (scanned >= MAX_SHELL_SCRIPTS) break;
63
+ if (!entry.isFile() || !entry.name.endsWith('.sh')) continue;
64
+ scanned += 1;
65
+ const file = path.join(dir, entry.name);
66
+ let content;
67
+ try {
68
+ const st = fs.statSync(file);
69
+ if (st.size > MAX_SHELL_SCRIPT_BYTES) continue;
70
+ content = fs.readFileSync(file, 'utf8');
71
+ } catch {
72
+ continue;
73
+ }
74
+ if (mentionsAiCli(content)) aiMentions += 1;
75
+ if (looksLikeJsonPiping(content)) jsonPiping += 1;
76
+ }
77
+ return { aiMentions, jsonPiping };
78
+ }
79
+
80
+ /* ---------- 3: schedulers (inspected-where-possible) ---------- */
81
+
82
+ // cron: the CURRENT user's own crontab only (`crontab -l`), never the system-wide crontab (would need elevated permissions, not attempted).
83
+ function probeCron() {
84
+ let out;
85
+ try {
86
+ out = execFileSync('crontab', ['-l'], { timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }).toString('utf8');
87
+ } catch {
88
+ return { inspected: false, matches: 0 }; // no crontab / binary missing / not permitted: not invented
89
+ }
90
+ const lines = out.split('\n').filter((l) => l.trim() && !l.trim().startsWith('#'));
91
+ return { inspected: true, matches: lines.filter(mentionsAiCli).length };
92
+ }
93
+
94
+ // launchd (macOS): the user's own ~/Library/LaunchAgents only (never LaunchDaemons, system-wide, needs root).
95
+ function probeLaunchd(home) {
96
+ const dir = path.join(home, 'Library', 'LaunchAgents');
97
+ let entries;
98
+ try {
99
+ entries = fs.readdirSync(dir, { withFileTypes: true });
100
+ } catch {
101
+ return { inspected: false, matches: 0 };
102
+ }
103
+ let matches = 0;
104
+ for (const entry of entries) {
105
+ if (!entry.isFile() || !entry.name.endsWith('.plist')) continue;
106
+ try {
107
+ const content = fs.readFileSync(path.join(dir, entry.name), 'utf8');
108
+ if (mentionsAiCli(content)) matches += 1;
109
+ } catch {
110
+ /* unreadable: skip, never invented */
111
+ }
112
+ }
113
+ return { inspected: true, matches };
114
+ }
115
+
116
+ // pm2: the user's own process dump (~/.pm2/dump.pm2, JSON). Counts AI-CLI
117
+ // mentions across the whole dump — coarse but derived, never returns it.
118
+ function probePm2(home) {
119
+ let raw;
120
+ try {
121
+ raw = fs.readFileSync(path.join(home, '.pm2', 'dump.pm2'), 'utf8');
122
+ } catch {
123
+ return { inspected: false, matches: 0 };
124
+ }
125
+ const matches = (raw.match(new RegExp(AI_CLI_RE.source, 'gi')) || []).length;
126
+ return { inspected: true, matches };
127
+ }
128
+
129
+ // systemd (Linux, user-level only): ~/.config/systemd/user/*.service.
130
+ // Never the system-wide /etc/systemd (needs root, not attempted).
131
+ function probeSystemdUser(home) {
132
+ const dir = path.join(home, '.config', 'systemd', 'user');
133
+ let entries;
134
+ try {
135
+ entries = fs.readdirSync(dir, { withFileTypes: true });
136
+ } catch {
137
+ return { inspected: false, matches: 0 };
138
+ }
139
+ let matches = 0;
140
+ for (const entry of entries) {
141
+ if (!entry.isFile() || !entry.name.endsWith('.service')) continue;
142
+ try {
143
+ const content = fs.readFileSync(path.join(dir, entry.name), 'utf8');
144
+ if (mentionsAiCli(content)) matches += 1;
145
+ } catch {
146
+ /* unreadable: skip, never invented */
147
+ }
148
+ }
149
+ return { inspected: true, matches };
150
+ }
151
+
152
+ // Deterministic (no-LLM) automations detection.
153
+ function detectAutomations(root) {
154
+ const home = getHomeDir();
155
+ const npm = scanNpmScripts(root);
156
+ const shell = scanShellScripts(root);
157
+
158
+ return {
159
+ scripts: { npm: npm.aiMentions, shell: shell.aiMentions },
160
+ jsonPiping: npm.jsonPiping + shell.jsonPiping,
161
+ schedulers: {
162
+ cron: probeCron(),
163
+ launchd: probeLaunchd(home),
164
+ pm2: probePm2(home),
165
+ systemd: probeSystemdUser(home),
166
+ },
167
+ };
168
+ }
169
+
170
+ module.exports = { detectAutomations, mentionsAiCli, looksLikeJsonPiping };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ // Availability client. VIEW = GET `works/me/availability`; SET = PUT
4
+ // (the same endpoint register uses).
5
+
6
+ const { postJsonWithTimeout } = require('./backend-request');
7
+ const { reasonForError } = require('./onboarding-client');
8
+ const { WORK_MODES } = require('./onboarding-flow');
9
+
10
+ const DEFAULT_TIMEOUT_MS = 20000;
11
+
12
+ function parseJson(raw) {
13
+ try {
14
+ return raw ? JSON.parse(raw) : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function bearerHeaders(accessToken) {
21
+ const headers = {};
22
+ if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
23
+ return headers;
24
+ }
25
+
26
+ function bool(v) {
27
+ return v === true ? true : v === false ? false : null;
28
+ }
29
+
30
+ function str(v) {
31
+ return typeof v === 'string' && v ? v : null;
32
+ }
33
+
34
+ function normalizeAvailability(data) {
35
+ const it = data && typeof data === 'object' ? data : {};
36
+ return {
37
+ available: bool(it.available),
38
+ // A numeric hours value (incl. 0) must survive, not be dropped as "absent".
39
+ monthlyHours: typeof it.monthlyHours === 'number' ? String(it.monthlyHours) : str(it.monthlyHours),
40
+ workModes: Array.isArray(it.workModes) ? it.workModes.filter((m) => WORK_MODES.includes(m)) : [],
41
+ country: str(it.country),
42
+ subdivision: str(it.subdivision),
43
+ timezone: str(it.timezone),
44
+ city: typeof it.city === 'number' ? it.city : null,
45
+ longFullTimeProjects: bool(it.longFullTimeProjects),
46
+ availabilityExpiringDate: it.availabilityExpiringDate ? String(it.availabilityExpiringDate) : null,
47
+ };
48
+ }
49
+
50
+ async function requestAvailability({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
51
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
52
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
53
+ let res;
54
+ try {
55
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
56
+ } catch (e) {
57
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
58
+ }
59
+ if (res.status === 404) return { ok: false, reason: 'not-found' };
60
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
61
+ const env = parseJson(res.raw);
62
+ const data = env && env.status === 'OK' ? env.data : env;
63
+ if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
64
+ return { ok: true, availability: normalizeAvailability(data) };
65
+ }
66
+
67
+ async function fetchAvailability(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
68
+ const {
69
+ getMeAvailabilityEndpoint = require('./config').getMeAvailabilityEndpoint,
70
+ requestAvailability: request = requestAvailability,
71
+ } = deps;
72
+ const endpoint = getMeAvailabilityEndpoint();
73
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
74
+ return request({ hubAccessToken }, { endpoint, timeoutMs });
75
+ }
76
+
77
+ // Partial upsert: only the changed fields ride the PUT (all fields optional).
78
+ async function saveAvailability(deps = {}, { hubAccessToken, availability, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
79
+ const {
80
+ getSetAvailabilityEndpoint = require('./config').getSetAvailabilityEndpoint,
81
+ requestSetAvailability = require('./onboarding-client').requestSetAvailability,
82
+ } = deps;
83
+ const endpoint = getSetAvailabilityEndpoint();
84
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
85
+ return requestSetAvailability({ availability, hubAccessToken }, { endpoint, timeoutMs });
86
+ }
87
+
88
+ module.exports = {
89
+ normalizeAvailability,
90
+ requestAvailability,
91
+ fetchAvailability,
92
+ saveAvailability,
93
+ };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ // `availability` flow: view (hours/month, open-to-work, location), and `--set`
4
+ // (interactive, explicit confirmation — it writes your profile). Talent Bearer.
5
+
6
+ const { styleLabelPrefix } = require('./ansi');
7
+ const { WORK_MODES, validateAvailability } = require('./onboarding-flow');
8
+
9
+ function makeAvailabilityDeps(overrides = {}) {
10
+ const client = require('./availability-client');
11
+ return {
12
+ fetchAvailability: (opts) => client.fetchAvailability({}, opts),
13
+ saveAvailability: (opts) => client.saveAvailability({}, opts),
14
+ promptSelect: (opts) => require('./prompt-select').promptSelect(opts),
15
+ promptMultiSelect: (opts) => require('./prompt-select').promptMultiSelect(opts),
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ function workModesLine(fp, modes) {
21
+ return modes && modes.length ? modes.map((m) => fp.workModeLabels[m] || m).join(', ') : fp.unknown;
22
+ }
23
+
24
+ function locationLine(a) {
25
+ return [a.country, a.subdivision, a.timezone].filter(Boolean).join(' · ') || null;
26
+ }
27
+
28
+ function renderView(io, fp, a) {
29
+ io.section(fp.title);
30
+ io.notify(styleLabelPrefix(fp.openToWork(a.available === true ? fp.yes : a.available === false ? fp.no : fp.unknown)));
31
+ io.notify(styleLabelPrefix(fp.monthlyHours(a.monthlyHours != null ? a.monthlyHours : fp.unknown)));
32
+ io.notify(styleLabelPrefix(fp.workModes(workModesLine(fp, a.workModes))));
33
+ const loc = locationLine(a);
34
+ if (loc) io.notify(styleLabelPrefix(fp.location(loc)));
35
+ if (a.availabilityExpiringDate) io.notify(styleLabelPrefix(fp.expires(a.availabilityExpiringDate)));
36
+ io.success(fp.done);
37
+ }
38
+
39
+ async function runSet({ io, ask, session, catalog, opts, deps }) {
40
+ const fp = catalog.availability;
41
+ const hubAccessToken = session ? session.hubAccessToken : null;
42
+ if (!opts.stdinIsTTY) {
43
+ io.error(fp.setNeedsInteractive);
44
+ return { ok: false, reason: 'non-interactive' };
45
+ }
46
+ const selOut = (s) => io.notify(s);
47
+ const yesNo = [{ label: fp.yes, value: true }, { label: fp.no, value: false }];
48
+ const pickYesNo = (header) => deps.promptSelect({ ask, stdinIsTTY: opts.stdinIsTTY, out: selOut, header, items: yesNo, labelFor: (x) => x.label, input: opts.input, output: opts.output });
49
+
50
+ const read = await io.withProgress(fp.loading, () => deps.fetchAvailability({ hubAccessToken }));
51
+ const current = read.ok ? read.availability : null;
52
+ if (!read.ok && read.reason !== 'not-found') {
53
+ io.error(fp.fetchFailed(read.reason));
54
+ return { ok: false, reason: read.reason };
55
+ }
56
+
57
+ io.section(fp.setTitle);
58
+ if (current) renderView({ section: () => {}, notify: io.notify, success: () => {} }, fp, current);
59
+
60
+ // Arrow-key yes/no pickers for the discrete fields.
61
+ const openPick = await pickYesNo(fp.askOpenToWork);
62
+ if (!openPick) { io.notify(fp.setCancelled); return { ok: true, cancelled: true }; }
63
+ const available = openPick.value;
64
+ const hoursRaw = (await io.ask(fp.askMonthlyHours)).trim();
65
+ if (hoursRaw && !/^\d{1,3}$/.test(hoursRaw)) { io.error(fp.monthlyHoursInvalid); return { ok: false, reason: 'bad-hours' }; }
66
+ const pickedModes = await deps.promptMultiSelect({
67
+ ask,
68
+ stdinIsTTY: opts.stdinIsTTY,
69
+ out: selOut,
70
+ items: WORK_MODES,
71
+ labelFor: (v) => fp.workModeLabels[v] || v,
72
+ header: fp.workModesAsk,
73
+ hint: fp.workModesHint,
74
+ initialMarked: (current && Array.isArray(current.workModes)) ? current.workModes.map((m) => WORK_MODES.indexOf(m)).filter((i) => i >= 0) : [],
75
+ input: opts.input,
76
+ output: opts.output,
77
+ });
78
+ // Reuse the register validation (dedupe/upcase, enum check) — no duplicated logic.
79
+ const workModes = validateAvailability({ workModes: pickedModes }).availability.workModes || [];
80
+ if (workModes.length === 1 && workModes[0] === 'REMOTE') io.notify(fp.onlyRemoteNote);
81
+
82
+ // Only the asked fields ride the partial upsert.
83
+ const body = { available };
84
+ if (hoursRaw) body.monthlyHours = hoursRaw;
85
+ if (workModes.length) body.workModes = workModes;
86
+
87
+ io.notify(fp.setDiff(
88
+ `${available ? fp.yes : fp.no}`,
89
+ hoursRaw || (current && current.monthlyHours) || fp.unknown,
90
+ workModesLine(fp, workModes),
91
+ ));
92
+ const confirmed = await io.confirm(fp.setConfirm);
93
+ if (!confirmed) { io.notify(fp.setCancelled); return { ok: true, cancelled: true }; }
94
+
95
+ const res = await io.withProgress(fp.setSaving, () => deps.saveAvailability({ hubAccessToken, availability: body }));
96
+ if (!res.ok) { io.error(fp.setFailed(res.reason)); return { ok: false, reason: res.reason }; }
97
+ io.success(fp.setDone);
98
+ return { ok: true, set: body };
99
+ }
100
+
101
+ async function runAvailability({ io, rawOut, ask, session, catalog, opts = {}, deps = makeAvailabilityDeps() }) {
102
+ const fp = catalog.availability;
103
+ if (opts.set) return runSet({ io, ask, session, catalog, opts, deps });
104
+
105
+ const hubAccessToken = session ? session.hubAccessToken : null;
106
+ const doFetch = () => deps.fetchAvailability({ hubAccessToken });
107
+ const res = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
108
+ if (!res.ok) {
109
+ io.error(res.reason === 'not-found' ? fp.notFound : fp.fetchFailed(res.reason));
110
+ return { ok: false, reason: res.reason };
111
+ }
112
+ if (opts.json) {
113
+ (rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, availability: res.availability }, null, 2)}\n`);
114
+ return { ok: true, availability: res.availability };
115
+ }
116
+ renderView(io, fp, res.availability);
117
+ return { ok: true, availability: res.availability };
118
+ }
119
+
120
+ module.exports = {
121
+ makeAvailabilityDeps,
122
+ runAvailability,
123
+ locationLine,
124
+ };