ocsmarttools 0.1.2 → 0.1.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to `ocsmarttools` are documented here.
4
4
 
5
+ ## [0.1.4] - 2026-02-22
6
+
7
+ ### Changed
8
+ - Broadened peer compatibility range to `openclaw >=2026.2.19` for easier installs across existing instances.
9
+
10
+ ### Fixed
11
+ - Routing policy sync is now fail-safe on restricted/read-only environments (never breaks plugin startup).
12
+ - Setup/config flows now return clear messages when routing sync is skipped due to filesystem permissions/path issues.
13
+
14
+ ## [0.1.3] - 2026-02-22
15
+
16
+ ### Added
17
+ - Plugin-native auto-routing guide sync for `AGENTS.md` with managed markers, enabled by default (`autoInjectRoutingGuide`).
18
+ - New chat and CLI sync commands (`/ocsmarttools sync`, `openclaw ocsmarttools sync`) to re-apply routing guidance instantly.
19
+
20
+ ### Changed
21
+ - `setup` now re-syncs the managed routing guide automatically.
22
+ - Added `autoInjectRoutingGuide` to config schema, status, config output, and editable keys.
23
+
24
+ ### Fixed
25
+ - Removed CLI registration conflict by making `stats reset` a subcommand under `stats`.
26
+
5
27
  ## [0.1.2] - 2026-02-22
6
28
 
7
29
  ### Added
package/README.md CHANGED
@@ -23,6 +23,10 @@ flowchart LR
23
23
 
24
24
  ## Install
25
25
 
26
+ Compatibility:
27
+ - OpenClaw: `>=2026.2.19`
28
+ - Works with existing installed instances (no core patch required)
29
+
26
30
  ### npm
27
31
 
28
32
  ```bash
@@ -43,7 +47,8 @@ openclaw gateway restart
43
47
 
44
48
  1. Install + enable + restart.
45
49
  2. Done. The plugin auto-bootstraps and starts working in background.
46
- 3. Optional check:
50
+ 3. It also auto-manages an OCSmartTools routing block in `AGENTS.md` (unless disabled).
51
+ 4. Optional check:
47
52
 
48
53
  ```text
49
54
  /ocsmarttools status
@@ -66,6 +71,7 @@ Model note:
66
71
  | `/ocsmarttools stats reset` | Resets the stats window |
67
72
  | `/ocsmarttools setup [safe\|standard]` | Applies recommended defaults for the selected mode |
68
73
  | `/ocsmarttools mode <safe\|standard>` | Changes mode only |
74
+ | `/ocsmarttools sync` | Re-applies the auto-managed routing policy block in `AGENTS.md` |
69
75
  | `/ocsmarttools config` | Shows effective plugin config |
70
76
  | `/ocsmarttools config keys` | Lists editable config keys |
71
77
  | `/ocsmarttools config set <key> <value>` | Updates one config key with validation |
@@ -82,6 +88,7 @@ Model note:
82
88
  | `openclaw ocsmarttools stats reset` | Resets the stats window |
83
89
  | `openclaw ocsmarttools setup [safe\|standard]` | Applies recommended defaults for the selected mode |
84
90
  | `openclaw ocsmarttools mode <safe\|standard>` | Changes mode only |
91
+ | `openclaw ocsmarttools sync` | Re-applies the auto-managed routing policy block in `AGENTS.md` |
85
92
  | `openclaw ocsmarttools config` | Shows effective plugin config |
86
93
  | `openclaw ocsmarttools config keys` | Lists editable config keys |
87
94
  | `openclaw ocsmarttools config set <key> <value>` | Updates one config key with validation |
@@ -96,6 +103,8 @@ Model note:
96
103
  /ocsmarttools config set storeLargeResults true
97
104
  /ocsmarttools config set toolSearch.useLiveRegistry true
98
105
  /ocsmarttools config set toolSearch.liveTimeoutMs 1500
106
+ /ocsmarttools config set autoInjectRoutingGuide true
107
+ /ocsmarttools config set autoInjectRoutingGuide false
99
108
  /ocsmarttools config reset maxResultChars
100
109
  /ocsmarttools stats
101
110
  ```
@@ -143,6 +152,7 @@ If your instance uses strict `tools.allow`, include:
143
152
  ## Safety and Limits
144
153
 
145
154
  - `ocsmarttools` does not bypass OpenClaw tool policy.
155
+ - Routing policy is auto-injected into `AGENTS.md` with managed markers; it can be re-synced via `/ocsmarttools sync`.
146
156
  - `tool_batch` is intentionally bounded (`maxSteps`, `maxForEach`).
147
157
  - Large-result handles are in-memory and expire by TTL.
148
158
  - `tool_result_get` works only while handle is still valid.
@@ -8,6 +8,7 @@
8
8
  "properties": {
9
9
  "enabled": { "type": "boolean" },
10
10
  "mode": { "type": "string", "enum": ["safe", "standard"] },
11
+ "autoInjectRoutingGuide": { "type": "boolean" },
11
12
  "maxSteps": { "type": "integer", "minimum": 1, "maximum": 200 },
12
13
  "maxForEach": { "type": "integer", "minimum": 1, "maximum": 200 },
13
14
  "maxResultChars": { "type": "integer", "minimum": 500, "maximum": 500000 },
@@ -31,6 +32,7 @@
31
32
  },
32
33
  "uiHints": {
33
34
  "mode": { "label": "Mode" },
35
+ "autoInjectRoutingGuide": { "label": "Auto Inject Routing Guide" },
34
36
  "maxSteps": { "label": "Max Steps", "advanced": true },
35
37
  "maxForEach": { "label": "Max ForEach", "advanced": true },
36
38
  "maxResultChars": { "label": "Max Result Chars", "advanced": true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocsmarttools",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Provider-agnostic advanced tool orchestration plugin for OpenClaw with search, dispatch, and batching",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "license": "MIT",
26
26
  "peerDependencies": {
27
- "openclaw": ">=2026.2.21"
27
+ "openclaw": ">=2026.2.19"
28
28
  },
29
29
  "devDependencies": {
30
30
  "typescript": "^5.8.0",
@@ -12,6 +12,7 @@ import {
12
12
  resetStats,
13
13
  resetConfig,
14
14
  setConfigKey,
15
+ syncManagedRouting,
15
16
  updateMode,
16
17
  } from "./operations.js";
17
18
  import type { AdvToolsMode } from "../lib/plugin-config.js";
@@ -72,6 +73,10 @@ export function registerChatCommands(api: OpenClawPluginApi, metrics: MetricsSto
72
73
  return { text: await updateMode(api, mode) };
73
74
  }
74
75
 
76
+ if (cmd === "sync") {
77
+ return { text: await syncManagedRouting(api) };
78
+ }
79
+
75
80
  if (cmd === "config") {
76
81
  const action = (parts[1] ?? "get").toLowerCase();
77
82
  if (action === "get") {
@@ -106,6 +111,7 @@ export function registerChatCommands(api: OpenClawPluginApi, metrics: MetricsSto
106
111
  "/ocsmarttools config keys",
107
112
  "/ocsmarttools config set <key> <value>",
108
113
  "/ocsmarttools config reset [key]",
114
+ "/ocsmarttools sync",
109
115
  ].join("\n"),
110
116
  };
111
117
  }
@@ -10,6 +10,7 @@ import {
10
10
  resetStats,
11
11
  resetConfig,
12
12
  setConfigKey,
13
+ syncManagedRouting,
13
14
  updateMode,
14
15
  } from "./operations.js";
15
16
  import type { AdvToolsMode } from "../lib/plugin-config.js";
@@ -52,16 +53,13 @@ export function registerCliCommands(api: OpenClawPluginApi, metrics: MetricsStor
52
53
  console.log(renderStatus(api));
53
54
  });
54
55
 
55
- adv
56
- .command("stats")
57
- .description("Show usage/savings metrics")
58
- .action(() => {
59
- // eslint-disable-next-line no-console
60
- console.log(renderStats(metrics));
61
- });
62
-
63
- adv
64
- .command("stats reset")
56
+ const statsCmd = adv.command("stats").description("Show usage/savings metrics");
57
+ statsCmd.action(() => {
58
+ // eslint-disable-next-line no-console
59
+ console.log(renderStats(metrics));
60
+ });
61
+ statsCmd
62
+ .command("reset")
65
63
  .description("Reset usage/savings metrics window")
66
64
  .action(() => {
67
65
  // eslint-disable-next-line no-console
@@ -91,6 +89,15 @@ export function registerCliCommands(api: OpenClawPluginApi, metrics: MetricsStor
91
89
  console.log(text);
92
90
  });
93
91
 
92
+ adv
93
+ .command("sync")
94
+ .description("Re-apply auto-managed routing policy to AGENTS.md")
95
+ .action(async () => {
96
+ const text = await syncManagedRouting(api);
97
+ // eslint-disable-next-line no-console
98
+ console.log(text);
99
+ });
100
+
94
101
  adv
95
102
  .command("config")
96
103
  .description("Show effective plugin config")
@@ -9,6 +9,7 @@ import {
9
9
  writeConfig,
10
10
  } from "../lib/plugin-config.js";
11
11
  import type { MetricsStore } from "../lib/metrics-store.js";
12
+ import { syncRoutingGuide } from "../lib/routing-guide.js";
12
13
 
13
14
  const ADVTOOLS_TOOL_NAMES = ["tool_search", "tool_dispatch", "tool_batch", "tool_result_get"];
14
15
  type ConfigKind = "boolean" | "integer" | "enum";
@@ -20,6 +21,7 @@ type ConfigSpec =
20
21
  const CONFIG_SPECS: Record<string, ConfigSpec> = {
21
22
  enabled: { kind: "boolean" },
22
23
  mode: { kind: "enum", values: ["safe", "standard"] },
24
+ autoInjectRoutingGuide: { kind: "boolean" },
23
25
  maxSteps: { kind: "integer", min: 1, max: 200 },
24
26
  maxForEach: { kind: "integer", min: 1, max: 200 },
25
27
  maxResultChars: { kind: "integer", min: 500, max: 500000 },
@@ -38,6 +40,7 @@ const CONFIG_SPECS: Record<string, ConfigSpec> = {
38
40
  const DEFAULT_BY_KEY: Record<string, boolean | number | string> = {
39
41
  enabled: DEFAULT_SETTINGS.enabled,
40
42
  mode: DEFAULT_SETTINGS.mode,
43
+ autoInjectRoutingGuide: DEFAULT_SETTINGS.autoInjectRoutingGuide,
41
44
  maxSteps: DEFAULT_SETTINGS.maxSteps,
42
45
  maxForEach: DEFAULT_SETTINGS.maxForEach,
43
46
  maxResultChars: DEFAULT_SETTINGS.maxResultChars,
@@ -140,6 +143,7 @@ export function renderStatus(api: OpenClawPluginApi): string {
140
143
  "OCSmartTools Status",
141
144
  `- plugin: ${api.id}`,
142
145
  `- mode: ${s.mode}`,
146
+ `- autoInjectRoutingGuide: ${s.autoInjectRoutingGuide}`,
143
147
  `- tool_search enabled: ${s.toolSearch.enabled}`,
144
148
  `- maxSteps: ${s.maxSteps}`,
145
149
  `- maxForEach: ${s.maxForEach}`,
@@ -165,6 +169,7 @@ export function renderHelp(): string {
165
169
  "- /ocsmarttools stats reset: Reset metrics window",
166
170
  "- /ocsmarttools setup [safe|standard]: Apply recommended defaults (default: standard)",
167
171
  "- /ocsmarttools mode <safe|standard>: Switch only the operating mode",
172
+ "- /ocsmarttools sync: Re-apply auto-managed routing policy to AGENTS.md",
168
173
  "- /ocsmarttools config: Show effective plugin config",
169
174
  "- /ocsmarttools config keys: List editable config keys",
170
175
  "- /ocsmarttools config set <key> <value>: Update one config key",
@@ -229,6 +234,7 @@ export function renderConfig(api: OpenClawPluginApi): string {
229
234
  "OCSmartTools Config",
230
235
  `- enabled: ${s.enabled}`,
231
236
  `- mode: ${s.mode}`,
237
+ `- autoInjectRoutingGuide: ${s.autoInjectRoutingGuide}`,
232
238
  `- maxSteps: ${s.maxSteps}`,
233
239
  `- maxForEach: ${s.maxForEach}`,
234
240
  `- maxResultChars: ${s.maxResultChars}`,
@@ -273,6 +279,13 @@ export async function setConfigKey(
273
279
  setValueAtPath(pluginCfg, key, parsed.value);
274
280
  await writeConfig(api, next);
275
281
 
282
+ if (key === "autoInjectRoutingGuide" && parsed.value === true) {
283
+ const sync = await syncRoutingGuide(api, next);
284
+ if (sync.error) {
285
+ return `Config updated: ${key}=${JSON.stringify(parsed.value)} (routing sync skipped: ${sync.error}).`;
286
+ }
287
+ }
288
+
276
289
  return `Config updated: ${key}=${JSON.stringify(parsed.value)}.`;
277
290
  }
278
291
 
@@ -288,6 +301,12 @@ export async function resetConfig(api: OpenClawPluginApi, key?: string): Promise
288
301
  setValueAtPath(pluginCfg, cfgKey, DEFAULT_BY_KEY[cfgKey]);
289
302
  }
290
303
  await writeConfig(api, next);
304
+ if (DEFAULT_SETTINGS.autoInjectRoutingGuide) {
305
+ const sync = await syncRoutingGuide(api, next);
306
+ if (sync.error) {
307
+ return `Config reset to plugin defaults (routing sync skipped: ${sync.error}).`;
308
+ }
309
+ }
291
310
  return "Config reset to plugin defaults.";
292
311
  }
293
312
 
@@ -297,6 +316,12 @@ export async function resetConfig(api: OpenClawPluginApi, key?: string): Promise
297
316
 
298
317
  setValueAtPath(pluginCfg, key, DEFAULT_BY_KEY[key]);
299
318
  await writeConfig(api, next);
319
+ if (key === "autoInjectRoutingGuide" && DEFAULT_BY_KEY[key] === true) {
320
+ const sync = await syncRoutingGuide(api, next);
321
+ if (sync.error) {
322
+ return `Config key reset: ${key}=${JSON.stringify(DEFAULT_BY_KEY[key])} (routing sync skipped: ${sync.error}).`;
323
+ }
324
+ }
300
325
  return `Config key reset: ${key}=${JSON.stringify(DEFAULT_BY_KEY[key])}.`;
301
326
  }
302
327
 
@@ -320,6 +345,7 @@ export async function applySetup(api: OpenClawPluginApi, mode: AdvToolsMode): Pr
320
345
 
321
346
  pluginCfg.enabled = true;
322
347
  pluginCfg.mode = mode;
348
+ pluginCfg.autoInjectRoutingGuide = true;
323
349
  pluginCfg.maxSteps = DEFAULT_SETTINGS.maxSteps;
324
350
  pluginCfg.maxForEach = DEFAULT_SETTINGS.maxForEach;
325
351
  pluginCfg.maxResultChars = DEFAULT_SETTINGS.maxResultChars;
@@ -344,10 +370,16 @@ export async function applySetup(api: OpenClawPluginApi, mode: AdvToolsMode): Pr
344
370
  );
345
371
 
346
372
  await writeConfig(api, next);
373
+ const sync = await syncRoutingGuide(api, next);
374
+ const syncLine = sync.error
375
+ ? `- routing guide sync skipped: ${sync.error}`
376
+ : `- routing guide ${sync.changed ? "synced" : "already up to date"} (${sync.filePath ?? "AGENTS.md"})`;
347
377
 
348
378
  return [
349
379
  "OCSmartTools setup applied.",
350
380
  `- mode: ${mode}`,
381
+ "- autoInjectRoutingGuide: true",
382
+ syncLine,
351
383
  `- ensured tools.allow includes: ${ADVTOOLS_TOOL_NAMES.join(", ")}`,
352
384
  "- config written via runtime config writer",
353
385
  "If your gateway does not hot-apply this change, run: openclaw gateway restart",
@@ -368,3 +400,19 @@ export async function updateMode(api: OpenClawPluginApi, mode: AdvToolsMode): Pr
368
400
  await writeConfig(api, next);
369
401
  return `Mode updated to ${mode}.`;
370
402
  }
403
+
404
+ export async function syncManagedRouting(api: OpenClawPluginApi): Promise<string> {
405
+ const loaded = api.runtime.config.loadConfig();
406
+ const settings = resolveSettings(api, loaded);
407
+ if (!settings.autoInjectRoutingGuide) {
408
+ return "Routing guide sync skipped: autoInjectRoutingGuide=false.";
409
+ }
410
+ const result = await syncRoutingGuide(api, loaded);
411
+ if (result.error) {
412
+ return `Routing guide sync skipped: ${result.error}`;
413
+ }
414
+ if (!result.changed) {
415
+ return `Routing guide already up to date (${result.filePath ?? "AGENTS.md"}).`;
416
+ }
417
+ return `Routing guide synced (${result.filePath ?? "AGENTS.md"}).`;
418
+ }
@@ -6,6 +6,7 @@ import {
6
6
  mergeUniqueStrings,
7
7
  writeConfig,
8
8
  } from "./plugin-config.js";
9
+ import { syncRoutingGuide } from "./routing-guide.js";
9
10
 
10
11
  const TOOL_NAMES = ["tool_search", "tool_dispatch", "tool_batch", "tool_result_get"];
11
12
 
@@ -45,6 +46,7 @@ export async function autoBootstrap(api: OpenClawPluginApi): Promise<{ changed:
45
46
 
46
47
  setDefault("enabled", true);
47
48
  setDefault("mode", DEFAULT_SETTINGS.mode);
49
+ setDefault("autoInjectRoutingGuide", DEFAULT_SETTINGS.autoInjectRoutingGuide);
48
50
  setDefault("maxSteps", DEFAULT_SETTINGS.maxSteps);
49
51
  setDefault("maxForEach", DEFAULT_SETTINGS.maxForEach);
50
52
  setDefault("maxResultChars", DEFAULT_SETTINGS.maxResultChars);
@@ -110,5 +112,19 @@ export async function autoBootstrap(api: OpenClawPluginApi): Promise<{ changed:
110
112
  if (changed) {
111
113
  await writeConfig(api, next);
112
114
  }
115
+
116
+ const routingGuideEnabled =
117
+ typeof pluginCfg.autoInjectRoutingGuide === "boolean"
118
+ ? pluginCfg.autoInjectRoutingGuide
119
+ : DEFAULT_SETTINGS.autoInjectRoutingGuide;
120
+ if (routingGuideEnabled) {
121
+ const sync = await syncRoutingGuide(api, next);
122
+ if (sync.error) {
123
+ notes.push(`routing guide skipped: ${sync.error}`);
124
+ } else if (sync.changed) {
125
+ notes.push(`synced routing guide (${sync.filePath ?? "AGENTS.md"})`);
126
+ }
127
+ }
128
+
113
129
  return { changed, notes };
114
130
  }
@@ -5,6 +5,7 @@ export type AdvToolsMode = "safe" | "standard";
5
5
  export type AdvToolsSettings = {
6
6
  enabled: boolean;
7
7
  mode: AdvToolsMode;
8
+ autoInjectRoutingGuide: boolean;
8
9
  maxSteps: number;
9
10
  maxForEach: number;
10
11
  maxResultChars: number;
@@ -25,6 +26,7 @@ export type AdvToolsSettings = {
25
26
  export const DEFAULT_SETTINGS: AdvToolsSettings = {
26
27
  enabled: true,
27
28
  mode: "standard",
29
+ autoInjectRoutingGuide: true,
28
30
  maxSteps: 25,
29
31
  maxForEach: 20,
30
32
  maxResultChars: 40000,
@@ -73,6 +75,10 @@ export function resolveSettings(api: OpenClawPluginApi, cfg: OpenClawConfig = ap
73
75
  return {
74
76
  enabled: asBool(pluginCfg.enabled, DEFAULT_SETTINGS.enabled),
75
77
  mode: asMode(pluginCfg.mode, DEFAULT_SETTINGS.mode),
78
+ autoInjectRoutingGuide: asBool(
79
+ pluginCfg.autoInjectRoutingGuide,
80
+ DEFAULT_SETTINGS.autoInjectRoutingGuide,
81
+ ),
76
82
  maxSteps: asInt(pluginCfg.maxSteps, DEFAULT_SETTINGS.maxSteps, 1, 200),
77
83
  maxForEach: asInt(pluginCfg.maxForEach, DEFAULT_SETTINGS.maxForEach, 1, 200),
78
84
  maxResultChars: asInt(pluginCfg.maxResultChars, DEFAULT_SETTINGS.maxResultChars, 500, 500000),
@@ -0,0 +1,97 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk";
5
+
6
+ const BLOCK_START = "<!-- OCSMARTTOOLS_ROUTING_START -->";
7
+ const BLOCK_END = "<!-- OCSMARTTOOLS_ROUTING_END -->";
8
+
9
+ const ROUTING_BLOCK = `${BLOCK_START}
10
+ ## OCSmartTools Routing Policy (Auto-Managed)
11
+
12
+ Default objective: preserve answer quality while reducing token and latency cost.
13
+
14
+ 1. If tool usage is needed and result size is uncertain, use \`tool_dispatch\`.
15
+ 2. If the task needs 2+ related tool calls, use \`tool_batch\`.
16
+ 3. Use \`tool_search\` only when tool choice is unclear.
17
+ 4. Prefer compact/tool-shaped outputs; use \`tool_result_get\` only when more detail is required.
18
+ 5. Use direct native tool calls only for simple one-shot small-output actions.
19
+
20
+ Common large/noisy tools: \`web_fetch\`, \`read\` (large files), \`exec\`, \`process\`, \`browser\`, \`nodes\`.
21
+ ${BLOCK_END}
22
+ `;
23
+
24
+ function asObj(value: unknown): Record<string, unknown> {
25
+ return value && typeof value === "object" && !Array.isArray(value)
26
+ ? (value as Record<string, unknown>)
27
+ : {};
28
+ }
29
+
30
+ function resolveWorkspaceDir(cfg: OpenClawConfig): string {
31
+ const root = cfg as Record<string, unknown>;
32
+ const agents = asObj(root.agents);
33
+ const defaults = asObj(agents.defaults);
34
+ const configured = defaults.workspace;
35
+ if (typeof configured === "string" && configured.trim()) {
36
+ return configured.trim();
37
+ }
38
+ if (typeof process.env.OPENCLAW_WORKSPACE === "string" && process.env.OPENCLAW_WORKSPACE.trim()) {
39
+ return process.env.OPENCLAW_WORKSPACE.trim();
40
+ }
41
+ return path.join(os.homedir(), ".openclaw", "workspace");
42
+ }
43
+
44
+ function upsertRoutingBlock(raw: string): string {
45
+ const source = raw.trim()
46
+ ? raw
47
+ : "# AGENTS.md - Workspace Directives\n\nAdd local operating preferences below.\n";
48
+
49
+ const start = source.indexOf(BLOCK_START);
50
+ const end = source.indexOf(BLOCK_END);
51
+ if (start >= 0 && end > start) {
52
+ const before = source.slice(0, start).replace(/\s*$/, "");
53
+ const after = source.slice(end + BLOCK_END.length).replace(/^\s*/, "");
54
+ return `${before}\n\n${ROUTING_BLOCK}\n${after}`.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
55
+ }
56
+ const joined = `${source.replace(/\s*$/, "")}\n\n${ROUTING_BLOCK}\n`;
57
+ return joined.replace(/\n{3,}/g, "\n\n");
58
+ }
59
+
60
+ export async function syncRoutingGuide(api: OpenClawPluginApi, cfg: OpenClawConfig): Promise<{
61
+ changed: boolean;
62
+ filePath?: string;
63
+ error?: string;
64
+ }> {
65
+ const workspaceDir = resolveWorkspaceDir(cfg);
66
+ const filePath = path.join(workspaceDir, "AGENTS.md");
67
+ let current = "";
68
+ try {
69
+ current = await fs.readFile(filePath, "utf8");
70
+ } catch (error) {
71
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
72
+ return {
73
+ changed: false,
74
+ filePath,
75
+ error: `read failed: ${error instanceof Error ? error.message : String(error)}`,
76
+ };
77
+ }
78
+ }
79
+
80
+ const next = upsertRoutingBlock(current);
81
+ if (next === current) {
82
+ return { changed: false, filePath };
83
+ }
84
+
85
+ try {
86
+ await fs.mkdir(workspaceDir, { recursive: true });
87
+ await fs.writeFile(filePath, next, "utf8");
88
+ } catch (error) {
89
+ return {
90
+ changed: false,
91
+ filePath,
92
+ error: `write failed: ${error instanceof Error ? error.message : String(error)}`,
93
+ };
94
+ }
95
+ api.logger.info(`[ocsmarttools] routing policy synced: ${filePath}`);
96
+ return { changed: true, filePath };
97
+ }