@openreef/cli 0.1.0 → 0.2.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 (62) hide show
  1. package/README.md +14 -7
  2. package/dist/commands/init.d.ts +1 -1
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +1 -1
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/install.d.ts +13 -0
  7. package/dist/commands/install.d.ts.map +1 -0
  8. package/dist/commands/install.js +560 -0
  9. package/dist/commands/install.js.map +1 -0
  10. package/dist/commands/list.d.ts +5 -0
  11. package/dist/commands/list.d.ts.map +1 -0
  12. package/dist/commands/list.js +81 -0
  13. package/dist/commands/list.js.map +1 -0
  14. package/dist/commands/status.d.ts +8 -0
  15. package/dist/commands/status.d.ts.map +1 -0
  16. package/dist/commands/status.js +198 -0
  17. package/dist/commands/status.js.map +1 -0
  18. package/dist/commands/uninstall.d.ts +8 -0
  19. package/dist/commands/uninstall.d.ts.map +1 -0
  20. package/dist/commands/uninstall.js +132 -0
  21. package/dist/commands/uninstall.js.map +1 -0
  22. package/dist/core/agents-md-generator.d.ts +3 -0
  23. package/dist/core/agents-md-generator.d.ts.map +1 -0
  24. package/dist/core/agents-md-generator.js +24 -0
  25. package/dist/core/agents-md-generator.js.map +1 -0
  26. package/dist/core/config-patcher.d.ts +32 -0
  27. package/dist/core/config-patcher.d.ts.map +1 -0
  28. package/dist/core/config-patcher.js +147 -0
  29. package/dist/core/config-patcher.js.map +1 -0
  30. package/dist/core/device-identity.d.ts +37 -0
  31. package/dist/core/device-identity.d.ts.map +1 -0
  32. package/dist/core/device-identity.js +135 -0
  33. package/dist/core/device-identity.js.map +1 -0
  34. package/dist/core/gateway-client.d.ts +98 -0
  35. package/dist/core/gateway-client.d.ts.map +1 -0
  36. package/dist/core/gateway-client.js +238 -0
  37. package/dist/core/gateway-client.js.map +1 -0
  38. package/dist/core/openclaw-paths.d.ts +17 -0
  39. package/dist/core/openclaw-paths.d.ts.map +1 -0
  40. package/dist/core/openclaw-paths.js +175 -0
  41. package/dist/core/openclaw-paths.js.map +1 -0
  42. package/dist/core/state-manager.d.ts +7 -0
  43. package/dist/core/state-manager.d.ts.map +1 -0
  44. package/dist/core/state-manager.js +61 -0
  45. package/dist/core/state-manager.js.map +1 -0
  46. package/dist/core/structural-validator.js +4 -4
  47. package/dist/core/structural-validator.js.map +1 -1
  48. package/dist/core/variable-resolver.d.ts +1 -0
  49. package/dist/core/variable-resolver.d.ts.map +1 -1
  50. package/dist/core/variable-resolver.js +21 -10
  51. package/dist/core/variable-resolver.js.map +1 -1
  52. package/dist/index.js +72 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/types/manifest.d.ts +1 -1
  55. package/dist/types/manifest.d.ts.map +1 -1
  56. package/dist/types/state.d.ts +41 -0
  57. package/dist/types/state.d.ts.map +1 -0
  58. package/dist/types/state.js +2 -0
  59. package/dist/types/state.js.map +1 -0
  60. package/package.json +6 -3
  61. package/schema/reef.schema.json +2 -2
  62. package/template/reef.json +1 -1
package/README.md CHANGED
@@ -44,8 +44,8 @@ my-formation/
44
44
  | Type | Agents | Use Case |
45
45
  |------|--------|----------|
46
46
  | `solo` | 1 | Single agent with curated personality, tools, and cron |
47
- | `team` | 2–5 | Defined roles with explicit communication topology |
48
- | `swarm` | 6+ | Large-scale or dynamically-spawning agent patterns |
47
+ | `shoal` | 2–5 | Defined roles with explicit communication topology |
48
+ | `school` | 6+ | Large-scale or dynamically-spawning agent patterns |
49
49
 
50
50
  ## Quick Start
51
51
 
@@ -83,7 +83,7 @@ A single JSON file declares your entire formation:
83
83
  ```json
84
84
  {
85
85
  "reef": "1.0",
86
- "type": "team",
86
+ "type": "shoal",
87
87
  "name": "my-formation",
88
88
  "version": "1.0.0",
89
89
  "description": "A research team with a manager and researcher",
@@ -126,6 +126,8 @@ Variables support `{{VARIABLE_NAME}}` interpolation across all text files. Sensi
126
126
 
127
127
  ## CLI
128
128
 
129
+ ### Offline Commands
130
+
129
131
  | Command | Description |
130
132
  |---------|-------------|
131
133
  | `reef init [name]` | Scaffold a new formation from the bundled template |
@@ -133,17 +135,22 @@ Variables support `{{VARIABLE_NAME}}` interpolation across all text files. Sensi
133
135
  | `reef validate <path>` | Run schema and structural validation on a formation |
134
136
  | `reef pack <path>` | Package a formation into a `.tar.gz` archive |
135
137
 
136
- **Planned** (requires running OpenClaw):
138
+ ### Online Commands (requires running OpenClaw)
137
139
 
138
140
  | Command | Description |
139
141
  |---------|-------------|
140
142
  | `reef install <path>` | Deploy a formation to OpenClaw |
143
+ | `reef uninstall <identifier>` | Remove a formation and all its resources |
144
+ | `reef list` | List installed formations |
145
+ | `reef status <identifier>` | Show status of a deployed formation |
146
+
147
+ **Planned:**
148
+
149
+ | Command | Description |
150
+ |---------|-------------|
141
151
  | `reef update <path>` | Update a deployed formation (preserves agent-written data) |
142
- | `reef uninstall <name>` | Remove a formation and all its resources |
143
152
  | `reef export <namespace>` | Snapshot running agents into a formation package |
144
153
  | `reef lock` | Pin dependency versions with integrity digests |
145
- | `reef list` | List installed formations |
146
- | `reef status <name>` | Show status of a deployed formation |
147
154
 
148
155
  ## Security
149
156
 
@@ -1,7 +1,7 @@
1
1
  export interface InitOptions {
2
2
  name?: string;
3
3
  namespace?: string;
4
- type?: 'solo' | 'team' | 'swarm';
4
+ type?: 'solo' | 'shoal' | 'school';
5
5
  yes?: boolean;
6
6
  }
7
7
  export declare function init(nameArg: string | undefined, options?: InitOptions): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACjC,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAqBD,wBAAsB,IAAI,CACxB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,IAAI,CAAC,CA4Cf"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IACnC,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAqBD,wBAAsB,IAAI,CACxB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,IAAI,CAAC,CA4Cf"}
@@ -25,7 +25,7 @@ function generateEnvExample(variables) {
25
25
  export async function init(nameArg, options = {}) {
26
26
  const name = nameArg ?? options.name ?? 'my-formation';
27
27
  const namespace = options.namespace ?? name;
28
- const type = options.type ?? 'team';
28
+ const type = options.type ?? 'shoal';
29
29
  const targetDir = resolve(name);
30
30
  // Check if directory already exists
31
31
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAU3C,SAAS,kBAAkB,CAAC,SAAmC;IAC7D,MAAM,KAAK,GAAa,CAAC,gCAAgC,CAAC,CAAC;IAE3D,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACvD,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACrB,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;QACzB,CAAC;aAAM,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YACxC,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACpD,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACjC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,OAA2B,EAC3B,UAAuB,EAAE;IAEzB,MAAM,IAAI,GAAG,OAAO,IAAI,OAAO,CAAC,IAAI,IAAI,cAAc,CAAC;IACvD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC;IAC5C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC;IAEpC,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhC,oCAAoC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;QACxB,OAAO,CAAC,KAAK,CACX,GAAG,KAAK,CAAC,KAAK,eAAe,IAAI,+DAA+D,CACjG,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,iCAAiC;IACnC,CAAC;IAED,+DAA+D;IAC/D,MAAM,WAAW,GAAG,eAAe,EAAE,CAAC;IACtC,MAAM,OAAO,CAAC,WAAW,EAAE,SAAS,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC;IAExD,4BAA4B;IAC5B,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAiB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE/C,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC;IAErB,MAAM,SAAS,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAExE,uCAAuC;IACvC,IAAI,QAAQ,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrE,MAAM,UAAU,GAAG,kBAAkB,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAC1D,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,UAAU,CAAC,CAAC;IAC/D,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,cAAc,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACzE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC3B,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,qCAAqC,CAAC,EAAE,CAAC,CAAC;IACrE,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;AACpD,CAAC"}
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export interface InstallOptions {
2
+ set?: string[];
3
+ namespace?: string;
4
+ force?: boolean;
5
+ merge?: boolean;
6
+ yes?: boolean;
7
+ noEnv?: boolean;
8
+ gatewayUrl?: string;
9
+ gatewayToken?: string;
10
+ gatewayPassword?: string;
11
+ }
12
+ export declare function install(formationPath: string, options: InstallOptions): Promise<void>;
13
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"AA2CA,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAkCD,wBAAsB,OAAO,CAC3B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CA4nBf"}
@@ -0,0 +1,560 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { join, dirname } from 'node:path';
3
+ import ora from 'ora';
4
+ import chalk from 'chalk';
5
+ import { loadManifest } from '../core/manifest-loader.js';
6
+ import { validateSchema } from '../core/schema-validator.js';
7
+ import { validateStructure } from '../core/structural-validator.js';
8
+ import { resolveVariables } from '../core/variable-resolver.js';
9
+ import { interpolate } from '../core/template-interpolator.js';
10
+ import { resolveWorkspacePath, resolveGatewayUrl, validateAgentIds, } from '../core/openclaw-paths.js';
11
+ import { readConfig, writeConfig, addAgentEntry, removeAgentEntry, addBinding, removeBinding, setAgentToAgent, removeAgentToAgent, } from '../core/config-patcher.js';
12
+ import { GatewayClient, resolveGatewayAuth } from '../core/gateway-client.js';
13
+ import { loadState, saveState, deleteState, listStates, computeFileHash, } from '../core/state-manager.js';
14
+ import { generateAgentsMd } from '../core/agents-md-generator.js';
15
+ import { listFiles } from '../utils/fs.js';
16
+ import { icons, header, label, value, table } from '../utils/output.js';
17
+ const TOKEN_RE = /\{\{\w+\}\}/;
18
+ function isBinaryBuffer(buf) {
19
+ const check = buf.subarray(0, 8192);
20
+ return check.includes(0);
21
+ }
22
+ function parseSets(sets) {
23
+ const result = {};
24
+ if (!sets)
25
+ return result;
26
+ for (const s of sets) {
27
+ const eq = s.indexOf('=');
28
+ if (eq === -1)
29
+ continue;
30
+ result[s.slice(0, eq)] = s.slice(eq + 1);
31
+ }
32
+ return result;
33
+ }
34
+ function parseIdentifier(identifier) {
35
+ const slash = identifier.indexOf('/');
36
+ if (slash !== -1) {
37
+ return {
38
+ namespace: identifier.slice(0, slash),
39
+ name: identifier.slice(slash + 1),
40
+ };
41
+ }
42
+ return { name: identifier };
43
+ }
44
+ export async function install(formationPath, options) {
45
+ // ── Phase 1: Parse + Validate IDs ──
46
+ const spinner = ora('Loading formation...').start();
47
+ let manifest;
48
+ try {
49
+ manifest = await loadManifest(formationPath);
50
+ }
51
+ catch (err) {
52
+ spinner.fail('Failed to load formation');
53
+ throw err;
54
+ }
55
+ const schemaResult = await validateSchema(manifest);
56
+ if (!schemaResult.valid) {
57
+ spinner.fail('Schema validation failed');
58
+ for (const issue of schemaResult.issues) {
59
+ console.error(` ${icons.error} ${issue.message}`);
60
+ }
61
+ process.exit(1);
62
+ }
63
+ const structResult = await validateStructure(manifest, formationPath);
64
+ if (!structResult.valid) {
65
+ spinner.fail('Structural validation failed');
66
+ for (const issue of structResult.issues) {
67
+ if (issue.severity === 'error') {
68
+ console.error(` ${icons.error} ${issue.message}`);
69
+ }
70
+ }
71
+ process.exit(1);
72
+ }
73
+ const namespace = options.namespace ?? manifest.namespace;
74
+ const slugs = Object.keys(manifest.agents);
75
+ const idValidation = validateAgentIds(slugs, namespace);
76
+ if (!idValidation.valid) {
77
+ spinner.fail('Agent ID validation failed');
78
+ for (const error of idValidation.errors) {
79
+ console.error(` ${icons.error} ${error}`);
80
+ }
81
+ process.exit(1);
82
+ }
83
+ spinner.succeed('Formation loaded and validated');
84
+ // ── Phase 2: Variables ──
85
+ const { resolved: resolvedVars, missing } = await resolveVariables(manifest.variables ?? {}, formationPath, {
86
+ interactive: !options.yes,
87
+ cliOverrides: parseSets(options.set),
88
+ noEnv: options.noEnv,
89
+ });
90
+ if (missing.length > 0) {
91
+ console.error(`${icons.error} Missing required variables: ${missing.join(', ')}`);
92
+ console.error(' Use --set KEY=VALUE or set them in .env / environment.');
93
+ process.exit(1);
94
+ }
95
+ // ── Phase 3: Conflicts ──
96
+ const { config, path: configPath } = await readConfig();
97
+ const existingState = await loadState(namespace, manifest.name);
98
+ if (existingState && !options.force && !options.merge) {
99
+ console.error(`${icons.error} Formation "${namespace}/${manifest.name}" is already installed.`);
100
+ console.error(' Use --force to replace or --merge to update.');
101
+ process.exit(1);
102
+ }
103
+ // Check for agent ID conflicts in config (from other formations)
104
+ if (!options.force && !options.merge) {
105
+ const agentsList = config.agents?.list ?? [];
106
+ const conflicting = agentsList.filter((a) => {
107
+ const id = a.id;
108
+ return id.startsWith(`${namespace}-`) && idValidation.ids.has(slugs.find((s) => `${namespace}-${s}` === id) ?? '');
109
+ });
110
+ if (conflicting.length > 0) {
111
+ console.error(`${icons.error} Config already contains agents with matching IDs:`);
112
+ for (const a of conflicting) {
113
+ console.error(` - ${a.id}`);
114
+ }
115
+ console.error(' Use --force to replace.');
116
+ process.exit(1);
117
+ }
118
+ }
119
+ // --force cleanup
120
+ if (options.force && existingState) {
121
+ if (!options.yes) {
122
+ const { confirm } = await import('@inquirer/prompts');
123
+ const ok = await confirm({
124
+ message: `This will remove the existing installation of "${namespace}/${manifest.name}". Continue?`,
125
+ });
126
+ if (!ok) {
127
+ console.log('Aborted.');
128
+ process.exit(0);
129
+ }
130
+ }
131
+ const cleanupSpinner = ora('Cleaning up existing installation...').start();
132
+ // Remove cron jobs via RPC
133
+ if (existingState.cronJobs.length > 0) {
134
+ try {
135
+ const gwUrl = options.gatewayUrl ?? resolveGatewayUrl(config, process.env);
136
+ const gwAuth = resolveGatewayAuth({
137
+ gatewayUrl: options.gatewayUrl,
138
+ gatewayToken: options.gatewayToken,
139
+ gatewayPassword: options.gatewayPassword,
140
+ config,
141
+ });
142
+ const gw = new GatewayClient({
143
+ url: gwUrl,
144
+ ...gwAuth,
145
+ });
146
+ await gw.connect();
147
+ for (const job of existingState.cronJobs) {
148
+ try {
149
+ await gw.cronRemove(job.id);
150
+ }
151
+ catch {
152
+ // Job may already be gone
153
+ }
154
+ }
155
+ gw.close();
156
+ }
157
+ catch {
158
+ cleanupSpinner.warn('Could not connect to Gateway for cron cleanup — jobs may be orphaned');
159
+ }
160
+ }
161
+ // Remove from config
162
+ let cleanConfig = config;
163
+ for (const agent of Object.values(existingState.agents)) {
164
+ cleanConfig = removeAgentEntry(cleanConfig, agent.id);
165
+ }
166
+ for (const binding of existingState.bindings) {
167
+ cleanConfig = removeBinding(cleanConfig, binding);
168
+ }
169
+ // Remove agentToAgent allow entry
170
+ if (existingState.agentToAgent?.allowAdded) {
171
+ const allStates = await listStates();
172
+ const otherInNamespace = allStates.some((s) => s.namespace === namespace &&
173
+ s.name !== existingState.name);
174
+ cleanConfig = removeAgentToAgent(cleanConfig, namespace, otherInNamespace, existingState.agentToAgent.wasEnabled);
175
+ }
176
+ await writeConfig(configPath, cleanConfig, { silent: true });
177
+ // Delete workspaces
178
+ const { rm } = await import('node:fs/promises');
179
+ for (const agent of Object.values(existingState.agents)) {
180
+ try {
181
+ await rm(agent.workspace, { recursive: true, force: true });
182
+ }
183
+ catch {
184
+ // Already gone
185
+ }
186
+ }
187
+ await deleteState(namespace, manifest.name);
188
+ cleanupSpinner.succeed('Cleaned up existing installation');
189
+ }
190
+ // ── Phase 4: Dependencies ──
191
+ if (manifest.dependencies?.skills) {
192
+ const skills = Object.keys(manifest.dependencies.skills);
193
+ if (skills.length > 0) {
194
+ console.log(`${icons.warning} ${chalk.yellow('Skills not yet installed (ClawHub integration planned):')} ${skills.join(', ')}`);
195
+ }
196
+ }
197
+ if (manifest.dependencies?.services?.length) {
198
+ const unconfigured = manifest.dependencies.services.filter((s) => s.required);
199
+ if (unconfigured.length > 0) {
200
+ console.log(`${icons.warning} ${chalk.yellow('Required services — ensure they are configured:')} ${unconfigured.map((s) => s.name).join(', ')}`);
201
+ }
202
+ }
203
+ // ── Phase 5: Confirm ──
204
+ if (!options.yes && !options.merge) {
205
+ console.log('');
206
+ console.log(header('Deployment Plan'));
207
+ console.log('');
208
+ console.log(`${label('Formation:')} ${value(`${namespace}/${manifest.name}`)} v${manifest.version}`);
209
+ console.log('');
210
+ // Agents
211
+ console.log(label('Agents:'));
212
+ const agentRows = [];
213
+ for (const [slug] of Object.entries(manifest.agents)) {
214
+ agentRows.push([
215
+ ` ${slug}`,
216
+ `→ ${idValidation.ids.get(slug)}`,
217
+ ]);
218
+ }
219
+ console.log(table(agentRows));
220
+ // Bindings
221
+ if (manifest.bindings?.length) {
222
+ console.log('');
223
+ console.log(label('Channel Bindings:'));
224
+ for (const b of manifest.bindings) {
225
+ console.log(` ${b.channel} → ${namespace}-${b.agent}`);
226
+ }
227
+ }
228
+ // Cron
229
+ if (manifest.cron?.length) {
230
+ console.log('');
231
+ console.log(label('Cron Jobs:'));
232
+ for (const c of manifest.cron) {
233
+ console.log(` ${c.schedule} → ${namespace}-${c.agent}: "${c.prompt.slice(0, 60)}${c.prompt.length > 60 ? '...' : ''}"`);
234
+ }
235
+ }
236
+ // Variables
237
+ if (manifest.variables && Object.keys(manifest.variables).length > 0) {
238
+ console.log('');
239
+ console.log(label('Variables:'));
240
+ for (const [name, config] of Object.entries(manifest.variables)) {
241
+ const val = config.sensitive
242
+ ? '********'
243
+ : (resolvedVars[name] ?? '(not set)');
244
+ console.log(` ${name} = ${val}`);
245
+ }
246
+ }
247
+ console.log('');
248
+ const { confirm } = await import('@inquirer/prompts');
249
+ const proceed = await confirm({ message: 'Deploy this formation?' });
250
+ if (!proceed) {
251
+ console.log('Aborted.');
252
+ process.exit(0);
253
+ }
254
+ }
255
+ // ── Phase 6: Deploy ──
256
+ const deploySpinner = ora('Deploying formation...').start();
257
+ const agentStates = {};
258
+ const fileHashes = {};
259
+ const cronJobStates = [];
260
+ const openClawBindings = [];
261
+ // Deploy workspace files
262
+ deploySpinner.text = 'Writing workspace files...';
263
+ for (const [slug, agentDef] of Object.entries(manifest.agents)) {
264
+ const agentId = idValidation.ids.get(slug);
265
+ const workspacePath = resolveWorkspacePath(agentId);
266
+ await mkdir(workspacePath, { recursive: true });
267
+ const sourceDir = join(formationPath, agentDef.source);
268
+ const files = await listFiles(sourceDir);
269
+ const deployedFiles = [];
270
+ for (const relativePath of files) {
271
+ const srcFile = join(sourceDir, relativePath);
272
+ const destPath = join(workspacePath, relativePath);
273
+ await mkdir(dirname(destPath), { recursive: true });
274
+ const rawBytes = await readFile(srcFile);
275
+ if (isBinaryBuffer(rawBytes)) {
276
+ // --merge: skip unchanged files
277
+ if (options.merge && existingState) {
278
+ const hash = computeFileHash(rawBytes);
279
+ const existingHash = existingState.fileHashes[`${agentId}:${relativePath}`];
280
+ if (existingHash === hash) {
281
+ deployedFiles.push(relativePath);
282
+ fileHashes[`${agentId}:${relativePath}`] = hash;
283
+ continue;
284
+ }
285
+ }
286
+ await writeFile(destPath, rawBytes);
287
+ fileHashes[`${agentId}:${relativePath}`] = computeFileHash(rawBytes);
288
+ deployedFiles.push(relativePath);
289
+ continue;
290
+ }
291
+ let content = rawBytes.toString('utf-8');
292
+ if (TOKEN_RE.test(content)) {
293
+ content = interpolate(content, resolvedVars);
294
+ }
295
+ const written = Buffer.from(content, 'utf-8');
296
+ // --merge: skip unchanged files
297
+ if (options.merge && existingState) {
298
+ const hash = computeFileHash(written);
299
+ const existingHash = existingState.fileHashes[`${agentId}:${relativePath}`];
300
+ if (existingHash === hash) {
301
+ deployedFiles.push(relativePath);
302
+ fileHashes[`${agentId}:${relativePath}`] = hash;
303
+ continue;
304
+ }
305
+ }
306
+ await writeFile(destPath, written);
307
+ fileHashes[`${agentId}:${relativePath}`] = computeFileHash(written);
308
+ deployedFiles.push(relativePath);
309
+ }
310
+ // Generate AGENTS.md
311
+ if (manifest.agentToAgent?.[slug]?.length) {
312
+ const agentsMd = generateAgentsMd(manifest, slug, namespace);
313
+ const buf = Buffer.from(agentsMd, 'utf-8');
314
+ await writeFile(join(workspacePath, 'AGENTS.md'), buf);
315
+ fileHashes[`${agentId}:AGENTS.md`] = computeFileHash(buf);
316
+ deployedFiles.push('AGENTS.md');
317
+ }
318
+ agentStates[slug] = {
319
+ id: agentId,
320
+ slug,
321
+ workspace: workspacePath,
322
+ files: deployedFiles,
323
+ };
324
+ }
325
+ // Patch config
326
+ deploySpinner.text = 'Patching OpenClaw config...';
327
+ let patchedConfig = (await readConfig(configPath)).config;
328
+ for (const [slug, agentDef] of Object.entries(manifest.agents)) {
329
+ const agentId = idValidation.ids.get(slug);
330
+ patchedConfig = addAgentEntry(patchedConfig, {
331
+ id: agentId,
332
+ name: slug,
333
+ workspace: resolveWorkspacePath(agentId),
334
+ model: agentDef.model,
335
+ });
336
+ }
337
+ // Map bindings — use validated IDs from the map, not raw string composition
338
+ for (const binding of manifest.bindings ?? []) {
339
+ if (binding.direction) {
340
+ console.log(`\n${icons.warning} ${chalk.yellow(`'direction' field in binding ${binding.channel} → ${binding.agent} is ignored (OpenClaw bindings are always inbound)`)}`);
341
+ }
342
+ const resolvedAgentId = idValidation.ids.get(binding.agent);
343
+ if (!resolvedAgentId) {
344
+ console.log(`\n${icons.warning} ${chalk.yellow(`Binding references unknown agent "${binding.agent}" — skipped`)}`);
345
+ continue;
346
+ }
347
+ const openClawBinding = {
348
+ agentId: resolvedAgentId,
349
+ match: { channel: binding.channel },
350
+ };
351
+ patchedConfig = addBinding(patchedConfig, openClawBinding);
352
+ openClawBindings.push(openClawBinding);
353
+ }
354
+ // Agent-to-agent messaging
355
+ const a2aState = { wasEnabled: false, allowAdded: false };
356
+ if (manifest.agentToAgent &&
357
+ Object.keys(manifest.agentToAgent).length > 0) {
358
+ const tools = patchedConfig.tools;
359
+ const a2a = tools?.agentToAgent;
360
+ a2aState.wasEnabled = a2a?.enabled === true;
361
+ patchedConfig = setAgentToAgent(patchedConfig, namespace);
362
+ a2aState.allowAdded = true;
363
+ }
364
+ await writeConfig(configPath, patchedConfig, { silent: options.merge });
365
+ // Cron jobs via Gateway RPC
366
+ if (manifest.cron?.length) {
367
+ deploySpinner.text = 'Creating cron jobs...';
368
+ const gwUrl = options.gatewayUrl ?? resolveGatewayUrl(patchedConfig, process.env);
369
+ const gwAuth = resolveGatewayAuth({
370
+ gatewayUrl: options.gatewayUrl,
371
+ gatewayToken: options.gatewayToken,
372
+ gatewayPassword: options.gatewayPassword,
373
+ config: patchedConfig,
374
+ });
375
+ const gw = new GatewayClient({
376
+ url: gwUrl,
377
+ ...gwAuth,
378
+ });
379
+ try {
380
+ await gw.connect();
381
+ if (options.merge && existingState) {
382
+ // --merge: update existing cron jobs, add new ones
383
+ const existingJobs = await gw.cronList({ includeDisabled: true });
384
+ const existingByName = new Map(existingJobs.map((j) => [j.name, j]));
385
+ for (const [i, cronEntry] of manifest.cron.entries()) {
386
+ const jobName = `reef:${namespace}:${cronEntry.agent}-${i}`;
387
+ const existing = existingByName.get(jobName);
388
+ if (existing) {
389
+ // Update in-place
390
+ await gw.cronUpdate(existing.id, {
391
+ schedule: {
392
+ kind: 'cron',
393
+ expr: cronEntry.schedule,
394
+ tz: cronEntry.timezone,
395
+ },
396
+ payload: {
397
+ kind: 'agentTurn',
398
+ message: cronEntry.prompt,
399
+ },
400
+ agentId: idValidation.ids.get(cronEntry.agent),
401
+ enabled: true,
402
+ });
403
+ cronJobStates.push({
404
+ id: existing.id,
405
+ name: jobName,
406
+ agentSlug: cronEntry.agent,
407
+ });
408
+ }
409
+ else {
410
+ // Add new
411
+ const result = await gw.cronAdd({
412
+ name: jobName,
413
+ agentId: idValidation.ids.get(cronEntry.agent),
414
+ enabled: true,
415
+ schedule: {
416
+ kind: 'cron',
417
+ expr: cronEntry.schedule,
418
+ tz: cronEntry.timezone,
419
+ },
420
+ sessionTarget: 'isolated',
421
+ wakeMode: 'now',
422
+ payload: {
423
+ kind: 'agentTurn',
424
+ message: cronEntry.prompt,
425
+ },
426
+ });
427
+ cronJobStates.push({
428
+ id: result.id,
429
+ name: jobName,
430
+ agentSlug: cronEntry.agent,
431
+ });
432
+ }
433
+ }
434
+ }
435
+ else {
436
+ // Fresh install
437
+ for (const [i, cronEntry] of manifest.cron.entries()) {
438
+ const jobName = `reef:${namespace}:${cronEntry.agent}-${i}`;
439
+ const cronAgentId = idValidation.ids.get(cronEntry.agent);
440
+ if (!cronAgentId) {
441
+ console.log(`\n${icons.warning} ${chalk.yellow(`Cron job references unknown agent "${cronEntry.agent}" — skipped`)}`);
442
+ continue;
443
+ }
444
+ const result = await gw.cronAdd({
445
+ name: jobName,
446
+ agentId: cronAgentId,
447
+ enabled: true,
448
+ schedule: {
449
+ kind: 'cron',
450
+ expr: cronEntry.schedule,
451
+ tz: cronEntry.timezone,
452
+ },
453
+ sessionTarget: 'isolated',
454
+ wakeMode: 'now',
455
+ payload: {
456
+ kind: 'agentTurn',
457
+ message: cronEntry.prompt,
458
+ },
459
+ });
460
+ cronJobStates.push({
461
+ id: result.id,
462
+ name: jobName,
463
+ agentSlug: cronEntry.agent,
464
+ });
465
+ }
466
+ }
467
+ }
468
+ catch (err) {
469
+ deploySpinner.fail('Failed to create cron jobs');
470
+ console.error(`${icons.error} Gateway error: ${err instanceof Error ? err.message : err}`);
471
+ console.error(' Ensure OpenClaw Gateway is running. Config and workspace files were deployed successfully.');
472
+ // Still save state for what we deployed
473
+ }
474
+ finally {
475
+ gw.close();
476
+ }
477
+ }
478
+ // ── Phase 7: Validate ──
479
+ deploySpinner.text = 'Validating deployment...';
480
+ const finalConfig = (await readConfig(configPath)).config;
481
+ const agentsList = finalConfig.agents?.list ?? [];
482
+ // agent_exists: check agents in config
483
+ for (const [slug] of Object.entries(manifest.agents)) {
484
+ const agentId = idValidation.ids.get(slug);
485
+ const found = agentsList.some((a) => a.id === agentId);
486
+ if (!found) {
487
+ console.log(`\n${icons.warning} Agent "${agentId}" not found in config after deploy`);
488
+ }
489
+ }
490
+ // binding_active: check bindings in config
491
+ const finalBindings = (finalConfig.bindings ?? []);
492
+ for (const binding of openClawBindings) {
493
+ const found = finalBindings.some((b) => {
494
+ const match = b.match;
495
+ return b.agentId === binding.agentId && match?.channel === binding.match.channel;
496
+ });
497
+ if (!found) {
498
+ console.log(`\n${icons.warning} Binding ${binding.match.channel} → ${binding.agentId} not found in config after deploy`);
499
+ }
500
+ }
501
+ // cron_exists: verify cron jobs via Gateway (if we created any and Gateway is reachable)
502
+ if (cronJobStates.length > 0) {
503
+ try {
504
+ const gwUrl = options.gatewayUrl ?? resolveGatewayUrl(patchedConfig, process.env);
505
+ const gwAuth = resolveGatewayAuth({
506
+ gatewayUrl: options.gatewayUrl,
507
+ gatewayToken: options.gatewayToken,
508
+ gatewayPassword: options.gatewayPassword,
509
+ config: patchedConfig,
510
+ });
511
+ const verifyGw = new GatewayClient({
512
+ url: gwUrl,
513
+ ...gwAuth,
514
+ });
515
+ await verifyGw.connect();
516
+ const liveJobs = await verifyGw.cronList({ includeDisabled: true });
517
+ verifyGw.close();
518
+ const liveIds = new Set(liveJobs.map((j) => j.id));
519
+ for (const job of cronJobStates) {
520
+ if (!liveIds.has(job.id)) {
521
+ console.log(`\n${icons.warning} Cron job "${job.name}" (${job.id}) not found on Gateway after deploy`);
522
+ }
523
+ }
524
+ }
525
+ catch {
526
+ // Gateway not reachable for verification — skip
527
+ }
528
+ }
529
+ // ── Phase 8: State ──
530
+ const variablesForState = {};
531
+ for (const [name, config] of Object.entries(manifest.variables ?? {})) {
532
+ if (config.sensitive) {
533
+ // Store as env var reference, not the secret value
534
+ variablesForState[name] = `$${name}`;
535
+ }
536
+ else {
537
+ variablesForState[name] = resolvedVars[name] ?? '';
538
+ }
539
+ }
540
+ const state = {
541
+ name: manifest.name,
542
+ version: manifest.version,
543
+ namespace,
544
+ installedAt: new Date().toISOString(),
545
+ agents: agentStates,
546
+ bindings: openClawBindings,
547
+ cronJobs: cronJobStates,
548
+ variables: variablesForState,
549
+ fileHashes,
550
+ agentToAgent: a2aState.allowAdded ? a2aState : undefined,
551
+ };
552
+ await saveState(state);
553
+ deploySpinner.succeed('Formation deployed');
554
+ console.log('');
555
+ console.log(`${icons.success} ${chalk.green(`${Object.keys(agentStates).length} agents deployed, ${openClawBindings.length} bindings wired, ${cronJobStates.length} cron jobs scheduled`)}`);
556
+ console.log('');
557
+ console.log(` ${label('Manage:')} reef status ${namespace}/${manifest.name}`);
558
+ console.log(` ${label('Remove:')} reef uninstall ${namespace}/${manifest.name}`);
559
+ }
560
+ //# sourceMappingURL=install.js.map