zigrix 0.1.0-alpha.9 → 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.
Files changed (82) hide show
  1. package/README.md +85 -182
  2. package/dist/agents/registry.js +14 -0
  3. package/dist/config/defaults.d.ts +87 -6
  4. package/dist/config/defaults.js +82 -51
  5. package/dist/config/load.d.ts +5 -3
  6. package/dist/config/load.js +69 -30
  7. package/dist/config/schema.d.ts +21 -1
  8. package/dist/config/schema.js +15 -1
  9. package/dist/configure.d.ts +1 -0
  10. package/dist/configure.js +24 -14
  11. package/dist/dashboard/.next/BUILD_ID +1 -1
  12. package/dist/dashboard/.next/app-build-manifest.json +21 -21
  13. package/dist/dashboard/.next/app-path-routes-manifest.json +7 -7
  14. package/dist/dashboard/.next/build-manifest.json +2 -2
  15. package/dist/dashboard/.next/prerender-manifest.json +10 -10
  16. package/dist/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/dist/dashboard/.next/server/app/_not-found.html +1 -1
  18. package/dist/dashboard/.next/server/app/_not-found.rsc +1 -1
  19. package/dist/dashboard/.next/server/app/api/auth/login/route.js +1 -1
  20. package/dist/dashboard/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
  21. package/dist/dashboard/.next/server/app/api/auth/logout/route.js +1 -1
  22. package/dist/dashboard/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  23. package/dist/dashboard/.next/server/app/api/auth/session/route.js +1 -1
  24. package/dist/dashboard/.next/server/app/api/auth/session/route_client-reference-manifest.js +1 -1
  25. package/dist/dashboard/.next/server/app/api/auth/setup/route.js +1 -1
  26. package/dist/dashboard/.next/server/app/api/auth/setup/route_client-reference-manifest.js +1 -1
  27. package/dist/dashboard/.next/server/app/api/overview/route_client-reference-manifest.js +1 -1
  28. package/dist/dashboard/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
  29. package/dist/dashboard/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
  30. package/dist/dashboard/.next/server/app/api/tasks/[taskId]/conversation/route_client-reference-manifest.js +1 -1
  31. package/dist/dashboard/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  32. package/dist/dashboard/.next/server/app/login/page_client-reference-manifest.js +1 -1
  33. package/dist/dashboard/.next/server/app/login.html +1 -1
  34. package/dist/dashboard/.next/server/app/login.rsc +1 -1
  35. package/dist/dashboard/.next/server/app/page.js +2 -2
  36. package/dist/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  37. package/dist/dashboard/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  38. package/dist/dashboard/.next/server/app/setup.html +1 -1
  39. package/dist/dashboard/.next/server/app/setup.rsc +1 -1
  40. package/dist/dashboard/.next/server/app-paths-manifest.json +7 -7
  41. package/dist/dashboard/.next/server/chunks/972.js +1 -1
  42. package/dist/dashboard/.next/server/functions-config-manifest.json +4 -4
  43. package/dist/dashboard/.next/server/middleware.js +1 -1
  44. package/dist/dashboard/.next/server/pages/404.html +1 -1
  45. package/dist/dashboard/.next/server/pages/500.html +1 -1
  46. package/dist/dashboard/.next/static/chunks/app/page-0314989c31e18b4b.js +1 -0
  47. package/dist/dashboard/.next/static/css/{94d75aff24d0c077.css → c3a7306cb2ba3f6c.css} +1 -1
  48. package/dist/dashboard.js +47 -0
  49. package/dist/doctor.js +28 -5
  50. package/dist/index.js +170 -170
  51. package/dist/onboard.d.ts +60 -0
  52. package/dist/onboard.js +403 -18
  53. package/dist/orchestration/dispatch.d.ts +1 -0
  54. package/dist/orchestration/dispatch.js +17 -5
  55. package/dist/orchestration/evidence.js +18 -5
  56. package/dist/orchestration/finalize.d.ts +1 -0
  57. package/dist/orchestration/finalize.js +5 -3
  58. package/dist/orchestration/report.js +4 -1
  59. package/dist/orchestration/worker.d.ts +1 -1
  60. package/dist/orchestration/worker.js +53 -18
  61. package/dist/state/tasks.d.ts +5 -0
  62. package/dist/state/tasks.js +7 -0
  63. package/package.json +23 -2
  64. package/rules/defaults/README.md +9 -9
  65. package/rules/defaults/{back-zig.md → backend-agent.md} +4 -4
  66. package/rules/defaults/{front-zig.md → frontend-agent.md} +4 -4
  67. package/rules/defaults/orchestrator-agent.md +261 -0
  68. package/rules/defaults/{qa-zig.md → qa-agent.md} +11 -11
  69. package/rules/defaults/{sec-zig.md → security-agent.md} +4 -4
  70. package/rules/defaults/{sys-zig.md → system-agent.md} +8 -9
  71. package/rules/defaults/worker-common.md +25 -19
  72. package/skills/zigrix-doctor/SKILL.md +4 -2
  73. package/skills/zigrix-evidence/SKILL.md +7 -3
  74. package/skills/zigrix-main-agent-guide/SKILL.md +38 -28
  75. package/skills/zigrix-shared/SKILL.md +27 -3
  76. package/skills/zigrix-task-create/SKILL.md +8 -2
  77. package/skills/zigrix-task-status/SKILL.md +5 -2
  78. package/skills/zigrix-worker/SKILL.md +12 -4
  79. package/dist/dashboard/.next/static/chunks/app/page-25f54e54e74fb3af.js +0 -1
  80. package/rules/defaults/pro-zig.md +0 -238
  81. /package/dist/dashboard/.next/static/{TlUj0t8APzTccK13DVZZW → PT4hYxzrqxj-Zq4ZjtKNg}/_buildManifest.js +0 -0
  82. /package/dist/dashboard/.next/static/{TlUj0t8APzTccK13DVZZW → PT4hYxzrqxj-Zq4ZjtKNg}/_ssgManifest.js +0 -0
package/dist/onboard.d.ts CHANGED
@@ -14,6 +14,23 @@ export interface OpenClawConfig {
14
14
  agents?: {
15
15
  list?: OpenClawAgent[];
16
16
  };
17
+ gateway?: {
18
+ port?: number | string;
19
+ bind?: string;
20
+ mode?: string;
21
+ auth?: {
22
+ token?: string;
23
+ };
24
+ };
25
+ plugins?: {
26
+ entries?: {
27
+ 'device-pair'?: {
28
+ config?: {
29
+ publicUrl?: string;
30
+ };
31
+ };
32
+ };
33
+ };
17
34
  }
18
35
  export type AgentRoleAssignments = Record<string, StandardAgentRole>;
19
36
  export interface PathStabilizeResult {
@@ -31,10 +48,13 @@ export interface OnboardResult {
31
48
  ok: boolean;
32
49
  action: string;
33
50
  baseDir: string;
51
+ workspaceBaseDir: string;
34
52
  configPath: string;
35
53
  paths: ZigrixPaths;
36
54
  openclawDetected: boolean;
37
55
  openclawHome: string;
56
+ openclawBinPath: string | null;
57
+ openclawGatewayUrl: string | null;
38
58
  agentsRegistered: string[];
39
59
  agentsSkipped: string[];
40
60
  rulesCopied: string[];
@@ -43,20 +63,37 @@ export interface OnboardResult {
43
63
  skillsSkipped: string[];
44
64
  skillsFailed: string[];
45
65
  pathStabilized: PathStabilizeResult;
66
+ openclawPathStabilized: PathStabilizeResult;
46
67
  warnings: string[];
47
68
  checks: {
48
69
  zigrixInPath: boolean;
70
+ openclawInPath: boolean;
49
71
  openclawSkillsDir: boolean;
50
72
  };
51
73
  }
52
74
  export interface RunOnboardOptions {
53
75
  yes?: boolean;
54
76
  projectDir?: string;
77
+ projectsBaseDir?: string;
55
78
  orchestratorId?: string;
79
+ gatewayUrl?: string;
56
80
  silent?: boolean;
57
81
  }
58
82
  export declare function detectOpenClawHome(): string;
59
83
  export declare function loadOpenClawConfig(openclawHome: string): OpenClawConfig | null;
84
+ export declare function resolveOpenClawGatewayUrl(openclawConfig: OpenClawConfig | null): string | null;
85
+ export declare function resolveOpenClawGatewayToken(openclawConfig: OpenClawConfig | null): string | null;
86
+ /**
87
+ * Discover the openclaw binary path.
88
+ * Strategy order:
89
+ * 1. `which openclaw` — available in current PATH
90
+ * 2. Common nvm/volta/fnm managed paths
91
+ * 3. Well-known locations: /usr/local/bin, ~/.local/bin
92
+ * 4. Walk node_modules/.bin from openclaw home
93
+ *
94
+ * Returns the resolved absolute path or null.
95
+ */
96
+ export declare function resolveOpenClawBin(openclawHome?: string): string | null;
60
97
  export declare function filterAgents(agents: OpenClawAgent[]): OpenClawAgent[];
61
98
  export declare function registerAgents(config: ZigrixConfig, agents: OpenClawAgent[], roleAssignments?: AgentRoleAssignments): {
62
99
  config: ZigrixConfig;
@@ -67,6 +104,11 @@ export declare function registerAgents(config: ZigrixConfig, agents: OpenClawAge
67
104
  * Find the bundled default rules directory shipped with the zigrix package.
68
105
  */
69
106
  export declare function resolveBundledRulesDir(): string | null;
107
+ export declare function resolveRuleSeedSource(projectDir?: string): {
108
+ sourceDir: string | null;
109
+ source: 'external' | 'bundled' | 'none';
110
+ searched: string[];
111
+ };
70
112
  export declare function seedRules(sourceDir: string, targetDir: string): {
71
113
  copied: string[];
72
114
  skipped: string[];
@@ -106,6 +148,22 @@ export declare function ensureZigrixInPath(opts?: {
106
148
  _overrideSystemBinDir?: string | null;
107
149
  _overrideStablePaths?: string[];
108
150
  }): PathStabilizeResult;
151
+ export declare function checkOpenClawInPath(opts?: {
152
+ _overrideStablePaths?: string[];
153
+ }): boolean;
154
+ /**
155
+ * Ensure openclaw is reachable from PATH.
156
+ * Same strategy as ensureZigrixInPath():
157
+ * 1. /usr/local/bin — stable path, accessible from non-login shells
158
+ * 2. ~/.local/bin — user-local fallback
159
+ *
160
+ * @param opts._overrideSystemBinDir - Override system bin dir selection (for testing)
161
+ * @param opts._overrideStablePaths - Override stable paths list (for testing)
162
+ */
163
+ export declare function ensureOpenClawInPath(opts?: {
164
+ _overrideSystemBinDir?: string | null;
165
+ _overrideStablePaths?: string[];
166
+ }): PathStabilizeResult;
109
167
  /**
110
168
  * Find the skills/ directory bundled with this zigrix package.
111
169
  */
@@ -122,6 +180,8 @@ export declare function registerSkills(openclawHome: string): SkillRegistrationR
122
180
  */
123
181
  export declare function promptAgentSelection(agents: OpenClawAgent[]): Promise<OpenClawAgent[]>;
124
182
  export declare function promptAgentRoleAssignments(agents: OpenClawAgent[]): Promise<AgentRoleAssignments>;
183
+ export declare function promptWorkspaceBaseDir(defaultPath: string): Promise<string>;
184
+ export declare function promptGatewayUrl(defaultValue: string): Promise<string>;
125
185
  export declare function ensureOrchestratorId(config: ZigrixConfig, preferredId?: string): {
126
186
  config: ZigrixConfig;
127
187
  changed: boolean;
package/dist/onboard.js CHANGED
@@ -1,16 +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';
5
7
  import { inferStandardAgentRole, STANDARD_AGENT_ROLES } from './agents/roles.js';
8
+ import { LEGACY_DEFAULT_GATEWAY_URL, resolveAbsolutePath } from './config/defaults.js';
6
9
  import { loadConfig, writeConfigFile, writeDefaultConfig } from './config/load.js';
7
10
  import { ensureBaseState, resolvePaths } from './state/paths.js';
8
11
  import { rebuildIndex } from './state/tasks.js';
9
12
  // ─── OpenClaw detection ───────────────────────────────────────────────────────
10
13
  export function detectOpenClawHome() {
14
+ const fallbackHome = process.env.HOME ?? os.homedir();
11
15
  return process.env.OPENCLAW_HOME
12
- ? path.resolve(process.env.OPENCLAW_HOME)
13
- : path.join(process.env.HOME ?? '~', '.openclaw');
16
+ ? resolveAbsolutePath(process.env.OPENCLAW_HOME)
17
+ : path.join(fallbackHome, '.openclaw');
14
18
  }
15
19
  export function loadOpenClawConfig(openclawHome) {
16
20
  const configPath = path.join(openclawHome, 'openclaw.json');
@@ -24,6 +28,114 @@ export function loadOpenClawConfig(openclawHome) {
24
28
  return null;
25
29
  }
26
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
+ }
27
139
  // ─── Agent filtering ──────────────────────────────────────────────────────────
28
140
  export function filterAgents(agents) {
29
141
  return agents.filter((a) => a.id !== 'main');
@@ -77,6 +189,28 @@ export function resolveBundledRulesDir() {
77
189
  }
78
190
  return null;
79
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
+ }
80
214
  export function seedRules(sourceDir, targetDir) {
81
215
  const copied = [];
82
216
  const skipped = [];
@@ -315,6 +449,121 @@ export function ensureZigrixInPath(opts) {
315
449
  : `Created zigrix at ${symlinkPath}, but ${userBinDir} is not in your PATH. Add it:\n export PATH="${userBinDir}:$PATH"`;
316
450
  return { alreadyInPath: false, symlinkCreated: true, symlinkPath, warning };
317
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
+ }
318
567
  // ─── OpenClaw skill registration ──────────────────────────────────────────────
319
568
  /**
320
569
  * Find the skills/ directory bundled with this zigrix package.
@@ -463,6 +712,51 @@ export async function promptAgentRoleAssignments(agents) {
463
712
  return defaults;
464
713
  }
465
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
+ }
466
760
  export function ensureOrchestratorId(config, preferredId) {
467
761
  const next = structuredClone(config);
468
762
  const participants = new Set(next.agents.orchestration.participants);
@@ -516,7 +810,7 @@ function ensureConfig() {
516
810
  if (existing.configPath && fs.existsSync(existing.configPath)) {
517
811
  return { configPath: existing.configPath, isNew: false };
518
812
  }
519
- const configPath = writeDefaultConfig(undefined, false);
813
+ const configPath = writeDefaultConfig(false);
520
814
  return { configPath, isNew: true };
521
815
  }
522
816
  // ─── Main onboard function ────────────────────────────────────────────────────
@@ -527,10 +821,22 @@ export async function runOnboard(options) {
527
821
  if (!silent)
528
822
  console.log(msg);
529
823
  };
530
- // 1. Ensure ~/.zigrix base state (idempotent)
824
+ // 1. Ensure config.paths.baseDir state (idempotent)
531
825
  const { configPath } = ensureConfig();
532
826
  const loaded = loadConfig({ configPath });
533
- const paths = resolvePaths(loaded.config);
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);
534
840
  ensureBaseState(paths);
535
841
  rebuildIndex(paths);
536
842
  // 2. Detect OpenClaw
@@ -548,6 +854,23 @@ export async function runOnboard(options) {
548
854
  log(`⚠️ ${warnings[warnings.length - 1]}`);
549
855
  }
550
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
+ })();
551
874
  // 3. Agent selection and registration
552
875
  let agentsRegistered = [];
553
876
  let agentsSkipped = [];
@@ -563,7 +886,6 @@ export async function runOnboard(options) {
563
886
  else {
564
887
  selectedAgents = await promptAgentSelection(allAgents);
565
888
  }
566
- let nextConfig = loaded.config;
567
889
  if (selectedAgents.length > 0) {
568
890
  const roleAssignments = options.yes
569
891
  ? Object.fromEntries(selectedAgents.map((agent) => [agent.id, inferStandardAgentRole({ agentId: agent.id, theme: agent.identity?.theme ?? null })]))
@@ -593,7 +915,7 @@ export async function runOnboard(options) {
593
915
  }
594
916
  }
595
917
  if (!openclawConfig && options.orchestratorId) {
596
- const orchResult = ensureOrchestratorId(loaded.config, options.orchestratorId);
918
+ const orchResult = ensureOrchestratorId(nextConfig, options.orchestratorId);
597
919
  if (orchResult.warning) {
598
920
  warnings.push(orchResult.warning);
599
921
  log(`⚠️ ${orchResult.warning}`);
@@ -604,19 +926,25 @@ export async function runOnboard(options) {
604
926
  }
605
927
  }
606
928
  // 4. Seed rules
607
- const projectDir = options.projectDir ?? process.cwd();
608
- const rulesSourceDir = path.join(projectDir, 'orchestration', 'rules');
609
929
  const rulesTargetDir = paths.rulesDir;
610
- if (!fs.existsSync(rulesSourceDir)) {
611
- warnings.push(`orchestration/rules/ not found at ${rulesSourceDir}. Rules seeding skipped.`);
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.');
612
935
  log(`⚠️ ${warnings[warnings.length - 1]}`);
613
936
  }
614
- const { copied: rulesCopied, skipped: rulesSkipped } = seedRules(rulesSourceDir, rulesTargetDir);
615
- if (rulesCopied.length > 0) {
616
- log(`✅ Rules copied: ${rulesCopied.join(', ')}`);
617
- }
618
- if (rulesSkipped.length > 0) {
619
- log(`⏭️ Rules already exist (skipped): ${rulesSkipped.join(', ')}`);
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
+ }
620
948
  }
621
949
  // 5. Stabilize PATH — ensure zigrix is reachable
622
950
  const pathResult = ensureZigrixInPath();
@@ -630,7 +958,59 @@ export async function runOnboard(options) {
630
958
  warnings.push(pathResult.warning);
631
959
  log(`⚠️ ${pathResult.warning}`);
632
960
  }
633
- // 6. Register OpenClaw skills (symlink skill packs)
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)
634
1014
  let skillsRegistered = [];
635
1015
  let skillsSkipped = [];
636
1016
  let skillsFailed = [];
@@ -661,10 +1041,13 @@ export async function runOnboard(options) {
661
1041
  ok: true,
662
1042
  action: 'onboard',
663
1043
  baseDir: paths.baseDir,
1044
+ workspaceBaseDir: nextConfig.workspace.projectsBaseDir,
664
1045
  configPath,
665
1046
  paths,
666
1047
  openclawDetected: openclawExists,
667
1048
  openclawHome,
1049
+ openclawBinPath,
1050
+ openclawGatewayUrl: resolvedGatewayUrl || null,
668
1051
  agentsRegistered,
669
1052
  agentsSkipped,
670
1053
  rulesCopied,
@@ -673,9 +1056,11 @@ export async function runOnboard(options) {
673
1056
  skillsSkipped,
674
1057
  skillsFailed,
675
1058
  pathStabilized: pathResult,
1059
+ openclawPathStabilized: openclawPathResult,
676
1060
  warnings,
677
1061
  checks: {
678
1062
  zigrixInPath: pathResult.alreadyInPath || pathResult.symlinkCreated,
1063
+ openclawInPath: openclawPathResult.alreadyInPath || openclawPathResult.symlinkCreated,
679
1064
  openclawSkillsDir: openclawSkillsDirExists,
680
1065
  },
681
1066
  };
@@ -1,5 +1,6 @@
1
1
  import type { ZigrixConfig } from '../config/schema.js';
2
2
  import { type ZigrixPaths } from '../state/paths.js';
3
+ export declare function resolveConfiguredProjectDir(config: ZigrixConfig, explicitProjectDir?: string): string | undefined;
3
4
  export declare function dispatchTask(paths: ZigrixPaths, config: ZigrixConfig, params: {
4
5
  title: string;
5
6
  description: string;