zigrix 0.1.0-alpha.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +159 -120
- package/dist/agents/registry.js +19 -2
- package/dist/agents/roles.d.ts +10 -0
- package/dist/agents/roles.js +83 -0
- package/dist/config/defaults.d.ts +88 -6
- package/dist/config/defaults.js +82 -50
- package/dist/config/load.d.ts +5 -3
- package/dist/config/load.js +69 -30
- package/dist/config/schema.d.ts +46 -4
- package/dist/config/schema.js +49 -3
- package/dist/configure.d.ts +2 -0
- package/dist/configure.js +37 -14
- package/dist/dashboard/.next/BUILD_ID +1 -1
- package/dist/dashboard/.next/app-build-manifest.json +13 -13
- package/dist/dashboard/.next/app-path-routes-manifest.json +3 -3
- package/dist/dashboard/.next/build-manifest.json +2 -2
- package/dist/dashboard/.next/prerender-manifest.json +6 -6
- package/dist/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/_not-found.html +1 -1
- package/dist/dashboard/.next/server/app/_not-found.rsc +1 -1
- package/dist/dashboard/.next/server/app/api/auth/login/route.js +1 -1
- package/dist/dashboard/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/auth/logout/route.js +1 -1
- package/dist/dashboard/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/auth/session/route.js +1 -1
- package/dist/dashboard/.next/server/app/api/auth/session/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/auth/setup/route.js +1 -1
- package/dist/dashboard/.next/server/app/api/auth/setup/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/overview/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/tasks/[taskId]/conversation/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/login.html +1 -1
- package/dist/dashboard/.next/server/app/login.rsc +1 -1
- package/dist/dashboard/.next/server/app/page.js +2 -2
- package/dist/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/dist/dashboard/.next/server/app/setup.html +1 -1
- package/dist/dashboard/.next/server/app/setup.rsc +1 -1
- package/dist/dashboard/.next/server/app-paths-manifest.json +3 -3
- package/dist/dashboard/.next/server/chunks/972.js +1 -1
- package/dist/dashboard/.next/server/functions-config-manifest.json +3 -3
- package/dist/dashboard/.next/server/middleware.js +1 -1
- package/dist/dashboard/.next/server/pages/404.html +1 -1
- package/dist/dashboard/.next/server/pages/500.html +1 -1
- package/dist/dashboard/.next/static/chunks/app/page-0314989c31e18b4b.js +1 -0
- package/dist/dashboard/.next/static/css/{94d75aff24d0c077.css → c3a7306cb2ba3f6c.css} +1 -1
- package/dist/dashboard.js +47 -0
- package/dist/doctor.js +28 -5
- package/dist/index.js +175 -171
- package/dist/onboard.d.ts +76 -2
- package/dist/onboard.js +529 -25
- package/dist/orchestration/dispatch.d.ts +3 -1
- package/dist/orchestration/dispatch.js +173 -45
- package/dist/orchestration/evidence.js +31 -4
- package/dist/orchestration/finalize.d.ts +1 -0
- package/dist/orchestration/finalize.js +5 -3
- package/dist/orchestration/report.js +9 -1
- package/dist/orchestration/worker.d.ts +1 -1
- package/dist/orchestration/worker.js +58 -8
- package/dist/rules/templates.js +3 -6
- package/dist/state/tasks.d.ts +12 -0
- package/dist/state/tasks.js +7 -0
- package/package.json +23 -2
- package/rules/defaults/README.md +9 -9
- package/rules/defaults/{back-zig.md → backend-agent.md} +4 -4
- package/rules/defaults/{front-zig.md → frontend-agent.md} +4 -4
- package/rules/defaults/orchestrator-agent.md +261 -0
- package/rules/defaults/{qa-zig.md → qa-agent.md} +11 -11
- package/rules/defaults/{sec-zig.md → security-agent.md} +4 -4
- package/rules/defaults/{sys-zig.md → system-agent.md} +8 -9
- package/rules/defaults/worker-common.md +25 -19
- package/skills/zigrix-doctor/SKILL.md +4 -2
- package/skills/zigrix-evidence/SKILL.md +7 -3
- package/skills/zigrix-main-agent-guide/SKILL.md +128 -0
- package/skills/zigrix-shared/SKILL.md +27 -3
- package/skills/zigrix-task-create/SKILL.md +8 -2
- package/skills/zigrix-task-status/SKILL.md +5 -2
- package/skills/zigrix-worker/SKILL.md +12 -4
- package/dist/dashboard/.next/static/chunks/app/page-25f54e54e74fb3af.js +0 -1
- package/rules/defaults/pro-zig.md +0 -238
- /package/dist/dashboard/.next/static/{2a4glWei05xr4Jg0Ly6cp → PT4hYxzrqxj-Zq4ZjtKNg}/_buildManifest.js +0 -0
- /package/dist/dashboard/.next/static/{2a4glWei05xr4Jg0Ly6cp → PT4hYxzrqxj-Zq4ZjtKNg}/_ssgManifest.js +0 -0
package/dist/onboard.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
1
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { addAgent } from './agents/registry.js';
|
|
7
|
+
import { inferStandardAgentRole, STANDARD_AGENT_ROLES } from './agents/roles.js';
|
|
8
|
+
import { LEGACY_DEFAULT_GATEWAY_URL, resolveAbsolutePath } from './config/defaults.js';
|
|
5
9
|
import { loadConfig, writeConfigFile, writeDefaultConfig } from './config/load.js';
|
|
6
10
|
import { ensureBaseState, resolvePaths } from './state/paths.js';
|
|
7
11
|
import { rebuildIndex } from './state/tasks.js';
|
|
8
12
|
// ─── OpenClaw detection ───────────────────────────────────────────────────────
|
|
9
13
|
export function detectOpenClawHome() {
|
|
14
|
+
const fallbackHome = process.env.HOME ?? os.homedir();
|
|
10
15
|
return process.env.OPENCLAW_HOME
|
|
11
|
-
?
|
|
12
|
-
: path.join(
|
|
16
|
+
? resolveAbsolutePath(process.env.OPENCLAW_HOME)
|
|
17
|
+
: path.join(fallbackHome, '.openclaw');
|
|
13
18
|
}
|
|
14
19
|
export function loadOpenClawConfig(openclawHome) {
|
|
15
20
|
const configPath = path.join(openclawHome, 'openclaw.json');
|
|
@@ -23,12 +28,120 @@ export function loadOpenClawConfig(openclawHome) {
|
|
|
23
28
|
return null;
|
|
24
29
|
}
|
|
25
30
|
}
|
|
31
|
+
function normalizeGatewayUrl(input) {
|
|
32
|
+
const trimmed = input?.trim() ?? '';
|
|
33
|
+
if (!trimmed)
|
|
34
|
+
return '';
|
|
35
|
+
const withScheme = /^[a-z]+:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
36
|
+
const parsed = new URL(withScheme);
|
|
37
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
38
|
+
throw new Error(`unsupported gateway url protocol: ${parsed.protocol}`);
|
|
39
|
+
}
|
|
40
|
+
return parsed.toString().replace(/\/+$/, '');
|
|
41
|
+
}
|
|
42
|
+
function coerceGatewayPort(value) {
|
|
43
|
+
const n = typeof value === 'string' && value.trim().length > 0 ? Number(value) : typeof value === 'number' ? value : NaN;
|
|
44
|
+
if (!Number.isFinite(n) || n <= 0 || n > 65535)
|
|
45
|
+
return null;
|
|
46
|
+
return Math.trunc(n);
|
|
47
|
+
}
|
|
48
|
+
export function resolveOpenClawGatewayUrl(openclawConfig) {
|
|
49
|
+
if (!openclawConfig || typeof openclawConfig !== 'object')
|
|
50
|
+
return null;
|
|
51
|
+
const directUrl = openclawConfig.gateway?.url;
|
|
52
|
+
if (typeof directUrl === 'string' && directUrl.trim().length > 0) {
|
|
53
|
+
return normalizeGatewayUrl(directUrl);
|
|
54
|
+
}
|
|
55
|
+
const port = coerceGatewayPort(openclawConfig.gateway?.port);
|
|
56
|
+
if (port) {
|
|
57
|
+
return `http://127.0.0.1:${port}`;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
export function resolveOpenClawGatewayToken(openclawConfig) {
|
|
62
|
+
const token = openclawConfig?.gateway?.auth?.token;
|
|
63
|
+
return typeof token === 'string' && token.trim().length > 0 ? token : null;
|
|
64
|
+
}
|
|
65
|
+
function shouldReplaceStoredGatewayUrl(stored, detected) {
|
|
66
|
+
const trimmed = stored.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
return Boolean(detected);
|
|
69
|
+
if (!detected)
|
|
70
|
+
return false;
|
|
71
|
+
return trimmed === LEGACY_DEFAULT_GATEWAY_URL;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Discover the openclaw binary path.
|
|
75
|
+
* Strategy order:
|
|
76
|
+
* 1. `which openclaw` — available in current PATH
|
|
77
|
+
* 2. Common nvm/volta/fnm managed paths
|
|
78
|
+
* 3. Well-known locations: /usr/local/bin, ~/.local/bin
|
|
79
|
+
* 4. Walk node_modules/.bin from openclaw home
|
|
80
|
+
*
|
|
81
|
+
* Returns the resolved absolute path or null.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveOpenClawBin(openclawHome) {
|
|
84
|
+
// Strategy 1: which/where
|
|
85
|
+
try {
|
|
86
|
+
const result = execSync('which openclaw', { encoding: 'utf8', timeout: 3000 }).trim();
|
|
87
|
+
if (result && fs.existsSync(result))
|
|
88
|
+
return path.resolve(result);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// not in PATH
|
|
92
|
+
}
|
|
93
|
+
// Strategy 2: nvm/volta/fnm managed global bin dirs
|
|
94
|
+
const home = process.env.HOME ?? '';
|
|
95
|
+
const nodeVersion = process.versions.node;
|
|
96
|
+
const majorMinorPatch = nodeVersion; // e.g., "25.5.0"
|
|
97
|
+
const nvmCandidate = path.join(home, '.nvm', 'versions', 'node', `v${majorMinorPatch}`, 'bin', 'openclaw');
|
|
98
|
+
if (fs.existsSync(nvmCandidate))
|
|
99
|
+
return path.resolve(nvmCandidate);
|
|
100
|
+
// Also try nvm current symlink
|
|
101
|
+
const nvmCurrent = path.join(home, '.nvm', 'current', 'bin', 'openclaw');
|
|
102
|
+
if (fs.existsSync(nvmCurrent))
|
|
103
|
+
return path.resolve(nvmCurrent);
|
|
104
|
+
// Volta
|
|
105
|
+
const voltaCandidate = path.join(home, '.volta', 'bin', 'openclaw');
|
|
106
|
+
if (fs.existsSync(voltaCandidate))
|
|
107
|
+
return path.resolve(voltaCandidate);
|
|
108
|
+
// fnm
|
|
109
|
+
const fnmCandidate = path.join(home, '.fnm', 'node-versions', `v${majorMinorPatch}`, 'installation', 'bin', 'openclaw');
|
|
110
|
+
if (fs.existsSync(fnmCandidate))
|
|
111
|
+
return path.resolve(fnmCandidate);
|
|
112
|
+
// Strategy 3: well-known system paths
|
|
113
|
+
for (const dir of ['/usr/local/bin', path.join(home, '.local', 'bin')]) {
|
|
114
|
+
const candidate = path.join(dir, 'openclaw');
|
|
115
|
+
if (fs.existsSync(candidate))
|
|
116
|
+
return path.resolve(candidate);
|
|
117
|
+
}
|
|
118
|
+
// Strategy 4: node global lib path (npm root -g)
|
|
119
|
+
try {
|
|
120
|
+
const globalRoot = execSync('npm root -g', { encoding: 'utf8', timeout: 3000 }).trim();
|
|
121
|
+
if (globalRoot) {
|
|
122
|
+
const candidate = path.join(path.dirname(globalRoot), 'bin', 'openclaw');
|
|
123
|
+
if (fs.existsSync(candidate))
|
|
124
|
+
return path.resolve(candidate);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// npm not available or timed out
|
|
129
|
+
}
|
|
130
|
+
// Strategy 5: from openclaw home
|
|
131
|
+
if (openclawHome) {
|
|
132
|
+
// Some installations place a bin reference inside the home dir
|
|
133
|
+
const homeBin = path.join(openclawHome, 'bin', 'openclaw');
|
|
134
|
+
if (fs.existsSync(homeBin))
|
|
135
|
+
return path.resolve(homeBin);
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
26
139
|
// ─── Agent filtering ──────────────────────────────────────────────────────────
|
|
27
140
|
export function filterAgents(agents) {
|
|
28
141
|
return agents.filter((a) => a.id !== 'main');
|
|
29
142
|
}
|
|
30
143
|
// ─── Agent registration ───────────────────────────────────────────────────────
|
|
31
|
-
export function registerAgents(config, agents) {
|
|
144
|
+
export function registerAgents(config, agents, roleAssignments) {
|
|
32
145
|
let current = structuredClone(config);
|
|
33
146
|
const registered = [];
|
|
34
147
|
const skipped = [];
|
|
@@ -38,7 +151,10 @@ export function registerAgents(config, agents) {
|
|
|
38
151
|
skipped.push(agent.id);
|
|
39
152
|
continue;
|
|
40
153
|
}
|
|
41
|
-
const role = agent.
|
|
154
|
+
const role = roleAssignments?.[agent.id] ?? inferStandardAgentRole({
|
|
155
|
+
agentId: agent.id,
|
|
156
|
+
theme: agent.identity?.theme ?? null,
|
|
157
|
+
});
|
|
42
158
|
const result = addAgent(current, {
|
|
43
159
|
id: agent.id,
|
|
44
160
|
role,
|
|
@@ -73,6 +189,28 @@ export function resolveBundledRulesDir() {
|
|
|
73
189
|
}
|
|
74
190
|
return null;
|
|
75
191
|
}
|
|
192
|
+
export function resolveRuleSeedSource(projectDir) {
|
|
193
|
+
const searched = [];
|
|
194
|
+
const base = projectDir?.trim();
|
|
195
|
+
if (base) {
|
|
196
|
+
const resolvedBase = resolveAbsolutePath(base);
|
|
197
|
+
const candidates = [
|
|
198
|
+
path.join(resolvedBase, 'rules', 'defaults'),
|
|
199
|
+
path.join(resolvedBase, 'rules'),
|
|
200
|
+
];
|
|
201
|
+
searched.push(...candidates);
|
|
202
|
+
for (const candidate of candidates) {
|
|
203
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
204
|
+
return { sourceDir: candidate, source: 'external', searched };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const bundled = resolveBundledRulesDir();
|
|
209
|
+
if (bundled) {
|
|
210
|
+
return { sourceDir: bundled, source: 'bundled', searched };
|
|
211
|
+
}
|
|
212
|
+
return { sourceDir: null, source: 'none', searched };
|
|
213
|
+
}
|
|
76
214
|
export function seedRules(sourceDir, targetDir) {
|
|
77
215
|
const copied = [];
|
|
78
216
|
const skipped = [];
|
|
@@ -102,9 +240,9 @@ export function seedRules(sourceDir, targetDir) {
|
|
|
102
240
|
return { copied, skipped, source: effectiveSource === sourceDir ? 'external' : 'bundled' };
|
|
103
241
|
}
|
|
104
242
|
// ─── PATH check and stabilization ─────────────────────────────────────────────
|
|
105
|
-
export
|
|
106
|
-
|
|
107
|
-
const dirs =
|
|
243
|
+
export const STABLE_SHELL_PATHS = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
|
|
244
|
+
export function checkZigrixInPath(opts) {
|
|
245
|
+
const dirs = opts?._overrideStablePaths ?? STABLE_SHELL_PATHS;
|
|
108
246
|
for (const dir of dirs) {
|
|
109
247
|
try {
|
|
110
248
|
if (!fs.existsSync(dir))
|
|
@@ -215,9 +353,10 @@ export function findUserBinDir() {
|
|
|
215
353
|
* 2. ~/.local/bin — user-local fallback; may not be in PATH, shows warning if so
|
|
216
354
|
*
|
|
217
355
|
* @param opts._overrideSystemBinDir - Override system bin dir selection (for testing)
|
|
356
|
+
* @param opts._overrideStablePaths - Override stable paths list (for testing)
|
|
218
357
|
*/
|
|
219
358
|
export function ensureZigrixInPath(opts) {
|
|
220
|
-
if (checkZigrixInPath()) {
|
|
359
|
+
if (checkZigrixInPath({ _overrideStablePaths: opts?._overrideStablePaths })) {
|
|
221
360
|
return { alreadyInPath: true, symlinkCreated: false, symlinkPath: null, warning: null };
|
|
222
361
|
}
|
|
223
362
|
const binEntry = resolveZigrixBin();
|
|
@@ -249,7 +388,15 @@ export function ensureZigrixInPath(opts) {
|
|
|
249
388
|
fs.chmodSync(symlinkPath, 0o755);
|
|
250
389
|
return { alreadyInPath: false, symlinkCreated: true, symlinkPath, warning: null };
|
|
251
390
|
}
|
|
252
|
-
catch {
|
|
391
|
+
catch (e) {
|
|
392
|
+
if (e.code === 'EACCES') {
|
|
393
|
+
return {
|
|
394
|
+
alreadyInPath: false,
|
|
395
|
+
symlinkCreated: false,
|
|
396
|
+
symlinkPath: null,
|
|
397
|
+
warning: `Cannot write to ${systemBinDir} (permission denied). Run:\n sudo ln -sfn $(which zigrix) ${symlinkPath}`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
253
400
|
// Fall through to user bin dir
|
|
254
401
|
}
|
|
255
402
|
}
|
|
@@ -302,6 +449,121 @@ export function ensureZigrixInPath(opts) {
|
|
|
302
449
|
: `Created zigrix at ${symlinkPath}, but ${userBinDir} is not in your PATH. Add it:\n export PATH="${userBinDir}:$PATH"`;
|
|
303
450
|
return { alreadyInPath: false, symlinkCreated: true, symlinkPath, warning };
|
|
304
451
|
}
|
|
452
|
+
// ─── OpenClaw PATH check and stabilization ────────────────────────────────────
|
|
453
|
+
export function checkOpenClawInPath(opts) {
|
|
454
|
+
const dirs = opts?._overrideStablePaths ?? STABLE_SHELL_PATHS;
|
|
455
|
+
for (const dir of dirs) {
|
|
456
|
+
try {
|
|
457
|
+
if (!fs.existsSync(dir))
|
|
458
|
+
continue;
|
|
459
|
+
const entries = fs.readdirSync(dir);
|
|
460
|
+
if (entries.includes('openclaw')) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// skip unreadable dirs
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Ensure openclaw is reachable from PATH.
|
|
472
|
+
* Same strategy as ensureZigrixInPath():
|
|
473
|
+
* 1. /usr/local/bin — stable path, accessible from non-login shells
|
|
474
|
+
* 2. ~/.local/bin — user-local fallback
|
|
475
|
+
*
|
|
476
|
+
* @param opts._overrideSystemBinDir - Override system bin dir selection (for testing)
|
|
477
|
+
* @param opts._overrideStablePaths - Override stable paths list (for testing)
|
|
478
|
+
*/
|
|
479
|
+
export function ensureOpenClawInPath(opts) {
|
|
480
|
+
if (checkOpenClawInPath({ _overrideStablePaths: opts?._overrideStablePaths })) {
|
|
481
|
+
return { alreadyInPath: true, symlinkCreated: false, symlinkPath: null, warning: null };
|
|
482
|
+
}
|
|
483
|
+
const binPath = resolveOpenClawBin();
|
|
484
|
+
if (!binPath) {
|
|
485
|
+
return {
|
|
486
|
+
alreadyInPath: false,
|
|
487
|
+
symlinkCreated: false,
|
|
488
|
+
symlinkPath: null,
|
|
489
|
+
warning: 'Could not locate openclaw binary. Ensure openclaw is installed via npm.',
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// ── Strategy 1: system bin dir (accessible from non-login shells) ──────────
|
|
493
|
+
const systemBinDir = opts !== undefined && '_overrideSystemBinDir' in opts
|
|
494
|
+
? opts._overrideSystemBinDir
|
|
495
|
+
: findSystemBinDir();
|
|
496
|
+
if (systemBinDir) {
|
|
497
|
+
const symlinkPath = path.join(systemBinDir, 'openclaw');
|
|
498
|
+
try {
|
|
499
|
+
// Remove stale entry if present
|
|
500
|
+
try {
|
|
501
|
+
if (fs.existsSync(symlinkPath) || fs.lstatSync(symlinkPath).isSymbolicLink()) {
|
|
502
|
+
fs.unlinkSync(symlinkPath);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
// doesn't exist — fine
|
|
507
|
+
}
|
|
508
|
+
fs.symlinkSync(binPath, symlinkPath);
|
|
509
|
+
fs.chmodSync(symlinkPath, 0o755);
|
|
510
|
+
return { alreadyInPath: false, symlinkCreated: true, symlinkPath, warning: null };
|
|
511
|
+
}
|
|
512
|
+
catch (e) {
|
|
513
|
+
if (e.code === 'EACCES') {
|
|
514
|
+
return {
|
|
515
|
+
alreadyInPath: false,
|
|
516
|
+
symlinkCreated: false,
|
|
517
|
+
symlinkPath: null,
|
|
518
|
+
warning: `Cannot write to ${systemBinDir} (permission denied). Run:\n sudo ln -sfn ${binPath} ${symlinkPath}`,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
// Fall through to user bin dir
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// ── Strategy 2: user-local bin dir ────────────────────────────────────────
|
|
525
|
+
const userBinDir = findUserBinDir();
|
|
526
|
+
try {
|
|
527
|
+
fs.mkdirSync(userBinDir, { recursive: true });
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return {
|
|
531
|
+
alreadyInPath: false,
|
|
532
|
+
symlinkCreated: false,
|
|
533
|
+
symlinkPath: null,
|
|
534
|
+
warning: `Could not create ${userBinDir}. Create it manually and add to PATH.`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
const symlinkPath = path.join(userBinDir, 'openclaw');
|
|
538
|
+
try {
|
|
539
|
+
// Remove existing if present (stale symlink)
|
|
540
|
+
if (fs.existsSync(symlinkPath) || fs.lstatSync(symlinkPath).isSymbolicLink()) {
|
|
541
|
+
fs.unlinkSync(symlinkPath);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// doesn't exist, fine
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
fs.symlinkSync(binPath, symlinkPath);
|
|
549
|
+
fs.chmodSync(symlinkPath, 0o755);
|
|
550
|
+
}
|
|
551
|
+
catch (e) {
|
|
552
|
+
return {
|
|
553
|
+
alreadyInPath: false,
|
|
554
|
+
symlinkCreated: false,
|
|
555
|
+
symlinkPath: null,
|
|
556
|
+
warning: `Failed to create symlink at ${symlinkPath}: ${e instanceof Error ? e.message : String(e)}`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
// Check if userBinDir is actually in PATH
|
|
560
|
+
const pathEnv = process.env.PATH ?? '';
|
|
561
|
+
const inPath = pathEnv.split(path.delimiter).some((d) => path.resolve(d) === path.resolve(userBinDir));
|
|
562
|
+
const warning = inPath
|
|
563
|
+
? null
|
|
564
|
+
: `Created openclaw at ${symlinkPath}, but ${userBinDir} is not in your PATH. Add it:\n export PATH="${userBinDir}:$PATH"`;
|
|
565
|
+
return { alreadyInPath: false, symlinkCreated: true, symlinkPath, warning };
|
|
566
|
+
}
|
|
305
567
|
// ─── OpenClaw skill registration ──────────────────────────────────────────────
|
|
306
568
|
/**
|
|
307
569
|
* Find the skills/ directory bundled with this zigrix package.
|
|
@@ -418,13 +680,137 @@ export async function promptAgentSelection(agents) {
|
|
|
418
680
|
return agents;
|
|
419
681
|
}
|
|
420
682
|
}
|
|
683
|
+
export async function promptAgentRoleAssignments(agents) {
|
|
684
|
+
const defaults = Object.fromEntries(agents.map((agent) => [agent.id, inferStandardAgentRole({ agentId: agent.id, theme: agent.identity?.theme ?? null })]));
|
|
685
|
+
if (agents.length === 0)
|
|
686
|
+
return defaults;
|
|
687
|
+
try {
|
|
688
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
689
|
+
const assignments = {};
|
|
690
|
+
for (const agent of agents) {
|
|
691
|
+
const suggested = defaults[agent.id];
|
|
692
|
+
let selected = [];
|
|
693
|
+
while (selected.length !== 1) {
|
|
694
|
+
selected = await checkbox({
|
|
695
|
+
message: `Assign role for ${agent.id} (space toggle + enter confirm, choose exactly one):`,
|
|
696
|
+
choices: STANDARD_AGENT_ROLES.map((role) => ({
|
|
697
|
+
name: `${role}${role === suggested ? ' (suggested)' : ''}`,
|
|
698
|
+
value: role,
|
|
699
|
+
checked: role === suggested,
|
|
700
|
+
})),
|
|
701
|
+
});
|
|
702
|
+
if (selected.length !== 1) {
|
|
703
|
+
console.log(`⚠️ Select exactly one role for ${agent.id}.`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
assignments[agent.id] = selected[0];
|
|
707
|
+
}
|
|
708
|
+
return assignments;
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
console.log('ℹ️ Non-interactive mode — inferring roles from agent ids/themes.');
|
|
712
|
+
return defaults;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
export async function promptWorkspaceBaseDir(defaultPath) {
|
|
716
|
+
try {
|
|
717
|
+
const { input } = await import('@inquirer/prompts');
|
|
718
|
+
const answer = await input({
|
|
719
|
+
message: 'Workspace directory',
|
|
720
|
+
default: defaultPath,
|
|
721
|
+
validate: (value) => {
|
|
722
|
+
if (!value || value.trim().length === 0)
|
|
723
|
+
return 'Workspace directory is required';
|
|
724
|
+
return true;
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
return resolveAbsolutePath((answer || defaultPath).trim());
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
console.log('ℹ️ Non-interactive mode — using configured workspace directory.');
|
|
731
|
+
return resolveAbsolutePath(defaultPath);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
export async function promptGatewayUrl(defaultValue) {
|
|
735
|
+
try {
|
|
736
|
+
const { input } = await import('@inquirer/prompts');
|
|
737
|
+
const answer = await input({
|
|
738
|
+
message: 'OpenClaw gateway URL (leave blank to skip)',
|
|
739
|
+
default: defaultValue,
|
|
740
|
+
validate: (value) => {
|
|
741
|
+
const trimmed = value.trim();
|
|
742
|
+
if (!trimmed)
|
|
743
|
+
return true;
|
|
744
|
+
try {
|
|
745
|
+
normalizeGatewayUrl(trimmed);
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
return error.message;
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
return normalizeGatewayUrl((answer || defaultValue).trim());
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
console.log('ℹ️ Non-interactive mode — skipping gateway URL prompt.');
|
|
757
|
+
return normalizeGatewayUrl(defaultValue);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
export function ensureOrchestratorId(config, preferredId) {
|
|
761
|
+
const next = structuredClone(config);
|
|
762
|
+
const participants = new Set(next.agents.orchestration.participants);
|
|
763
|
+
const excluded = new Set(next.agents.orchestration.excluded);
|
|
764
|
+
const participantMode = participants.size > 0;
|
|
765
|
+
const eligible = Object.entries(next.agents.registry)
|
|
766
|
+
.filter(([agentId, agent]) => {
|
|
767
|
+
if (!agent.enabled)
|
|
768
|
+
return false;
|
|
769
|
+
if (agent.role !== 'orchestrator')
|
|
770
|
+
return false;
|
|
771
|
+
if (excluded.has(agentId))
|
|
772
|
+
return false;
|
|
773
|
+
if (participantMode && !participants.has(agentId))
|
|
774
|
+
return false;
|
|
775
|
+
return true;
|
|
776
|
+
})
|
|
777
|
+
.map(([agentId]) => agentId)
|
|
778
|
+
.sort();
|
|
779
|
+
const requested = preferredId?.trim();
|
|
780
|
+
const current = next.agents.orchestration.orchestratorId;
|
|
781
|
+
if (requested) {
|
|
782
|
+
if (!eligible.includes(requested)) {
|
|
783
|
+
return {
|
|
784
|
+
config: next,
|
|
785
|
+
changed: false,
|
|
786
|
+
warning: `requested orchestrator '${requested}' is not eligible (eligible: ${eligible.join(', ') || 'none'})`,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
const changed = current !== requested;
|
|
790
|
+
next.agents.orchestration.orchestratorId = requested;
|
|
791
|
+
return { config: next, changed };
|
|
792
|
+
}
|
|
793
|
+
if (eligible.includes(current)) {
|
|
794
|
+
return { config: next, changed: false };
|
|
795
|
+
}
|
|
796
|
+
const fallback = eligible[0];
|
|
797
|
+
if (!fallback) {
|
|
798
|
+
return {
|
|
799
|
+
config: next,
|
|
800
|
+
changed: false,
|
|
801
|
+
warning: 'no eligible orchestrator role agent found. set one with --orchestrator-id after assigning roles.',
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
next.agents.orchestration.orchestratorId = fallback;
|
|
805
|
+
return { config: next, changed: true };
|
|
806
|
+
}
|
|
421
807
|
// ─── Ensure config (idempotent) ───────────────────────────────────────────────
|
|
422
808
|
function ensureConfig() {
|
|
423
809
|
const existing = loadConfig({});
|
|
424
810
|
if (existing.configPath && fs.existsSync(existing.configPath)) {
|
|
425
811
|
return { configPath: existing.configPath, isNew: false };
|
|
426
812
|
}
|
|
427
|
-
const configPath = writeDefaultConfig(
|
|
813
|
+
const configPath = writeDefaultConfig(false);
|
|
428
814
|
return { configPath, isNew: true };
|
|
429
815
|
}
|
|
430
816
|
// ─── Main onboard function ────────────────────────────────────────────────────
|
|
@@ -435,10 +821,22 @@ export async function runOnboard(options) {
|
|
|
435
821
|
if (!silent)
|
|
436
822
|
console.log(msg);
|
|
437
823
|
};
|
|
438
|
-
// 1. Ensure
|
|
824
|
+
// 1. Ensure config.paths.baseDir state (idempotent)
|
|
439
825
|
const { configPath } = ensureConfig();
|
|
440
826
|
const loaded = loadConfig({ configPath });
|
|
441
|
-
|
|
827
|
+
let nextConfig = structuredClone(loaded.config);
|
|
828
|
+
const configuredWorkspace = nextConfig.workspace.projectsBaseDir?.trim() || path.join(nextConfig.paths.baseDir, 'workspace');
|
|
829
|
+
const desiredWorkspace = options.projectsBaseDir
|
|
830
|
+
? resolveAbsolutePath(options.projectsBaseDir)
|
|
831
|
+
: options.yes
|
|
832
|
+
? resolveAbsolutePath(configuredWorkspace)
|
|
833
|
+
: await promptWorkspaceBaseDir(configuredWorkspace);
|
|
834
|
+
if (nextConfig.workspace.projectsBaseDir !== desiredWorkspace) {
|
|
835
|
+
nextConfig.workspace.projectsBaseDir = desiredWorkspace;
|
|
836
|
+
writeConfigFile(configPath, nextConfig);
|
|
837
|
+
log(`✅ Workspace base dir set to: ${desiredWorkspace}`);
|
|
838
|
+
}
|
|
839
|
+
const paths = resolvePaths(nextConfig);
|
|
442
840
|
ensureBaseState(paths);
|
|
443
841
|
rebuildIndex(paths);
|
|
444
842
|
// 2. Detect OpenClaw
|
|
@@ -456,6 +854,23 @@ export async function runOnboard(options) {
|
|
|
456
854
|
log(`⚠️ ${warnings[warnings.length - 1]}`);
|
|
457
855
|
}
|
|
458
856
|
}
|
|
857
|
+
const storedGatewayUrl = nextConfig.openclaw.gatewayUrl?.trim() ?? '';
|
|
858
|
+
const detectedGatewayUrl = resolveOpenClawGatewayUrl(openclawConfig);
|
|
859
|
+
const resolvedGatewayUrl = (() => {
|
|
860
|
+
if (typeof options.gatewayUrl === 'string' && options.gatewayUrl.trim().length > 0) {
|
|
861
|
+
return normalizeGatewayUrl(options.gatewayUrl);
|
|
862
|
+
}
|
|
863
|
+
if (storedGatewayUrl && !shouldReplaceStoredGatewayUrl(storedGatewayUrl, detectedGatewayUrl)) {
|
|
864
|
+
return normalizeGatewayUrl(storedGatewayUrl);
|
|
865
|
+
}
|
|
866
|
+
if (detectedGatewayUrl) {
|
|
867
|
+
return detectedGatewayUrl;
|
|
868
|
+
}
|
|
869
|
+
if (openclawExists && !options.yes) {
|
|
870
|
+
return '';
|
|
871
|
+
}
|
|
872
|
+
return storedGatewayUrl ? normalizeGatewayUrl(storedGatewayUrl) : '';
|
|
873
|
+
})();
|
|
459
874
|
// 3. Agent selection and registration
|
|
460
875
|
let agentsRegistered = [];
|
|
461
876
|
let agentsSkipped = [];
|
|
@@ -472,32 +887,64 @@ export async function runOnboard(options) {
|
|
|
472
887
|
selectedAgents = await promptAgentSelection(allAgents);
|
|
473
888
|
}
|
|
474
889
|
if (selectedAgents.length > 0) {
|
|
475
|
-
const
|
|
890
|
+
const roleAssignments = options.yes
|
|
891
|
+
? Object.fromEntries(selectedAgents.map((agent) => [agent.id, inferStandardAgentRole({ agentId: agent.id, theme: agent.identity?.theme ?? null })]))
|
|
892
|
+
: await promptAgentRoleAssignments(selectedAgents);
|
|
893
|
+
const result = registerAgents(nextConfig, selectedAgents, roleAssignments);
|
|
894
|
+
nextConfig = result.config;
|
|
476
895
|
agentsRegistered = result.registered;
|
|
477
896
|
agentsSkipped = result.skipped;
|
|
478
897
|
if (agentsRegistered.length > 0) {
|
|
479
|
-
writeConfigFile(configPath, result.config);
|
|
480
898
|
log(`✅ Registered agents: ${agentsRegistered.join(', ')}`);
|
|
481
899
|
}
|
|
482
900
|
if (agentsSkipped.length > 0) {
|
|
483
901
|
log(`⏭️ Already registered (skipped): ${agentsSkipped.join(', ')}`);
|
|
484
902
|
}
|
|
485
903
|
}
|
|
904
|
+
const orchResult = ensureOrchestratorId(nextConfig, options.orchestratorId);
|
|
905
|
+
nextConfig = orchResult.config;
|
|
906
|
+
if (orchResult.warning) {
|
|
907
|
+
warnings.push(orchResult.warning);
|
|
908
|
+
log(`⚠️ ${orchResult.warning}`);
|
|
909
|
+
}
|
|
910
|
+
if (agentsRegistered.length > 0 || orchResult.changed) {
|
|
911
|
+
writeConfigFile(configPath, nextConfig);
|
|
912
|
+
if (orchResult.changed) {
|
|
913
|
+
log(`✅ Orchestrator set to: ${nextConfig.agents.orchestration.orchestratorId}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (!openclawConfig && options.orchestratorId) {
|
|
918
|
+
const orchResult = ensureOrchestratorId(nextConfig, options.orchestratorId);
|
|
919
|
+
if (orchResult.warning) {
|
|
920
|
+
warnings.push(orchResult.warning);
|
|
921
|
+
log(`⚠️ ${orchResult.warning}`);
|
|
922
|
+
}
|
|
923
|
+
if (orchResult.changed) {
|
|
924
|
+
writeConfigFile(configPath, orchResult.config);
|
|
925
|
+
log(`✅ Orchestrator set to: ${orchResult.config.agents.orchestration.orchestratorId}`);
|
|
926
|
+
}
|
|
486
927
|
}
|
|
487
928
|
// 4. Seed rules
|
|
488
|
-
const projectDir = options.projectDir ?? process.cwd();
|
|
489
|
-
const rulesSourceDir = path.join(projectDir, 'orchestration', 'rules');
|
|
490
929
|
const rulesTargetDir = paths.rulesDir;
|
|
491
|
-
|
|
492
|
-
|
|
930
|
+
const ruleSeed = resolveRuleSeedSource(options.projectDir);
|
|
931
|
+
let rulesCopied = [];
|
|
932
|
+
let rulesSkipped = [];
|
|
933
|
+
if (!ruleSeed.sourceDir) {
|
|
934
|
+
warnings.push('No bundled rule templates available. Rules seeding skipped.');
|
|
493
935
|
log(`⚠️ ${warnings[warnings.length - 1]}`);
|
|
494
936
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
937
|
+
else {
|
|
938
|
+
if (options.projectDir && ruleSeed.source === 'bundled') {
|
|
939
|
+
log(`ℹ️ No external rule templates found under ${options.projectDir}; using bundled defaults.`);
|
|
940
|
+
}
|
|
941
|
+
({ copied: rulesCopied, skipped: rulesSkipped } = seedRules(ruleSeed.sourceDir, rulesTargetDir));
|
|
942
|
+
if (rulesCopied.length > 0) {
|
|
943
|
+
log(`✅ Rules copied: ${rulesCopied.join(', ')}`);
|
|
944
|
+
}
|
|
945
|
+
if (rulesSkipped.length > 0) {
|
|
946
|
+
log(`⏭️ Rules already exist (skipped): ${rulesSkipped.join(', ')}`);
|
|
947
|
+
}
|
|
501
948
|
}
|
|
502
949
|
// 5. Stabilize PATH — ensure zigrix is reachable
|
|
503
950
|
const pathResult = ensureZigrixInPath();
|
|
@@ -511,7 +958,59 @@ export async function runOnboard(options) {
|
|
|
511
958
|
warnings.push(pathResult.warning);
|
|
512
959
|
log(`⚠️ ${pathResult.warning}`);
|
|
513
960
|
}
|
|
514
|
-
// 6.
|
|
961
|
+
// 6. Detect and persist openclaw binary path
|
|
962
|
+
let openclawBinPath = null;
|
|
963
|
+
if (openclawExists) {
|
|
964
|
+
openclawBinPath = resolveOpenClawBin(openclawHome);
|
|
965
|
+
if (openclawBinPath) {
|
|
966
|
+
log(`✅ OpenClaw binary found: ${openclawBinPath}`);
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
warnings.push('openclaw binary not found. Dashboard conversation features may be limited.');
|
|
970
|
+
log(`⚠️ ${warnings[warnings.length - 1]}`);
|
|
971
|
+
}
|
|
972
|
+
// Persist openclaw integration config (home, binPath, gatewayUrl)
|
|
973
|
+
const currentConfig = loadConfig({ configPath }).config;
|
|
974
|
+
// Gateway URL: explicit flag > autodiscovery > interactive prompt > stored value
|
|
975
|
+
let finalGatewayUrl = resolvedGatewayUrl;
|
|
976
|
+
if (!finalGatewayUrl && openclawExists && !options.yes) {
|
|
977
|
+
const suggested = detectedGatewayUrl || '';
|
|
978
|
+
finalGatewayUrl = await promptGatewayUrl(suggested);
|
|
979
|
+
}
|
|
980
|
+
if (finalGatewayUrl) {
|
|
981
|
+
log(`✅ OpenClaw gateway URL: ${finalGatewayUrl}`);
|
|
982
|
+
}
|
|
983
|
+
else if (openclawExists) {
|
|
984
|
+
warnings.push('OpenClaw gateway URL not configured. Dashboard gateway features may be limited. Set later with `zigrix configure --section openclaw` or `zigrix onboard --gateway-url <url>`.');
|
|
985
|
+
log(`⚠️ ${warnings[warnings.length - 1]}`);
|
|
986
|
+
}
|
|
987
|
+
const needsUpdate = currentConfig.openclaw.home !== openclawHome ||
|
|
988
|
+
currentConfig.openclaw.binPath !== openclawBinPath ||
|
|
989
|
+
currentConfig.openclaw.gatewayUrl !== (finalGatewayUrl || '');
|
|
990
|
+
if (needsUpdate) {
|
|
991
|
+
currentConfig.openclaw.home = openclawHome;
|
|
992
|
+
currentConfig.openclaw.binPath = openclawBinPath;
|
|
993
|
+
currentConfig.openclaw.gatewayUrl = finalGatewayUrl || '';
|
|
994
|
+
writeConfigFile(configPath, currentConfig);
|
|
995
|
+
log(`✅ OpenClaw config persisted (home: ${openclawHome}, bin: ${openclawBinPath ?? 'not found'}, gateway: ${finalGatewayUrl || 'not set'})`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// 7. Stabilize PATH — ensure openclaw is reachable from non-login shells
|
|
999
|
+
let openclawPathResult = { alreadyInPath: false, symlinkCreated: false, symlinkPath: null, warning: null };
|
|
1000
|
+
if (openclawBinPath) {
|
|
1001
|
+
openclawPathResult = ensureOpenClawInPath();
|
|
1002
|
+
if (openclawPathResult.symlinkCreated) {
|
|
1003
|
+
log(`✅ openclaw symlinked to ${openclawPathResult.symlinkPath}`);
|
|
1004
|
+
}
|
|
1005
|
+
else if (openclawPathResult.alreadyInPath) {
|
|
1006
|
+
log('✅ openclaw already in PATH');
|
|
1007
|
+
}
|
|
1008
|
+
if (openclawPathResult.warning) {
|
|
1009
|
+
warnings.push(openclawPathResult.warning);
|
|
1010
|
+
log(`⚠️ ${openclawPathResult.warning}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// 8. Register OpenClaw skills (symlink skill packs)
|
|
515
1014
|
let skillsRegistered = [];
|
|
516
1015
|
let skillsSkipped = [];
|
|
517
1016
|
let skillsFailed = [];
|
|
@@ -542,10 +1041,13 @@ export async function runOnboard(options) {
|
|
|
542
1041
|
ok: true,
|
|
543
1042
|
action: 'onboard',
|
|
544
1043
|
baseDir: paths.baseDir,
|
|
1044
|
+
workspaceBaseDir: nextConfig.workspace.projectsBaseDir,
|
|
545
1045
|
configPath,
|
|
546
1046
|
paths,
|
|
547
1047
|
openclawDetected: openclawExists,
|
|
548
1048
|
openclawHome,
|
|
1049
|
+
openclawBinPath,
|
|
1050
|
+
openclawGatewayUrl: resolvedGatewayUrl || null,
|
|
549
1051
|
agentsRegistered,
|
|
550
1052
|
agentsSkipped,
|
|
551
1053
|
rulesCopied,
|
|
@@ -554,9 +1056,11 @@ export async function runOnboard(options) {
|
|
|
554
1056
|
skillsSkipped,
|
|
555
1057
|
skillsFailed,
|
|
556
1058
|
pathStabilized: pathResult,
|
|
1059
|
+
openclawPathStabilized: openclawPathResult,
|
|
557
1060
|
warnings,
|
|
558
1061
|
checks: {
|
|
559
1062
|
zigrixInPath: pathResult.alreadyInPath || pathResult.symlinkCreated,
|
|
1063
|
+
openclawInPath: openclawPathResult.alreadyInPath || openclawPathResult.symlinkCreated,
|
|
560
1064
|
openclawSkillsDir: openclawSkillsDirExists,
|
|
561
1065
|
},
|
|
562
1066
|
};
|