@robbiesrobotics/alice-agents 1.4.4 → 1.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/installer.mjs CHANGED
@@ -27,6 +27,8 @@ import { c, bold, dim, green, greenBold, red, yellow, cyan, gray,
27
27
  icons, separator, printSection, printSeparator, printBox,
28
28
  printStepDone, printStepFail, printStepSkip } from './colors.mjs';
29
29
  import { runSkillsWizardStep } from './skills.mjs';
30
+ import { resolveCodingAgentPreference } from './coding-agent.mjs';
31
+ import { loadAgentRegistry } from './agent-registry.mjs';
30
32
 
31
33
  function commandExists(cmd) {
32
34
  const probe = process.platform === 'win32' ? 'where' : 'which';
@@ -105,7 +107,11 @@ function getOpenClawVersion() {
105
107
 
106
108
  function getLatestNpmVersion() {
107
109
  try {
108
- const output = execSync('npm view openclaw version', { stdio: 'pipe', encoding: 'utf8' });
110
+ const output = execSync('npm view openclaw version', {
111
+ stdio: 'pipe',
112
+ encoding: 'utf8',
113
+ timeout: 5000,
114
+ });
109
115
  return output.trim();
110
116
  } catch {
111
117
  return null;
@@ -421,11 +427,6 @@ async function installRuntime(auto) {
421
427
 
422
428
  const __dirname = dirname(fileURLToPath(import.meta.url));
423
429
 
424
- function loadAgentRegistry() {
425
- const raw = readFileSync(join(__dirname, '..', 'templates', 'agents-starter.json'), 'utf8');
426
- return JSON.parse(raw);
427
- }
428
-
429
430
  function printBanner() {
430
431
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)));
431
432
  const version = pkg.version || '';
@@ -446,6 +447,13 @@ function printSummary(mode, tier, agents, preset, userInfo, detectedModels) {
446
447
  return printSummaryWithOptions(mode, tier, agents, preset, userInfo, detectedModels, null);
447
448
  }
448
449
 
450
+ function normalizeTierOption(tier) {
451
+ if (!tier) return null;
452
+ const normalized = String(tier).trim().toLowerCase();
453
+ if (normalized === 'starter' || normalized === 'pro') return normalized;
454
+ throw new Error(`Invalid --tier value "${tier}". Use starter or pro.`);
455
+ }
456
+
449
457
  function printSummaryWithOptions(mode, tier, agents, preset, userInfo, detectedModels, missionControl) {
450
458
  const modelLabel =
451
459
  preset === 'detected'
@@ -542,8 +550,6 @@ export async function runInstall(options = {}) {
542
550
  console.log(` ${icons.info} ${dim('No model configured yet — you\'ll be prompted to choose one.')}\n`);
543
551
  }
544
552
 
545
- const allAgents = loadAgentRegistry();
546
-
547
553
  // 2. Install mode
548
554
  let mode;
549
555
  if (options.modeOverride) {
@@ -596,7 +602,9 @@ export async function runInstall(options = {}) {
596
602
 
597
603
  // 5. Tier selection
598
604
  let tier;
599
- if (auto) {
605
+ if (options.tierOverride) {
606
+ tier = normalizeTierOption(options.tierOverride);
607
+ } else if (auto) {
600
608
  tier = 'starter';
601
609
  } else {
602
610
  tier = await promptTier();
@@ -604,16 +612,49 @@ export async function runInstall(options = {}) {
604
612
 
605
613
  if (tier === 'pro') {
606
614
  const { checkProLicense, validateLicenseRemote, storeLicense, isValidFormat } = await import('./license.mjs');
615
+ const explicitLicenseKey = String(options.licenseKey || '').trim();
607
616
 
608
- const existing = await checkProLicense();
617
+ const existing = await checkProLicense({ revalidate: true });
609
618
 
610
619
  if (existing.licensed) {
611
- printStepDone(`Pro license found (${existing.key.slice(0, 12)}...)`);
620
+ if (existing.provisional) {
621
+ printStepDone(`Pro license found (${existing.key.slice(0, 12)}...)`, `temporary grace until ${existing.graceUntil}`);
622
+ } else if (existing.needsRevalidation) {
623
+ printStepDone(`Pro license found (${existing.key.slice(0, 12)}...)`, 'using cached validation while the service is unavailable');
624
+ } else {
625
+ printStepDone(`Pro license found (${existing.key.slice(0, 12)}...)`);
626
+ }
627
+ } else if (auto && explicitLicenseKey) {
628
+ if (!isValidFormat(explicitLicenseKey)) {
629
+ throw new Error('Invalid --license-key format. Key must be ALICE-XXXX-XXXX-XXXX.');
630
+ }
631
+
632
+ console.log(' Validating provided Pro license key...');
633
+ const result = await validateLicenseRemote(explicitLicenseKey);
634
+ if (result.valid) {
635
+ storeLicense(explicitLicenseKey, {
636
+ status: 'validated',
637
+ plan: result.plan || 'pro',
638
+ transport: result.transport,
639
+ source: 'remote',
640
+ });
641
+ printStepDone('License verified! Welcome to A.L.I.C.E. Pro.');
642
+ } else if (result.graceEligible) {
643
+ const graceRecord = storeLicense(explicitLicenseKey, {
644
+ status: 'grace',
645
+ plan: 'pro',
646
+ transport: result.transport,
647
+ source: 'grace',
648
+ });
649
+ printStepDone('Key stored', `temporary grace until ${graceRecord.graceUntil}`);
650
+ } else {
651
+ throw new Error(`Pro license validation failed: ${result.message ?? 'Not recognized'}`);
652
+ }
612
653
  } else if (auto) {
613
654
  // --yes flag: skip interactive prompt, fallback to Starter if no stored license
614
655
  console.log('');
615
656
  console.log(` ${icons.info} ${dim('Pro tier requires a license key.')}`);
616
- console.log(` ${dim('Run without --yes to enter your license key.')}`);
657
+ console.log(` ${dim('Run without --yes to enter your license key, or pass --license-key for automation.')}`);
617
658
  console.log(` ${dim('Falling back to Starter tier.')}`);
618
659
  console.log(` ${dim('Purchase a license at:')} ${cyan('https://getalice.av3.ai/pricing')}`);
619
660
  tier = 'starter';
@@ -635,12 +676,22 @@ export async function runInstall(options = {}) {
635
676
  const result = await validateLicenseRemote(key);
636
677
 
637
678
  if (result.valid) {
638
- storeLicense(key);
639
- if (result.message === 'offline') {
640
- printStepDone('Key stored', 'offline — will validate on next run');
641
- } else {
642
- printStepDone('License verified! Welcome to A.L.I.C.E. Pro.');
643
- }
679
+ storeLicense(key, {
680
+ status: 'validated',
681
+ plan: result.plan || 'pro',
682
+ transport: result.transport,
683
+ source: 'remote',
684
+ });
685
+ printStepDone('License verified! Welcome to A.L.I.C.E. Pro.');
686
+ break;
687
+ } else if (result.graceEligible) {
688
+ const graceRecord = storeLicense(key, {
689
+ status: 'grace',
690
+ plan: 'pro',
691
+ transport: result.transport,
692
+ source: 'grace',
693
+ });
694
+ printStepDone('Key stored', `temporary grace until ${graceRecord.graceUntil}`);
644
695
  break;
645
696
  } else {
646
697
  printStepFail(`Invalid key: ${result.message ?? 'Not recognized'}`);
@@ -684,12 +735,21 @@ export async function runInstall(options = {}) {
684
735
  dashboardUrl,
685
736
  ingestUrl,
686
737
  sourceNode,
738
+ teamId: String(options.cloudTeamId || existingMissionControl?.teamId || '').trim(),
739
+ teamSlug: String(options.cloudTeamSlug || existingMissionControl?.teamSlug || '').trim(),
740
+ teamName: String(options.cloudTeamName || existingMissionControl?.teamName || '').trim(),
741
+ teamPlan: String(options.cloudTeamPlan || existingMissionControl?.teamPlan || '').trim(),
687
742
  hasIngestToken: !!ingestToken,
688
743
  ingestToken,
689
744
  };
690
745
  }
691
746
  }
692
747
 
748
+ const codingAgent = resolveCodingAgentPreference({
749
+ detectedModels,
750
+ override: options.codingTool,
751
+ });
752
+ const allAgents = loadAgentRegistry(tier);
693
753
  const agents = allAgents;
694
754
 
695
755
  // 6. Confirmation
@@ -733,7 +793,7 @@ export async function runInstall(options = {}) {
733
793
  }
734
794
 
735
795
  // Scaffold workspaces
736
- const results = scaffoldAll(agents, userInfo);
796
+ const { workspaces: results, installedSkill } = scaffoldAll(agents, userInfo, codingAgent);
737
797
  let newWorkspaces = 0;
738
798
  let updatedWorkspaces = 0;
739
799
  for (const r of results) {
@@ -744,6 +804,7 @@ export async function runInstall(options = {}) {
744
804
  }
745
805
  }
746
806
  printStepDone('Workspaces', `${newWorkspaces} created, ${updatedWorkspaces} updated`);
807
+ printStepDone('Coding agent skill', `${installedSkill.preferred.name} preferred, ${installedSkill.fallback.name} fallback`);
747
808
 
748
809
  // Skills installation step
749
810
  const finalRuntimeForSkills = await detectRuntime();
@@ -762,6 +823,9 @@ export async function runInstall(options = {}) {
762
823
  userName: userInfo.name,
763
824
  userTimezone: userInfo.timezone,
764
825
  modelPreset: effectivePreset,
826
+ skills: [...new Set([...(existing?.skills || []), installedSkill.skillId, ...(skillsInstalled || [])])],
827
+ codingTool: installedSkill.preferredTool,
828
+ codingSkill: installedSkill.skillId,
765
829
  missionControl: missionControl
766
830
  ? {
767
831
  enabled: true,
package/lib/license.mjs CHANGED
@@ -7,19 +7,71 @@ const LICENSE_DIR = join(homedir(), '.alice');
7
7
  const LICENSE_FILE = join(LICENSE_DIR, 'license');
8
8
  const VALIDATE_URL = 'https://getalice.av3.ai/api/license/validate';
9
9
  const KEY_REGEX = /^ALICE-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/;
10
+ const GRACE_PERIOD_MS = 72 * 60 * 60 * 1000;
11
+ const REVALIDATE_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
12
+
13
+ function nowIso() {
14
+ return new Date().toISOString();
15
+ }
16
+
17
+ function parseLicenseRecord(raw) {
18
+ const trimmed = raw?.trim();
19
+ if (!trimmed) return null;
20
+
21
+ if (trimmed.startsWith('{')) {
22
+ try {
23
+ const parsed = JSON.parse(trimmed);
24
+ if (!parsed?.key) return null;
25
+ return parsed;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ return {
32
+ key: trimmed,
33
+ plan: 'pro',
34
+ status: 'validated',
35
+ validatedAt: null,
36
+ updatedAt: null,
37
+ source: 'legacy',
38
+ };
39
+ }
40
+
41
+ function createLicenseRecord(key, data = {}) {
42
+ return {
43
+ key: key.trim().toUpperCase(),
44
+ plan: data.plan || 'pro',
45
+ status: data.status || 'validated',
46
+ validatedAt: data.status === 'validated' ? (data.validatedAt || nowIso()) : (data.validatedAt || null),
47
+ graceUntil: data.status === 'grace' ? (data.graceUntil || new Date(Date.now() + GRACE_PERIOD_MS).toISOString()) : null,
48
+ updatedAt: nowIso(),
49
+ transport: data.transport || null,
50
+ source: data.source || 'local',
51
+ };
52
+ }
53
+
54
+ function shouldRevalidate(record) {
55
+ if (!record?.validatedAt) return true;
56
+ const validatedAt = Date.parse(record.validatedAt);
57
+ if (Number.isNaN(validatedAt)) return true;
58
+ return Date.now() - validatedAt >= REVALIDATE_INTERVAL_MS;
59
+ }
10
60
 
11
61
  export function readStoredLicense() {
12
62
  try {
13
63
  if (!existsSync(LICENSE_FILE)) return null;
14
- return readFileSync(LICENSE_FILE, 'utf8').trim();
64
+ return parseLicenseRecord(readFileSync(LICENSE_FILE, 'utf8'));
15
65
  } catch {
16
66
  return null;
17
67
  }
18
68
  }
19
69
 
20
- export function storeLicense(key) {
70
+ export function storeLicense(key, data = {}) {
21
71
  mkdirSync(LICENSE_DIR, { recursive: true });
22
- writeFileSync(LICENSE_FILE, key.trim(), 'utf8');
72
+ const record = createLicenseRecord(key, data);
73
+ writeFileSync(LICENSE_FILE, JSON.stringify(record, null, 2) + '\n', 'utf8');
74
+ return record;
23
75
  }
24
76
 
25
77
  export function isValidFormat(key) {
@@ -27,24 +79,116 @@ export function isValidFormat(key) {
27
79
  }
28
80
 
29
81
  export async function validateLicenseRemote(key) {
30
- // Returns { valid: boolean, plan: string, message?: string }
82
+ // Returns { valid: boolean, plan?: string, message?: string, transient?: boolean, graceEligible?: boolean, transport?: string }
83
+ const normalizedKey = key.trim().toUpperCase();
84
+ const postBody = JSON.stringify({ key: normalizedKey });
85
+
31
86
  try {
32
- const res = await fetch(`${VALIDATE_URL}?key=${encodeURIComponent(key)}`, {
87
+ const res = await fetch(VALIDATE_URL, {
88
+ method: 'POST',
89
+ headers: { 'content-type': 'application/json' },
90
+ body: postBody,
33
91
  signal: AbortSignal.timeout(5000),
34
92
  });
35
- if (!res.ok) return { valid: false, message: 'Validation service unavailable' };
36
- return await res.json();
93
+ if (!res.ok && [404, 405, 415].includes(res.status)) {
94
+ const fallback = await fetch(`${VALIDATE_URL}?key=${encodeURIComponent(normalizedKey)}`, {
95
+ signal: AbortSignal.timeout(5000),
96
+ });
97
+ if (!fallback.ok) {
98
+ return {
99
+ valid: false,
100
+ message: 'Validation service unavailable',
101
+ transient: fallback.status >= 500,
102
+ graceEligible: isValidFormat(normalizedKey),
103
+ transport: 'get',
104
+ };
105
+ }
106
+ return { ...(await fallback.json()), transport: 'get' };
107
+ }
108
+ if (!res.ok) {
109
+ return {
110
+ valid: false,
111
+ message: res.status >= 500 ? 'Validation service unavailable' : 'License key not recognized',
112
+ transient: res.status >= 500,
113
+ graceEligible: res.status >= 500 && isValidFormat(normalizedKey),
114
+ transport: 'post',
115
+ };
116
+ }
117
+ return { ...(await res.json()), transport: 'post' };
37
118
  } catch {
38
- // Network error — fail open (allow install, validate next time)
39
- return { valid: true, plan: 'pro', message: 'offline' };
119
+ return {
120
+ valid: false,
121
+ message: 'Validation service unavailable',
122
+ transient: true,
123
+ graceEligible: isValidFormat(normalizedKey),
124
+ transport: 'post',
125
+ };
40
126
  }
41
127
  }
42
128
 
43
- export async function checkProLicense() {
129
+ export async function checkProLicense(options = {}) {
44
130
  // Returns: { licensed: boolean, key: string|null, source: 'stored'|'none' }
45
- const stored = readStoredLicense();
46
- if (stored && isValidFormat(stored)) {
47
- return { licensed: true, key: stored, source: 'stored' };
131
+ const record = readStoredLicense();
132
+ if (!record || !isValidFormat(record.key)) {
133
+ return { licensed: false, key: null, source: 'none' };
48
134
  }
49
- return { licensed: false, key: null, source: 'none' };
135
+
136
+ if (record.status === 'grace') {
137
+ const graceUntil = Date.parse(record.graceUntil || '');
138
+ if (!Number.isNaN(graceUntil) && graceUntil > Date.now()) {
139
+ return {
140
+ licensed: true,
141
+ key: record.key,
142
+ source: 'grace',
143
+ provisional: true,
144
+ graceUntil: record.graceUntil,
145
+ needsRevalidation: true,
146
+ };
147
+ }
148
+ return { licensed: false, key: null, source: 'expired_grace', message: 'Temporary grace period expired' };
149
+ }
150
+
151
+ const shouldRefresh = options.revalidate === true || shouldRevalidate(record);
152
+ if (shouldRefresh) {
153
+ const refreshed = await validateLicenseRemote(record.key);
154
+ if (refreshed.valid) {
155
+ const stored = storeLicense(record.key, {
156
+ status: 'validated',
157
+ plan: refreshed.plan || record.plan || 'pro',
158
+ transport: refreshed.transport,
159
+ source: 'remote',
160
+ });
161
+ return {
162
+ licensed: true,
163
+ key: stored.key,
164
+ source: 'revalidated',
165
+ provisional: false,
166
+ validatedAt: stored.validatedAt,
167
+ needsRevalidation: false,
168
+ };
169
+ }
170
+
171
+ if (refreshed.transient) {
172
+ return {
173
+ licensed: true,
174
+ key: record.key,
175
+ source: 'stored',
176
+ provisional: false,
177
+ validatedAt: record.validatedAt,
178
+ needsRevalidation: true,
179
+ message: refreshed.message,
180
+ };
181
+ }
182
+
183
+ return { licensed: false, key: null, source: 'invalid', message: refreshed.message || 'License key not recognized' };
184
+ }
185
+
186
+ return {
187
+ licensed: true,
188
+ key: record.key,
189
+ source: record.source || 'stored',
190
+ provisional: false,
191
+ validatedAt: record.validatedAt,
192
+ needsRevalidation: false,
193
+ };
50
194
  }
package/lib/manifest.mjs CHANGED
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
 
5
5
  const MANIFEST_NAME = '.alice-manifest.json';
6
+ const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '0.0.0';
6
7
 
7
8
  export function getManifestPath() {
8
9
  return join(homedir(), '.openclaw', MANIFEST_NAME);
@@ -18,16 +19,24 @@ export function readManifest() {
18
19
  }
19
20
 
20
21
  export function writeManifest(data) {
22
+ const existing = readManifest() || {};
23
+ const now = new Date().toISOString();
21
24
  const manifest = {
22
- version: '1.0.0',
23
- installedAt: data.installedAt || new Date().toISOString(),
24
- updatedAt: new Date().toISOString(),
25
- tier: data.tier,
26
- agents: data.agents,
27
- userName: data.userName,
28
- userTimezone: data.userTimezone,
29
- modelPreset: data.modelPreset,
30
- missionControl: data.missionControl || null,
25
+ ...existing,
26
+ version: data.version || existing.version || PACKAGE_VERSION,
27
+ schemaVersion: 2,
28
+ packageVersion: data.packageVersion || existing.packageVersion || PACKAGE_VERSION,
29
+ installedAt: data.installedAt || existing.installedAt || now,
30
+ updatedAt: now,
31
+ tier: data.tier ?? existing.tier ?? 'starter',
32
+ agents: Array.isArray(data.agents) ? [...new Set(data.agents)] : (existing.agents || []),
33
+ userName: data.userName ?? existing.userName ?? null,
34
+ userTimezone: data.userTimezone ?? existing.userTimezone ?? null,
35
+ modelPreset: data.modelPreset ?? existing.modelPreset ?? null,
36
+ missionControl: data.missionControl ?? existing.missionControl ?? null,
37
+ skills: Array.isArray(data.skills) ? [...new Set(data.skills)] : (existing.skills || []),
38
+ codingTool: data.codingTool ?? existing.codingTool ?? null,
39
+ codingSkill: data.codingSkill ?? existing.codingSkill ?? null,
31
40
  };
32
41
  writeFileSync(getManifestPath(), JSON.stringify(manifest, null, 2) + '\n', 'utf8');
33
42
  return manifest;
@@ -9,6 +9,7 @@ const CONFIG_PATH = join(OPENCLAW_HOME, 'openclaw.json');
9
9
  const MC_CONFIG_PATH = join(OPENCLAW_HOME, '.alice-mission-control.json');
10
10
  const BRIDGE_ID = 'mission-control-bridge';
11
11
  const DEFAULT_DASHBOARD_URL = 'https://alice.av3.ai';
12
+ const DEFAULT_ADMIN_URL = 'https://admin.av3.ai';
12
13
  const DEFAULT_INGEST_URL = `${DEFAULT_DASHBOARD_URL}/api/v1/ingest`;
13
14
  const TEMPLATE_DIR = join(__dirname, '..', 'templates', 'mission-control-bridge');
14
15
 
@@ -42,17 +43,39 @@ export function readMissionControlConfig() {
42
43
 
43
44
  export function buildMissionControlSettings(input = {}) {
44
45
  const dashboardUrl = normalizeUrl(input.dashboardUrl, DEFAULT_DASHBOARD_URL);
46
+ const adminUrl = normalizeUrl(input.adminUrl, DEFAULT_ADMIN_URL);
45
47
  const ingestUrl = normalizeUrl(input.ingestUrl, `${dashboardUrl}/api/v1/ingest`);
48
+ const runtimeBaseUrl = normalizeUrl(input.runtimeBaseUrl, `${dashboardUrl}/api/v1/runtime`);
49
+ const adminHeartbeatUrl = normalizeUrl(input.adminHeartbeatUrl, `${adminUrl}/api/admin/v1/node-heartbeat`);
50
+ const commandsUrl = normalizeUrl(input.commandsUrl, `${runtimeBaseUrl}/commands`);
51
+ const nodeRegisterUrl = normalizeUrl(input.nodeRegisterUrl, `${runtimeBaseUrl}/nodes/register`);
52
+ const nodeHeartbeatUrl = normalizeUrl(input.nodeHeartbeatUrl, `${runtimeBaseUrl}/nodes/heartbeat`);
46
53
  const sourceNode = String(input.sourceNode || hostname() || 'openclaw-local').trim();
47
54
  const ingestToken = String(input.ingestToken || '').trim();
55
+ const workerToken = String(input.workerToken || ingestToken || '').trim();
56
+ const teamId = String(input.teamId || '').trim();
57
+ const teamSlug = String(input.teamSlug || '').trim();
58
+ const teamName = String(input.teamName || '').trim();
59
+ const teamPlan = String(input.teamPlan || '').trim();
48
60
 
49
61
  return {
50
62
  enabled: input.enabled !== false,
51
63
  provider: 'cloud',
52
64
  dashboardUrl,
65
+ adminUrl,
53
66
  ingestUrl,
67
+ runtimeBaseUrl,
68
+ adminHeartbeatUrl,
69
+ commandsUrl,
70
+ nodeRegisterUrl,
71
+ nodeHeartbeatUrl,
54
72
  sourceNode,
73
+ ...(teamId ? { teamId } : {}),
74
+ ...(teamSlug ? { teamSlug } : {}),
75
+ ...(teamName ? { teamName } : {}),
76
+ ...(teamPlan ? { teamPlan } : {}),
55
77
  ...(ingestToken ? { ingestToken } : {}),
78
+ ...(workerToken ? { workerToken } : {}),
56
79
  };
57
80
  }
58
81
 
@@ -134,9 +157,17 @@ export function configureMissionControlCloud(input = {}) {
134
157
  enabled: settings.enabled,
135
158
  provider: settings.provider,
136
159
  dashboardUrl: settings.dashboardUrl,
160
+ adminUrl: settings.adminUrl,
137
161
  ingestUrl: settings.ingestUrl,
162
+ runtimeBaseUrl: settings.runtimeBaseUrl,
163
+ adminHeartbeatUrl: settings.adminHeartbeatUrl,
138
164
  sourceNode: settings.sourceNode,
165
+ teamId: settings.teamId ?? null,
166
+ teamSlug: settings.teamSlug ?? null,
167
+ teamName: settings.teamName ?? null,
168
+ teamPlan: settings.teamPlan ?? null,
139
169
  hasIngestToken: !!settings.ingestToken,
170
+ hasWorkerToken: !!settings.workerToken,
140
171
  },
141
172
  };
142
173
  }
@@ -0,0 +1,131 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const DEFAULT_REPO_ROOT = join(__dirname, '..');
8
+
9
+ export function readPackageVersion(repoRoot = DEFAULT_REPO_ROOT) {
10
+ const packagePath = join(repoRoot, 'package.json');
11
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
12
+ return String(pkg.version || '').trim();
13
+ }
14
+
15
+ export function getExpectedTag(version) {
16
+ return `v${version}`;
17
+ }
18
+
19
+ function git(repoRoot, args) {
20
+ return execFileSync('git', args, {
21
+ cwd: repoRoot,
22
+ encoding: 'utf8',
23
+ stdio: ['ignore', 'pipe', 'pipe'],
24
+ }).trim();
25
+ }
26
+
27
+ export function getTagsOnHead(repoRoot = DEFAULT_REPO_ROOT) {
28
+ const output = git(repoRoot, ['tag', '--points-at', 'HEAD']);
29
+ return output ? output.split('\n').map((line) => line.trim()).filter(Boolean) : [];
30
+ }
31
+
32
+ export function hasChangelogEntry(repoRoot, version) {
33
+ const changelogPath = join(repoRoot, 'landing', 'content', 'changelog.md');
34
+ if (!existsSync(changelogPath)) return false;
35
+ const changelog = readFileSync(changelogPath, 'utf8');
36
+ return changelog.includes(`## v${version} — `);
37
+ }
38
+
39
+ export function getReleaseNotePath(repoRoot, version) {
40
+ return join(repoRoot, 'releases', `v${version}.md`);
41
+ }
42
+
43
+ export function getReleaseNoteChecks(repoRoot, version) {
44
+ const releaseNotePath = getReleaseNotePath(repoRoot, version);
45
+ if (!existsSync(releaseNotePath)) {
46
+ return {
47
+ exists: false,
48
+ hasTitle: false,
49
+ approved: false,
50
+ hasAnnouncementSection: false,
51
+ hasChannelsSection: false,
52
+ path: releaseNotePath,
53
+ };
54
+ }
55
+
56
+ const content = readFileSync(releaseNotePath, 'utf8');
57
+ return {
58
+ exists: true,
59
+ hasTitle: new RegExp(`^#\\s+v${version}\\b`, 'm').test(content),
60
+ approved: /^Status:\s*approved\s*$/im.test(content),
61
+ hasAnnouncementSection: /^##\s+Announcement\s*$/im.test(content),
62
+ hasChannelsSection: /^##\s+Channels\s*$/im.test(content),
63
+ path: releaseNotePath,
64
+ };
65
+ }
66
+
67
+ export function collectReleaseIssues(repoRoot = DEFAULT_REPO_ROOT) {
68
+ const version = readPackageVersion(repoRoot);
69
+ const expectedTag = getExpectedTag(version);
70
+ const issues = [];
71
+
72
+ if (!version) {
73
+ issues.push('package.json is missing a version.');
74
+ return issues;
75
+ }
76
+
77
+ const tagsOnHead = getTagsOnHead(repoRoot);
78
+ if (!tagsOnHead.includes(expectedTag)) {
79
+ issues.push(`HEAD is missing the matching git tag ${expectedTag}.`);
80
+ }
81
+
82
+ if (!hasChangelogEntry(repoRoot, version)) {
83
+ issues.push(`landing/content/changelog.md is missing the v${version} entry.`);
84
+ }
85
+
86
+ const releaseNote = getReleaseNoteChecks(repoRoot, version);
87
+ if (!releaseNote.exists) {
88
+ issues.push(`Missing release brief: releases/v${version}.md.`);
89
+ } else {
90
+ if (!releaseNote.hasTitle) {
91
+ issues.push(`releases/v${version}.md is missing the '# v${version}' title.`);
92
+ }
93
+ if (!releaseNote.approved) {
94
+ issues.push(`releases/v${version}.md must include 'Status: approved' before publish.`);
95
+ }
96
+ if (!releaseNote.hasAnnouncementSection) {
97
+ issues.push(`releases/v${version}.md is missing the '## Announcement' section.`);
98
+ }
99
+ if (!releaseNote.hasChannelsSection) {
100
+ issues.push(`releases/v${version}.md is missing the '## Channels' section.`);
101
+ }
102
+ }
103
+
104
+ return issues;
105
+ }
106
+
107
+ export function runReleaseGuard(repoRoot = DEFAULT_REPO_ROOT) {
108
+ const version = readPackageVersion(repoRoot);
109
+ const issues = collectReleaseIssues(repoRoot);
110
+
111
+ if (issues.length) {
112
+ const formatted = issues.map((issue) => `- ${issue}`).join('\n');
113
+ throw new Error(
114
+ `Release guard blocked publish for v${version}.\n${formatted}\n\n` +
115
+ 'Fix the release brief, changelog, and git tag before running npm publish again.',
116
+ );
117
+ }
118
+
119
+ return `Release guard passed for v${version}.`;
120
+ }
121
+
122
+ const isEntrypoint = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
123
+
124
+ if (isEntrypoint) {
125
+ try {
126
+ console.log(runReleaseGuard());
127
+ } catch (error) {
128
+ console.error(error instanceof Error ? error.message : String(error));
129
+ process.exit(1);
130
+ }
131
+ }
package/lib/skills.mjs CHANGED
@@ -51,7 +51,6 @@ export const SKILL_CATALOG = [
51
51
  skills: [
52
52
  { id: 'github', label: 'GitHub', desc: 'Issues, PRs, CI via gh CLI' },
53
53
  { id: 'gh-issues', label: 'GitHub Issues Bot', desc: 'Auto-fix issues and open PRs' },
54
- { id: 'coding-agent', label: 'Coding Agent', desc: 'Delegate tasks to Codex / Claude Code' },
55
54
  { id: '1password', label: '1Password', desc: 'Secrets and credentials via op CLI' },
56
55
  ],
57
56
  },