clawchef 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,7 @@ Recipe-driven OpenClaw environment orchestrator.
19
19
  - Materializes files into target workspaces.
20
20
  - Installs skills.
21
21
  - Supports plugin preinstall via `openclaw.plugins[]` and runtime `--plugin` flags.
22
+ - Supports preserving existing OpenClaw state via `--keep-openclaw-state` (skip factory reset).
22
23
  - Configures channels with `openclaw channels add`.
23
24
  - Supports interactive channel login at the end of execution (`channels[].login: true`) for channels that expose login.
24
25
  - Supports remote HTTP orchestration via runtime flags (`--provider remote`) when OpenClaw is reachable via API.
@@ -78,6 +79,12 @@ clawchef cook recipes/sample.yaml -s
78
79
  Warning: `-s/--silent` suppresses the factory-reset confirmation and auto-chooses force reinstall on version mismatch.
79
80
  Use it only in CI/non-interactive flows where destructive reset behavior is expected.
80
81
 
82
+ Keep existing OpenClaw state (skip reset and keep current version on mismatch):
83
+
84
+ ```bash
85
+ clawchef cook recipes/sample.yaml --keep-openclaw-state
86
+ ```
87
+
81
88
  From-zero OpenClaw bootstrap (recommended):
82
89
 
83
90
  ```bash
@@ -142,9 +149,9 @@ clawchef scaffold ./my-recipe-project --name meetingbot
142
149
  Scaffold output:
143
150
 
144
151
  - `package.json` with `telegram-api-mock-server` in `devDependencies`
145
- - `src/recipe.yaml` with `telegram-mock` channel, plugin preinstall, and `workspaces[].assets`
146
- - `src/<project-name>-assets/{AGENTS.md,IDENTITY.md,SOUL.md,TOOLS.md}`
147
- - `src/<project-name>-assets/scripts/scheduling.mjs`
152
+ - `recipe.yaml` with `telegram-mock` channel, plugin preinstall, and `workspaces[].assets`
153
+ - `assets/{AGENTS.md,IDENTITY.md,SOUL.md,TOOLS.md}`
154
+ - `assets/scripts/scheduling.mjs`
148
155
  - `test/recipe-smoke.test.mjs`
149
156
 
150
157
  By default scaffold only writes files; it does not run `npm install`.
@@ -188,7 +195,7 @@ Notes:
188
195
 
189
196
  - `validate()` throws on invalid recipe.
190
197
  - `cook()` throws on runtime/configuration errors.
191
- - `scaffold()` creates `package.json`, `src/recipe.yaml`, `src/<project-name>-assets`, and `test/`.
198
+ - `scaffold()` creates `package.json`, `recipe.yaml`, `assets/`, and `test/`.
192
199
 
193
200
  ## Variable precedence
194
201
 
package/dist/api.js CHANGED
@@ -15,6 +15,7 @@ function normalizeCookOptions(options) {
15
15
  allowMissing: Boolean(options.allowMissing),
16
16
  verbose: Boolean(options.verbose),
17
17
  silent: options.silent ?? true,
18
+ keepOpenClawState: false,
18
19
  provider: options.provider ?? "command",
19
20
  remote: options.remote ?? {},
20
21
  };
package/dist/cli.js CHANGED
@@ -77,6 +77,7 @@ export function buildCli() {
77
77
  .option("--allow-missing", "Allow unresolved template variables", false)
78
78
  .option("--verbose", "Verbose logging", false)
79
79
  .option("-s, --silent", "Skip reset confirmation prompt", false)
80
+ .option("--keep-openclaw-state", "Preserve existing OpenClaw state (skip factory reset)", false)
80
81
  .option("--provider <provider>", "Execution provider: command | remote | mock")
81
82
  .option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p) => p.concat([v]), [])
82
83
  .option("--remote-base-url <url>", "Remote OpenClaw API base URL")
@@ -94,6 +95,7 @@ export function buildCli() {
94
95
  allowMissing: Boolean(opts.allowMissing),
95
96
  verbose: Boolean(opts.verbose),
96
97
  silent: Boolean(opts.silent),
98
+ keepOpenClawState: Boolean(opts.keepOpenclawState),
97
99
  provider,
98
100
  remote: {
99
101
  base_url: opts.remoteBaseUrl ?? readEnv("CLAWCHEF_REMOTE_BASE_URL"),
@@ -2,7 +2,7 @@ import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../
2
2
  import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
3
3
  export declare class CommandOpenClawProvider implements OpenClawProvider {
4
4
  private readonly stagedMessages;
5
- ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean): Promise<EnsureVersionResult>;
5
+ ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean, keepOpenClawState: boolean): Promise<EnsureVersionResult>;
6
6
  factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
7
7
  installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
8
8
  startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
@@ -221,7 +221,7 @@ function bootstrapRuntimeEnv(bootstrap) {
221
221
  }
222
222
  export class CommandOpenClawProvider {
223
223
  stagedMessages = new Map();
224
- async ensureVersion(config, dryRun, silent) {
224
+ async ensureVersion(config, dryRun, silent, keepOpenClawState) {
225
225
  const bin = config.bin ?? "openclaw";
226
226
  const installPolicy = config.install ?? "auto";
227
227
  const useCmd = commandFor(config, "use_version", { bin, version: config.version });
@@ -272,6 +272,9 @@ export class CommandOpenClawProvider {
272
272
  if (installedThisRun) {
273
273
  throw new ClawChefError(`OpenClaw version mismatch after install: current ${currentVersion}, expected ${config.version}`);
274
274
  }
275
+ if (keepOpenClawState) {
276
+ return { installedThisRun: false };
277
+ }
275
278
  const choice = await chooseVersionMismatchAction(currentVersion, config.version, silent);
276
279
  if (choice === "ignore") {
277
280
  return { installedThisRun: false };
@@ -2,7 +2,7 @@ import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../
2
2
  import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
3
3
  export declare class MockOpenClawProvider implements OpenClawProvider {
4
4
  private state;
5
- ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean): Promise<EnsureVersionResult>;
5
+ ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean, _keepOpenClawState: boolean): Promise<EnsureVersionResult>;
6
6
  installPlugin(_config: OpenClawSection, _pluginSpec: string, _dryRun: boolean): Promise<void>;
7
7
  factoryReset(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
8
8
  startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
@@ -7,7 +7,7 @@ export class MockOpenClawProvider {
7
7
  skills: new Set(),
8
8
  messages: new Map(),
9
9
  };
10
- async ensureVersion(config, _dryRun, _silent) {
10
+ async ensureVersion(config, _dryRun, _silent, _keepOpenClawState) {
11
11
  const policy = config.install ?? "auto";
12
12
  const installed = this.state.installedVersions.has(config.version);
13
13
  let installedThisRun = false;
@@ -6,7 +6,7 @@ export interface EnsureVersionResult {
6
6
  installedThisRun: boolean;
7
7
  }
8
8
  export interface OpenClawProvider {
9
- ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean): Promise<EnsureVersionResult>;
9
+ ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean, keepOpenClawState: boolean): Promise<EnsureVersionResult>;
10
10
  installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
11
11
  factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
12
12
  startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
@@ -5,7 +5,7 @@ export declare class RemoteOpenClawProvider implements OpenClawProvider {
5
5
  private readonly remoteConfig;
6
6
  constructor(remoteConfig: Partial<OpenClawRemoteConfig>);
7
7
  private perform;
8
- ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean): Promise<EnsureVersionResult>;
8
+ ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean, _keepOpenClawState: boolean): Promise<EnsureVersionResult>;
9
9
  installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
10
10
  factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
11
11
  startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
@@ -94,7 +94,7 @@ export class RemoteOpenClawProvider {
94
94
  clearTimeout(timeout);
95
95
  }
96
96
  }
97
- async ensureVersion(config, dryRun, _silent) {
97
+ async ensureVersion(config, dryRun, _silent, _keepOpenClawState) {
98
98
  const result = await this.perform(config, "ensure_version", {
99
99
  install: config.install,
100
100
  }, dryRun);
@@ -122,11 +122,14 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
122
122
  const remoteMode = options.provider === "remote";
123
123
  const workspacePaths = new Map();
124
124
  logger.info(`Running recipe: ${recipe.name}`);
125
- const versionResult = await provider.ensureVersion(recipe.openclaw, options.dryRun, options.silent);
125
+ const versionResult = await provider.ensureVersion(recipe.openclaw, options.dryRun, options.silent, options.keepOpenClawState);
126
126
  logger.info(`OpenClaw version ready: ${recipe.openclaw.version}`);
127
127
  if (versionResult.installedThisRun) {
128
128
  logger.info("OpenClaw was installed in this run; skipping factory reset");
129
129
  }
130
+ else if (options.keepOpenClawState) {
131
+ logger.info("Keeping existing OpenClaw state; skipping factory reset");
132
+ }
130
133
  else {
131
134
  const confirmed = await confirmFactoryReset(options);
132
135
  if (!confirmed) {
package/dist/scaffold.js CHANGED
@@ -76,7 +76,7 @@ openclaw:
76
76
 
77
77
  workspaces:
78
78
  - name: "\${workspace_name}"
79
- assets: "./${projectName}-assets"
79
+ assets: "./assets"
80
80
 
81
81
  agents:
82
82
  - workspace: "\${workspace_name}"
@@ -134,11 +134,17 @@ console.log("[scheduling] " + message);
134
134
  function makeRecipeSmokeTest() {
135
135
  return `import test from "node:test";
136
136
  import assert from "node:assert/strict";
137
+ import path from "node:path";
138
+ import { fileURLToPath } from "node:url";
137
139
  import { access } from "node:fs/promises";
138
140
 
141
+ const testDir = path.dirname(fileURLToPath(import.meta.url));
142
+ const projectRoot = path.resolve(testDir, "..");
143
+
139
144
  test("recipe scaffold files exist", async () => {
140
- await access("src/recipe.yaml");
141
- await access("package.json");
145
+ await access(path.join(projectRoot, "recipe.yaml"));
146
+ await access(path.join(projectRoot, "assets", "AGENTS.md"));
147
+ await access(path.join(projectRoot, "package.json"));
142
148
  assert.ok(true);
143
149
  });
144
150
  `;
@@ -149,16 +155,14 @@ export async function scaffoldProject(targetDirArg, options = {}) {
149
155
  const defaultName = path.basename(targetDir);
150
156
  const rawProjectName = options.projectName?.trim() || defaultName;
151
157
  const projectName = normalizeProjectName(rawProjectName);
152
- const srcDir = path.join(targetDir, "src");
153
- const assetsDir = path.join(srcDir, `${projectName}-assets`);
158
+ const assetsDir = path.join(targetDir, "assets");
154
159
  const assetsScriptsDir = path.join(assetsDir, "scripts");
155
160
  const testDir = path.join(targetDir, "test");
156
- await mkdir(srcDir, { recursive: true });
157
161
  await mkdir(assetsDir, { recursive: true });
158
162
  await mkdir(assetsScriptsDir, { recursive: true });
159
163
  await mkdir(testDir, { recursive: true });
160
164
  await writeFile(path.join(targetDir, "package.json"), makePackageJson(projectName), "utf8");
161
- await writeFile(path.join(srcDir, "recipe.yaml"), makeRecipeYaml(projectName), "utf8");
165
+ await writeFile(path.join(targetDir, "recipe.yaml"), makeRecipeYaml(projectName), "utf8");
162
166
  await writeFile(path.join(assetsDir, "AGENTS.md"), makeAgentsDoc(projectName), "utf8");
163
167
  await writeFile(path.join(assetsDir, "IDENTITY.md"), makeIdentityDoc(projectName), "utf8");
164
168
  await writeFile(path.join(assetsDir, "SOUL.md"), makeSoulDoc(), "utf8");
package/dist/types.d.ts CHANGED
@@ -135,6 +135,7 @@ export interface RunOptions {
135
135
  allowMissing: boolean;
136
136
  verbose: boolean;
137
137
  silent: boolean;
138
+ keepOpenClawState: boolean;
138
139
  provider: OpenClawProvider;
139
140
  remote: Partial<OpenClawRemoteConfig>;
140
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawchef",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Recipe-driven OpenClaw environment orchestrator",
5
5
  "homepage": "https://renorzr.github.io/clawchef",
6
6
  "repository": {
package/src/api.ts CHANGED
@@ -30,6 +30,7 @@ function normalizeCookOptions(options: CookOptions): RunOptions {
30
30
  allowMissing: Boolean(options.allowMissing),
31
31
  verbose: Boolean(options.verbose),
32
32
  silent: options.silent ?? true,
33
+ keepOpenClawState: false,
33
34
  provider: options.provider ?? "command",
34
35
  remote: options.remote ?? {},
35
36
  };
package/src/cli.ts CHANGED
@@ -87,6 +87,7 @@ export function buildCli(): Command {
87
87
  .option("--allow-missing", "Allow unresolved template variables", false)
88
88
  .option("--verbose", "Verbose logging", false)
89
89
  .option("-s, --silent", "Skip reset confirmation prompt", false)
90
+ .option("--keep-openclaw-state", "Preserve existing OpenClaw state (skip factory reset)", false)
90
91
  .option("--provider <provider>", "Execution provider: command | remote | mock")
91
92
  .option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p: string[]) => p.concat([v]), [])
92
93
  .option("--remote-base-url <url>", "Remote OpenClaw API base URL")
@@ -104,6 +105,7 @@ export function buildCli(): Command {
104
105
  allowMissing: Boolean(opts.allowMissing),
105
106
  verbose: Boolean(opts.verbose),
106
107
  silent: Boolean(opts.silent),
108
+ keepOpenClawState: Boolean(opts.keepOpenclawState),
107
109
  provider,
108
110
  remote: {
109
111
  base_url: opts.remoteBaseUrl ?? readEnv("CLAWCHEF_REMOTE_BASE_URL"),
@@ -273,7 +273,12 @@ function bootstrapRuntimeEnv(bootstrap: OpenClawBootstrap | undefined): Record<s
273
273
  export class CommandOpenClawProvider implements OpenClawProvider {
274
274
  private readonly stagedMessages = new Map<string, StagedMessage[]>();
275
275
 
276
- async ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean): Promise<EnsureVersionResult> {
276
+ async ensureVersion(
277
+ config: OpenClawSection,
278
+ dryRun: boolean,
279
+ silent: boolean,
280
+ keepOpenClawState: boolean,
281
+ ): Promise<EnsureVersionResult> {
277
282
  const bin = config.bin ?? "openclaw";
278
283
  const installPolicy = config.install ?? "auto";
279
284
  const useCmd = commandFor(config, "use_version", { bin, version: config.version });
@@ -338,6 +343,10 @@ export class CommandOpenClawProvider implements OpenClawProvider {
338
343
  );
339
344
  }
340
345
 
346
+ if (keepOpenClawState) {
347
+ return { installedThisRun: false };
348
+ }
349
+
341
350
  const choice = await chooseVersionMismatchAction(currentVersion, config.version, silent);
342
351
 
343
352
  if (choice === "ignore") {
@@ -21,7 +21,12 @@ export class MockOpenClawProvider implements OpenClawProvider {
21
21
  messages: new Map(),
22
22
  };
23
23
 
24
- async ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean): Promise<EnsureVersionResult> {
24
+ async ensureVersion(
25
+ config: OpenClawSection,
26
+ _dryRun: boolean,
27
+ _silent: boolean,
28
+ _keepOpenClawState: boolean,
29
+ ): Promise<EnsureVersionResult> {
25
30
  const policy = config.install ?? "auto";
26
31
  const installed = this.state.installedVersions.has(config.version);
27
32
  let installedThisRun = false;
@@ -7,7 +7,12 @@ export interface EnsureVersionResult {
7
7
  }
8
8
 
9
9
  export interface OpenClawProvider {
10
- ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean): Promise<EnsureVersionResult>;
10
+ ensureVersion(
11
+ config: OpenClawSection,
12
+ dryRun: boolean,
13
+ silent: boolean,
14
+ keepOpenClawState: boolean,
15
+ ): Promise<EnsureVersionResult>;
11
16
  installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
12
17
  factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
13
18
  startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
@@ -141,7 +141,12 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
141
141
  }
142
142
  }
143
143
 
144
- async ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean): Promise<EnsureVersionResult> {
144
+ async ensureVersion(
145
+ config: OpenClawSection,
146
+ dryRun: boolean,
147
+ _silent: boolean,
148
+ _keepOpenClawState: boolean,
149
+ ): Promise<EnsureVersionResult> {
145
150
  const result = await this.perform(
146
151
  config,
147
152
  "ensure_version",
@@ -148,11 +148,18 @@ export async function runRecipe(
148
148
  const workspacePaths = new Map<string, string>();
149
149
 
150
150
  logger.info(`Running recipe: ${recipe.name}`);
151
- const versionResult = await provider.ensureVersion(recipe.openclaw, options.dryRun, options.silent);
151
+ const versionResult = await provider.ensureVersion(
152
+ recipe.openclaw,
153
+ options.dryRun,
154
+ options.silent,
155
+ options.keepOpenClawState,
156
+ );
152
157
  logger.info(`OpenClaw version ready: ${recipe.openclaw.version}`);
153
158
 
154
159
  if (versionResult.installedThisRun) {
155
160
  logger.info("OpenClaw was installed in this run; skipping factory reset");
161
+ } else if (options.keepOpenClawState) {
162
+ logger.info("Keeping existing OpenClaw state; skipping factory reset");
156
163
  } else {
157
164
  const confirmed = await confirmFactoryReset(options);
158
165
  if (!confirmed) {
package/src/scaffold.ts CHANGED
@@ -89,7 +89,7 @@ openclaw:
89
89
 
90
90
  workspaces:
91
91
  - name: "\${workspace_name}"
92
- assets: "./${projectName}-assets"
92
+ assets: "./assets"
93
93
 
94
94
  agents:
95
95
  - workspace: "\${workspace_name}"
@@ -153,11 +153,17 @@ console.log("[scheduling] " + message);
153
153
  function makeRecipeSmokeTest(): string {
154
154
  return `import test from "node:test";
155
155
  import assert from "node:assert/strict";
156
+ import path from "node:path";
157
+ import { fileURLToPath } from "node:url";
156
158
  import { access } from "node:fs/promises";
157
159
 
160
+ const testDir = path.dirname(fileURLToPath(import.meta.url));
161
+ const projectRoot = path.resolve(testDir, "..");
162
+
158
163
  test("recipe scaffold files exist", async () => {
159
- await access("src/recipe.yaml");
160
- await access("package.json");
164
+ await access(path.join(projectRoot, "recipe.yaml"));
165
+ await access(path.join(projectRoot, "assets", "AGENTS.md"));
166
+ await access(path.join(projectRoot, "package.json"));
161
167
  assert.ok(true);
162
168
  });
163
169
  `;
@@ -171,18 +177,16 @@ export async function scaffoldProject(targetDirArg?: string, options: ScaffoldOp
171
177
  const rawProjectName = options.projectName?.trim() || defaultName;
172
178
  const projectName = normalizeProjectName(rawProjectName);
173
179
 
174
- const srcDir = path.join(targetDir, "src");
175
- const assetsDir = path.join(srcDir, `${projectName}-assets`);
180
+ const assetsDir = path.join(targetDir, "assets");
176
181
  const assetsScriptsDir = path.join(assetsDir, "scripts");
177
182
  const testDir = path.join(targetDir, "test");
178
183
 
179
- await mkdir(srcDir, { recursive: true });
180
184
  await mkdir(assetsDir, { recursive: true });
181
185
  await mkdir(assetsScriptsDir, { recursive: true });
182
186
  await mkdir(testDir, { recursive: true });
183
187
 
184
188
  await writeFile(path.join(targetDir, "package.json"), makePackageJson(projectName), "utf8");
185
- await writeFile(path.join(srcDir, "recipe.yaml"), makeRecipeYaml(projectName), "utf8");
189
+ await writeFile(path.join(targetDir, "recipe.yaml"), makeRecipeYaml(projectName), "utf8");
186
190
  await writeFile(path.join(assetsDir, "AGENTS.md"), makeAgentsDoc(projectName), "utf8");
187
191
  await writeFile(path.join(assetsDir, "IDENTITY.md"), makeIdentityDoc(projectName), "utf8");
188
192
  await writeFile(path.join(assetsDir, "SOUL.md"), makeSoulDoc(), "utf8");
package/src/types.ts CHANGED
@@ -149,6 +149,7 @@ export interface RunOptions {
149
149
  allowMissing: boolean;
150
150
  verbose: boolean;
151
151
  silent: boolean;
152
+ keepOpenClawState: boolean;
152
153
  provider: OpenClawProvider;
153
154
  remote: Partial<OpenClawRemoteConfig>;
154
155
  }