@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/README.md +19 -3
- package/bin/alice-install.mjs +22 -1
- package/lib/agent-registry.mjs +33 -0
- package/lib/coding-agent.mjs +187 -0
- package/lib/config-merger.mjs +5 -1
- package/lib/doctor.mjs +123 -43
- package/lib/installer.mjs +83 -19
- package/lib/license.mjs +158 -14
- package/lib/manifest.mjs +18 -9
- package/lib/mission-control.mjs +31 -0
- package/lib/release-guard.mjs +131 -0
- package/lib/skills.mjs +0 -1
- package/lib/workspace-scaffolder.mjs +11 -16
- package/package.json +5 -3
- package/templates/agents-pro.json +550 -0
- package/templates/mission-control-bridge/index.ts +494 -32
- package/templates/workspaces/dylan/SOUL.md +1 -1
- package/templates/workspaces/dylan/TOOLS.md +4 -4
- package/templates/workspaces/felix/SOUL.md +1 -1
- package/templates/workspaces/felix/TOOLS.md +3 -3
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', {
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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')
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
46
|
-
if (
|
|
47
|
-
return { licensed:
|
|
131
|
+
const record = readStoredLicense();
|
|
132
|
+
if (!record || !isValidFormat(record.key)) {
|
|
133
|
+
return { licensed: false, key: null, source: 'none' };
|
|
48
134
|
}
|
|
49
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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;
|
package/lib/mission-control.mjs
CHANGED
|
@@ -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
|
},
|