@robbiesrobotics/alice-agents 1.4.5 → 1.4.7

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/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;
@@ -53,6 +53,10 @@ export function buildMissionControlSettings(input = {}) {
53
53
  const sourceNode = String(input.sourceNode || hostname() || 'openclaw-local').trim();
54
54
  const ingestToken = String(input.ingestToken || '').trim();
55
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();
56
60
 
57
61
  return {
58
62
  enabled: input.enabled !== false,
@@ -66,6 +70,10 @@ export function buildMissionControlSettings(input = {}) {
66
70
  nodeRegisterUrl,
67
71
  nodeHeartbeatUrl,
68
72
  sourceNode,
73
+ ...(teamId ? { teamId } : {}),
74
+ ...(teamSlug ? { teamSlug } : {}),
75
+ ...(teamName ? { teamName } : {}),
76
+ ...(teamPlan ? { teamPlan } : {}),
69
77
  ...(ingestToken ? { ingestToken } : {}),
70
78
  ...(workerToken ? { workerToken } : {}),
71
79
  };
@@ -154,6 +162,10 @@ export function configureMissionControlCloud(input = {}) {
154
162
  runtimeBaseUrl: settings.runtimeBaseUrl,
155
163
  adminHeartbeatUrl: settings.adminHeartbeatUrl,
156
164
  sourceNode: settings.sourceNode,
165
+ teamId: settings.teamId ?? null,
166
+ teamSlug: settings.teamSlug ?? null,
167
+ teamName: settings.teamName ?? null,
168
+ teamPlan: settings.teamPlan ?? null,
157
169
  hasIngestToken: !!settings.ingestToken,
158
170
  hasWorkerToken: !!settings.workerToken,
159
171
  },
@@ -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
  },
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { buildCodingAgentSkillContent, resolveCodingAgentPreference } from './coding-agent.mjs';
5
6
 
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
8
  const TEMPLATES_DIR = join(__dirname, '..', 'templates', 'workspaces');
@@ -100,23 +101,17 @@ export function scaffoldWorkspace(agent, userInfo, agentCount) {
100
101
  return { workspaceDir, written, skipped };
101
102
  }
102
103
 
103
- export function scaffoldSkills() {
104
- // Install the claude-code skill into ~/.openclaw/skills/
105
- const claudeCodeSkillDir = join(SKILLS_DIR, 'claude-code');
106
- mkdirSync(claudeCodeSkillDir, { recursive: true });
104
+ export function scaffoldSkills(preference = null) {
105
+ const codingAgentPreference = preference || resolveCodingAgentPreference();
106
+ const codingAgentSkillDir = join(SKILLS_DIR, codingAgentPreference.skillId);
107
+ mkdirSync(codingAgentSkillDir, { recursive: true });
107
108
 
108
- const skillTemplatePath = join(__dirname, '..', 'templates', 'skills', 'claude-code', 'SKILL.md');
109
- const skillDestPath = join(claudeCodeSkillDir, 'SKILL.md');
110
-
111
- if (existsSync(skillTemplatePath)) {
112
- const content = readFileSync(skillTemplatePath, 'utf8');
113
- writeFileSync(skillDestPath, content, 'utf8');
114
- return true;
115
- }
116
- return false;
109
+ const skillDestPath = join(codingAgentSkillDir, 'SKILL.md');
110
+ writeFileSync(skillDestPath, buildCodingAgentSkillContent(codingAgentPreference), 'utf8');
111
+ return codingAgentPreference;
117
112
  }
118
113
 
119
- export function scaffoldAll(agents, userInfo) {
114
+ export function scaffoldAll(agents, userInfo, skillPreference = null) {
120
115
  const agentCount = agents.length;
121
116
  const results = [];
122
117
 
@@ -126,7 +121,7 @@ export function scaffoldAll(agents, userInfo) {
126
121
  }
127
122
 
128
123
  // Install skills
129
- scaffoldSkills();
124
+ const installedSkill = scaffoldSkills(skillPreference);
130
125
 
131
- return results;
126
+ return { workspaces: results, installedSkill };
132
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robbiesrobotics/alice-agents",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "A.L.I.C.E. — 28 AI agents for OpenClaw. One conversation, one team.",
5
5
  "bin": {
6
6
  "alice-agents": "bin/alice-install.mjs"
@@ -23,15 +23,17 @@
23
23
  "tools/",
24
24
  "snapshots/",
25
25
  "templates/agents-starter.json",
26
+ "templates/agents-pro.json",
26
27
  "templates/mission-control-bridge/",
27
28
  "templates/skills/",
28
29
  "templates/workspaces/",
29
- "SELF-HEALING-SPEC.md",
30
30
  "README.md"
31
31
  ],
32
32
  "scripts": {
33
33
  "test": "node --test test/*.test.mjs",
34
- "test:check": "node --check lib/*.mjs bin/*.mjs"
34
+ "test:check": "node --check lib/*.mjs bin/*.mjs",
35
+ "release:check": "node lib/release-guard.mjs",
36
+ "prepublishOnly": "npm test && npm run test:check && npm run release:check"
35
37
  },
36
38
  "publishConfig": {
37
39
  "access": "public"