@karmaniverous/jeeves 0.4.2 → 0.4.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.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import * as commander from 'commander';
3
3
  import { execSync, spawnSync } from 'node:child_process';
4
- import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, cpSync, rmSync } from 'node:fs';
4
+ import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, cpSync, rmSync } from 'node:fs';
5
5
  import { join, dirname } from 'node:path';
6
- import { gte } from 'semver';
6
+ import { valid, gte } from 'semver';
7
7
  import { z } from 'zod';
8
8
  import { lock } from 'proper-lockfile';
9
9
  import { fileURLToPath } from 'node:url';
@@ -261,14 +261,14 @@ const PLATFORM_COMPONENTS = [
261
261
  * Core library version, inlined at build time.
262
262
  *
263
263
  * @remarks
264
- * The `0.4.1` placeholder is replaced by
264
+ * The `0.4.3` placeholder is replaced by
265
265
  * `@rollup/plugin-replace` during the build with the actual version
266
266
  * from `package.json`. This ensures the correct version survives
267
267
  * when consumers bundle core into their own dist (where runtime
268
268
  * `import.meta.url`-based resolution would find the wrong package.json).
269
269
  */
270
270
  /** The core library version from package.json (inlined at build time). */
271
- const CORE_VERSION = '0.4.1';
271
+ const CORE_VERSION = '0.4.3';
272
272
 
273
273
  /**
274
274
  * Dynamic discovery of installed Jeeves component CLIs.
@@ -409,7 +409,7 @@ var init$1 = /*#__PURE__*/Object.freeze({
409
409
  /** Zod schema for a service entry in core config. */
410
410
  const serviceEntrySchema = z.object({
411
411
  /** Service URL (must be a valid URL). */
412
- url: z.string().url().describe('Service URL'),
412
+ url: z.url().describe('Service URL'),
413
413
  });
414
414
  /** Default bind address for all Jeeves services. */
415
415
  const DEFAULT_BIND_ADDRESS = '0.0.0.0';
@@ -443,7 +443,7 @@ const coreConfigSchema = z.object({
443
443
  .default(3600)
444
444
  .describe('Cache TTL in seconds'),
445
445
  })
446
- .default({})
446
+ .prefault({})
447
447
  .describe('Registry cache settings'),
448
448
  });
449
449
  /**
@@ -598,7 +598,18 @@ function atomicWrite(filePath, content) {
598
598
  const dir = dirname(filePath);
599
599
  const tempPath = join(dir, `.${String(Date.now())}.tmp`);
600
600
  writeFileSync(tempPath, content, 'utf-8');
601
- renameSync(tempPath, filePath);
601
+ try {
602
+ renameSync(tempPath, filePath);
603
+ }
604
+ catch (err) {
605
+ try {
606
+ unlinkSync(tempPath);
607
+ }
608
+ catch {
609
+ /* best-effort cleanup */
610
+ }
611
+ throw err;
612
+ }
602
613
  }
603
614
  /**
604
615
  * Execute a callback while holding a file lock.
@@ -1460,8 +1471,9 @@ function formatEndMarker(markerText) {
1460
1471
  * @returns `true` if the writer should proceed with the write.
1461
1472
  */
1462
1473
  function shouldWrite(myVersion, existing, stalenessThresholdMs = STALENESS_THRESHOLD_MS) {
1463
- // No existing stamp — always write
1464
- if (!existing)
1474
+ // Always write when version comparison is impossible: invalid/placeholder
1475
+ // versions (dev/test mode) or no existing stamp to compare against.
1476
+ if (!valid(myVersion) || !existing || !valid(existing.version))
1465
1477
  return true;
1466
1478
  // My version >= stamped version — always write (I'm current or newer)
1467
1479
  if (gte(myVersion, existing.version))
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { writeFileSync, renameSync, existsSync, readFileSync, rmSync, mkdirSync, readdirSync, copyFileSync } from 'node:fs';
2
+ import { writeFileSync, renameSync, unlinkSync, existsSync, readFileSync, rmSync, mkdirSync, readdirSync, copyFileSync } from 'node:fs';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import * as commander from 'commander';
5
5
  import { lock } from 'proper-lockfile';
@@ -119,14 +119,14 @@ const COMPONENT_VERSIONS_FILE = 'component-versions.json';
119
119
  * Core library version, inlined at build time.
120
120
  *
121
121
  * @remarks
122
- * The `0.4.1` placeholder is replaced by
122
+ * The `0.4.3` placeholder is replaced by
123
123
  * `@rollup/plugin-replace` during the build with the actual version
124
124
  * from `package.json`. This ensures the correct version survives
125
125
  * when consumers bundle core into their own dist (where runtime
126
126
  * `import.meta.url`-based resolution would find the wrong package.json).
127
127
  */
128
128
  /** The core library version from package.json (inlined at build time). */
129
- const CORE_VERSION = '0.4.1';
129
+ const CORE_VERSION = '0.4.3';
130
130
 
131
131
  /**
132
132
  * Shared file I/O helpers for managed section operations.
@@ -152,7 +152,18 @@ function atomicWrite(filePath, content) {
152
152
  const dir = dirname(filePath);
153
153
  const tempPath = join(dir, `.${String(Date.now())}.tmp`);
154
154
  writeFileSync(tempPath, content, 'utf-8');
155
- renameSync(tempPath, filePath);
155
+ try {
156
+ renameSync(tempPath, filePath);
157
+ }
158
+ catch (err) {
159
+ try {
160
+ unlinkSync(tempPath);
161
+ }
162
+ catch {
163
+ /* best-effort cleanup */
164
+ }
165
+ throw err;
166
+ }
156
167
  }
157
168
  /**
158
169
  * Execute a callback while holding a file lock.
@@ -151,16 +151,20 @@ z.object({
151
151
  /** Config file name (e.g., 'jeeves-watcher.config.json'). */
152
152
  configFileName: z.string().min(1),
153
153
  /** Returns a default config object for `init`. */
154
- initTemplate: z.function().returns(z.record(z.unknown())),
154
+ initTemplate: z.function({
155
+ input: [],
156
+ output: z.record(z.string(), z.unknown()),
157
+ }),
155
158
  /**
156
159
  * Service-side callback after config apply. Receives the merged,
157
160
  * validated config (not the raw patch). Optional — if omitted,
158
161
  * write-only (service picks up changes on restart).
159
162
  */
160
163
  onConfigApply: z
161
- .function()
162
- .args(z.record(z.unknown()))
163
- .returns(z.promise(z.void()))
164
+ .function({
165
+ input: [z.record(z.string(), z.unknown())],
166
+ output: z.promise(z.void()),
167
+ })
164
168
  .optional(),
165
169
  /**
166
170
  * Custom merge function for config apply. Receives the existing config
@@ -171,15 +175,22 @@ z.object({
171
175
  * name-based array merging for inference rules.
172
176
  */
173
177
  customMerge: z
174
- .function()
175
- .args(z.record(z.unknown()), z.record(z.unknown()))
176
- .returns(z.record(z.unknown()))
178
+ .function({
179
+ input: [
180
+ z.record(z.string(), z.unknown()),
181
+ z.record(z.string(), z.unknown()),
182
+ ],
183
+ output: z.record(z.string(), z.unknown()),
184
+ })
177
185
  .optional(),
178
186
  /**
179
187
  * Returns command + args for launching the service process.
180
188
  * Consumed by `start` CLI command and `service install`.
181
189
  */
182
- startCommand: z.function().args(z.string()).returns(z.array(z.string())),
190
+ startCommand: z.function({
191
+ input: [z.string()],
192
+ output: z.array(z.string()),
193
+ }),
183
194
  /** TOOLS.md section name (e.g., 'Watcher'). */
184
195
  sectionId: z.string().min(1, 'sectionId must be a non-empty string'),
185
196
  /** Refresh interval in seconds (must be a prime number). */
@@ -187,25 +198,23 @@ z.object({
187
198
  message: 'refreshIntervalSeconds must be a prime number',
188
199
  }),
189
200
  /** Produce the component's TOOLS.md section content. */
190
- generateToolsContent: z.function().returns(z.string()),
201
+ generateToolsContent: z.function({ input: [], output: z.string() }),
191
202
  /** Component dependencies for HEARTBEAT alert suppression. */
192
203
  dependencies: z
193
204
  .object({
205
+ /** Components that must be healthy for this component to function. */
194
206
  hard: z.array(z.string()),
207
+ /** Components that improve behavior but are not strictly required. */
195
208
  soft: z.array(z.string()),
196
209
  })
197
210
  .optional(),
198
211
  /** Extension point: add custom CLI commands to the service CLI. */
199
212
  customCliCommands: z
200
- .function()
201
- .args(z.custom())
202
- .returns(z.void())
213
+ .function({ input: [z.custom()], output: z.void() })
203
214
  .optional(),
204
215
  /** Extension point: return additional plugin tool descriptors. */
205
216
  customPluginTools: z
206
- .function()
207
- .args(z.custom())
208
- .returns(z.array(z.unknown()))
217
+ .function({ input: [z.custom()], output: z.array(z.unknown()) })
209
218
  .optional(),
210
219
  });
211
220
  /**
@@ -674,7 +683,7 @@ function createServiceManager(descriptor) {
674
683
  * Standard commands:
675
684
  * - `start -c <path>` - Launch the service process (foreground)
676
685
  * - `status [-p port]` - Probe service health
677
- * - `config [jsonpath] [-p port]` - Query running config
686
+ * - `config [jsonpath] [-p port]` - Query running config (jsonpath optional)
678
687
  * - `config validate -c <path>` - Validate a config file
679
688
  * - `config apply [-p port] [--file path] [--replace]` - Apply config patch
680
689
  * - `init [-o path]` - Generate default config
@@ -730,10 +739,7 @@ function createServiceCli(descriptor) {
730
739
  // --- config ---
731
740
  const configCmd = program
732
741
  .command('config')
733
- .description('Query or manage service configuration');
734
- configCmd
735
- .command('query')
736
- .description('Query running service config via JSONPath')
742
+ .description('Query or manage service configuration')
737
743
  .argument('[jsonpath]', 'JSONPath expression')
738
744
  .option('-p, --port <port>', 'Service port', String(descriptor.defaultPort))
739
745
  .action(async (jsonpath, opts) => {
@@ -790,9 +796,12 @@ function createServiceCli(descriptor) {
790
796
  patch = JSON.parse(input);
791
797
  }
792
798
  }
793
- const qs = opts.replace ? '?replace=true' : '';
799
+ const body = { patch };
800
+ if (opts.replace) {
801
+ body.replace = true;
802
+ }
794
803
  try {
795
- const result = await postJson(`${url}/config/apply${qs}`, patch);
804
+ const result = await postJson(`${url}/config/apply`, body);
796
805
  console.log(JSON.stringify(result, null, 2));
797
806
  }
798
807
  catch (err) {
package/dist/index.d.ts CHANGED
@@ -106,110 +106,28 @@ declare function isPrime(n: number): boolean;
106
106
  * plugin tools, and HTTP handlers.
107
107
  */
108
108
  declare const jeevesComponentDescriptorSchema: z.ZodObject<{
109
- /** Component name (e.g., 'watcher', 'runner', 'server', 'meta'). */
110
109
  name: z.ZodString;
111
- /** Component version (from package.json). */
112
110
  version: z.ZodString;
113
- /** npm package name for the service. */
114
111
  servicePackage: z.ZodString;
115
- /** npm package name for the plugin. */
116
112
  pluginPackage: z.ZodString;
117
- /** System service name. Defaults to `jeeves-${name}` when not provided. */
118
113
  serviceName: z.ZodOptional<z.ZodString>;
119
- /** Default port for the service's HTTP API. */
120
114
  defaultPort: z.ZodNumber;
121
- /** Zod schema for validating config files. */
122
- configSchema: z.ZodType<z.ZodTypeAny, z.ZodTypeDef, z.ZodTypeAny>;
123
- /** Config file name (e.g., 'jeeves-watcher.config.json'). */
115
+ configSchema: z.ZodCustom<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
124
116
  configFileName: z.ZodString;
125
- /** Returns a default config object for `init`. */
126
- initTemplate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
127
- /**
128
- * Service-side callback after config apply. Receives the merged,
129
- * validated config (not the raw patch). Optional — if omitted,
130
- * write-only (service picks up changes on restart).
131
- */
132
- onConfigApply: z.ZodOptional<z.ZodFunction<z.ZodTuple<[z.ZodRecord<z.ZodString, z.ZodUnknown>], z.ZodUnknown>, z.ZodPromise<z.ZodVoid>>>;
133
- /**
134
- * Custom merge function for config apply. Receives the existing config
135
- * and the patch, returns the merged result. Optional — if omitted,
136
- * the default deep-merge (object-recursive, array-replacing) is used.
137
- *
138
- * Use this to implement domain-specific merge strategies such as
139
- * name-based array merging for inference rules.
140
- */
141
- customMerge: z.ZodOptional<z.ZodFunction<z.ZodTuple<[z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodRecord<z.ZodString, z.ZodUnknown>], z.ZodUnknown>, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
142
- /**
143
- * Returns command + args for launching the service process.
144
- * Consumed by `start` CLI command and `service install`.
145
- */
146
- startCommand: z.ZodFunction<z.ZodTuple<[z.ZodString], z.ZodUnknown>, z.ZodArray<z.ZodString, "many">>;
147
- /** TOOLS.md section name (e.g., 'Watcher'). */
117
+ initTemplate: z.ZodFunction<z.ZodTuple<readonly [], null>, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
118
+ onConfigApply: z.ZodOptional<z.ZodFunction<z.ZodTuple<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>], null>, z.ZodPromise<z.ZodVoid>>>;
119
+ customMerge: z.ZodOptional<z.ZodFunction<z.ZodTuple<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodRecord<z.ZodString, z.ZodUnknown>], null>, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
120
+ startCommand: z.ZodFunction<z.ZodTuple<readonly [z.ZodString], null>, z.ZodArray<z.ZodString>>;
148
121
  sectionId: z.ZodString;
149
- /** Refresh interval in seconds (must be a prime number). */
150
- refreshIntervalSeconds: z.ZodEffects<z.ZodNumber, number, number>;
151
- /** Produce the component's TOOLS.md section content. */
152
- generateToolsContent: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodString>;
153
- /** Component dependencies for HEARTBEAT alert suppression. */
122
+ refreshIntervalSeconds: z.ZodNumber;
123
+ generateToolsContent: z.ZodFunction<z.ZodTuple<readonly [], null>, z.ZodString>;
154
124
  dependencies: z.ZodOptional<z.ZodObject<{
155
- hard: z.ZodArray<z.ZodString, "many">;
156
- soft: z.ZodArray<z.ZodString, "many">;
157
- }, "strip", z.ZodTypeAny, {
158
- hard: string[];
159
- soft: string[];
160
- }, {
161
- hard: string[];
162
- soft: string[];
163
- }>>;
164
- /** Extension point: add custom CLI commands to the service CLI. */
165
- customCliCommands: z.ZodOptional<z.ZodFunction<z.ZodTuple<[z.ZodType<Command<[], {}, {}>, z.ZodTypeDef, Command<[], {}, {}>>], z.ZodUnknown>, z.ZodVoid>>;
166
- /** Extension point: return additional plugin tool descriptors. */
167
- customPluginTools: z.ZodOptional<z.ZodFunction<z.ZodTuple<[z.ZodType<PluginApi, z.ZodTypeDef, PluginApi>], z.ZodUnknown>, z.ZodArray<z.ZodUnknown, "many">>>;
168
- }, "strip", z.ZodTypeAny, {
169
- name: string;
170
- version: string;
171
- servicePackage: string;
172
- pluginPackage: string;
173
- defaultPort: number;
174
- configSchema: z.ZodTypeAny;
175
- configFileName: string;
176
- initTemplate: (...args: unknown[]) => Record<string, unknown>;
177
- startCommand: (args_0: string, ...args: unknown[]) => string[];
178
- sectionId: string;
179
- refreshIntervalSeconds: number;
180
- generateToolsContent: (...args: unknown[]) => string;
181
- serviceName?: string | undefined;
182
- onConfigApply?: ((args_0: Record<string, unknown>, ...args: unknown[]) => Promise<void>) | undefined;
183
- customMerge?: ((args_0: Record<string, unknown>, args_1: Record<string, unknown>, ...args: unknown[]) => Record<string, unknown>) | undefined;
184
- dependencies?: {
185
- hard: string[];
186
- soft: string[];
187
- } | undefined;
188
- customCliCommands?: ((args_0: Command<[], {}, {}>, ...args: unknown[]) => void) | undefined;
189
- customPluginTools?: ((args_0: PluginApi, ...args: unknown[]) => unknown[]) | undefined;
190
- }, {
191
- name: string;
192
- version: string;
193
- servicePackage: string;
194
- pluginPackage: string;
195
- defaultPort: number;
196
- configSchema: z.ZodTypeAny;
197
- configFileName: string;
198
- initTemplate: (...args: unknown[]) => Record<string, unknown>;
199
- startCommand: (args_0: string, ...args: unknown[]) => string[];
200
- sectionId: string;
201
- refreshIntervalSeconds: number;
202
- generateToolsContent: (...args: unknown[]) => string;
203
- serviceName?: string | undefined;
204
- onConfigApply?: ((args_0: Record<string, unknown>, ...args: unknown[]) => Promise<void>) | undefined;
205
- customMerge?: ((args_0: Record<string, unknown>, args_1: Record<string, unknown>, ...args: unknown[]) => Record<string, unknown>) | undefined;
206
- dependencies?: {
207
- hard: string[];
208
- soft: string[];
209
- } | undefined;
210
- customCliCommands?: ((args_0: Command<[], {}, {}>, ...args: unknown[]) => void) | undefined;
211
- customPluginTools?: ((args_0: PluginApi, ...args: unknown[]) => unknown[]) | undefined;
212
- }>;
125
+ hard: z.ZodArray<z.ZodString>;
126
+ soft: z.ZodArray<z.ZodString>;
127
+ }, z.core.$strip>>;
128
+ customCliCommands: z.ZodOptional<z.ZodFunction<z.ZodTuple<readonly [z.ZodCustom<Command<[], {}, {}>, Command<[], {}, {}>>], null>, z.ZodVoid>>;
129
+ customPluginTools: z.ZodOptional<z.ZodFunction<z.ZodTuple<readonly [z.ZodCustom<PluginApi, PluginApi>], null>, z.ZodArray<z.ZodUnknown>>>;
130
+ }, z.core.$strip>;
213
131
  /** Inferred TypeScript type for the component descriptor. */
214
132
  type JeevesComponentDescriptor = z.infer<typeof jeevesComponentDescriptorSchema>;
215
133
  /**
@@ -394,7 +312,7 @@ declare function createPluginCli(options: CreatePluginCliOptions): Command;
394
312
  * Standard commands:
395
313
  * - `start -c <path>` - Launch the service process (foreground)
396
314
  * - `status [-p port]` - Probe service health
397
- * - `config [jsonpath] [-p port]` - Query running config
315
+ * - `config [jsonpath] [-p port]` - Query running config (jsonpath optional)
398
316
  * - `config validate -c <path>` - Validate a config file
399
317
  * - `config apply [-p port] [--file path] [--replace]` - Apply config patch
400
318
  * - `init [-o path]` - Generate default config
@@ -871,54 +789,16 @@ declare const CORE_VERSION: string;
871
789
  declare const DEFAULT_BIND_ADDRESS = "0.0.0.0";
872
790
  /** Zod schema for the core config file. */
873
791
  declare const coreConfigSchema: z.ZodObject<{
874
- /** JSON Schema pointer for IDE autocomplete. */
875
792
  $schema: z.ZodOptional<z.ZodString>;
876
- /** Owner identity keys (canonical identityLinks references). */
877
- owners: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
878
- /**
879
- * Bind address for all Jeeves services. Default: `0.0.0.0` (all interfaces).
880
- * Individual components can override in their own config.
881
- */
793
+ owners: z.ZodDefault<z.ZodArray<z.ZodString>>;
882
794
  bindAddress: z.ZodDefault<z.ZodString>;
883
- /** Service URL overrides keyed by service name. */
884
795
  services: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
885
- /** Service URL (must be a valid URL). */
886
- url: z.ZodString;
887
- }, "strip", z.ZodTypeAny, {
888
- url: string;
889
- }, {
890
- url: string;
891
- }>>>;
892
- /** Registry cache configuration. */
893
- registryCache: z.ZodDefault<z.ZodObject<{
894
- /** Cache TTL in seconds for npm registry queries. */
796
+ url: z.ZodURL;
797
+ }, z.core.$strip>>>;
798
+ registryCache: z.ZodPrefault<z.ZodObject<{
895
799
  ttlSeconds: z.ZodDefault<z.ZodNumber>;
896
- }, "strip", z.ZodTypeAny, {
897
- ttlSeconds: number;
898
- }, {
899
- ttlSeconds?: number | undefined;
900
- }>>;
901
- }, "strip", z.ZodTypeAny, {
902
- owners: string[];
903
- bindAddress: string;
904
- services: Record<string, {
905
- url: string;
906
- }>;
907
- registryCache: {
908
- ttlSeconds: number;
909
- };
910
- $schema?: string | undefined;
911
- }, {
912
- $schema?: string | undefined;
913
- owners?: string[] | undefined;
914
- bindAddress?: string | undefined;
915
- services?: Record<string, {
916
- url: string;
917
- }> | undefined;
918
- registryCache?: {
919
- ttlSeconds?: number | undefined;
920
- } | undefined;
921
- }>;
800
+ }, z.core.$strip>>;
801
+ }, z.core.$strip>;
922
802
  /** Core config type derived from the Zod schema. */
923
803
  type CoreConfig = z.infer<typeof coreConfigSchema>;
924
804
  /**
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { writeFileSync, renameSync, existsSync, readFileSync, mkdirSync, rmSync, readdirSync, copyFileSync, unlinkSync, cpSync } from 'node:fs';
1
+ import { writeFileSync, renameSync, unlinkSync, existsSync, readFileSync, mkdirSync, rmSync, readdirSync, copyFileSync, cpSync } from 'node:fs';
2
2
  import { join, dirname, resolve } from 'node:path';
3
3
  import { lock } from 'proper-lockfile';
4
4
  import { JSONPath } from 'jsonpath-plus';
5
5
  import * as commander from 'commander';
6
- import { gte, gt } from 'semver';
6
+ import { valid, gte, gt } from 'semver';
7
7
  import { homedir } from 'node:os';
8
8
  import { execSync, spawn } from 'node:child_process';
9
9
  import { z } from 'zod';
@@ -176,14 +176,14 @@ const PLATFORM_COMPONENTS = [
176
176
  * Core library version, inlined at build time.
177
177
  *
178
178
  * @remarks
179
- * The `0.4.1` placeholder is replaced by
179
+ * The `0.4.3` placeholder is replaced by
180
180
  * `@rollup/plugin-replace` during the build with the actual version
181
181
  * from `package.json`. This ensures the correct version survives
182
182
  * when consumers bundle core into their own dist (where runtime
183
183
  * `import.meta.url`-based resolution would find the wrong package.json).
184
184
  */
185
185
  /** The core library version from package.json (inlined at build time). */
186
- const CORE_VERSION = '0.4.1';
186
+ const CORE_VERSION = '0.4.3';
187
187
 
188
188
  /**
189
189
  * Workspace and config root initialization.
@@ -291,7 +291,18 @@ function atomicWrite(filePath, content) {
291
291
  const dir = dirname(filePath);
292
292
  const tempPath = join(dir, `.${String(Date.now())}.tmp`);
293
293
  writeFileSync(tempPath, content, 'utf-8');
294
- renameSync(tempPath, filePath);
294
+ try {
295
+ renameSync(tempPath, filePath);
296
+ }
297
+ catch (err) {
298
+ try {
299
+ unlinkSync(tempPath);
300
+ }
301
+ catch {
302
+ /* best-effort cleanup */
303
+ }
304
+ throw err;
305
+ }
295
306
  }
296
307
  /**
297
308
  * Execute a callback while holding a file lock.
@@ -1002,8 +1013,9 @@ function formatEndMarker(markerText) {
1002
1013
  * @returns `true` if the writer should proceed with the write.
1003
1014
  */
1004
1015
  function shouldWrite(myVersion, existing, stalenessThresholdMs = STALENESS_THRESHOLD_MS) {
1005
- // No existing stamp — always write
1006
- if (!existing)
1016
+ // Always write when version comparison is impossible: invalid/placeholder
1017
+ // versions (dev/test mode) or no existing stamp to compare against.
1018
+ if (!valid(myVersion) || !existing || !valid(existing.version))
1007
1019
  return true;
1008
1020
  // My version >= stamped version — always write (I'm current or newer)
1009
1021
  if (gte(myVersion, existing.version))
@@ -1491,16 +1503,20 @@ const jeevesComponentDescriptorSchema = z.object({
1491
1503
  /** Config file name (e.g., 'jeeves-watcher.config.json'). */
1492
1504
  configFileName: z.string().min(1),
1493
1505
  /** Returns a default config object for `init`. */
1494
- initTemplate: z.function().returns(z.record(z.unknown())),
1506
+ initTemplate: z.function({
1507
+ input: [],
1508
+ output: z.record(z.string(), z.unknown()),
1509
+ }),
1495
1510
  /**
1496
1511
  * Service-side callback after config apply. Receives the merged,
1497
1512
  * validated config (not the raw patch). Optional — if omitted,
1498
1513
  * write-only (service picks up changes on restart).
1499
1514
  */
1500
1515
  onConfigApply: z
1501
- .function()
1502
- .args(z.record(z.unknown()))
1503
- .returns(z.promise(z.void()))
1516
+ .function({
1517
+ input: [z.record(z.string(), z.unknown())],
1518
+ output: z.promise(z.void()),
1519
+ })
1504
1520
  .optional(),
1505
1521
  /**
1506
1522
  * Custom merge function for config apply. Receives the existing config
@@ -1511,15 +1527,22 @@ const jeevesComponentDescriptorSchema = z.object({
1511
1527
  * name-based array merging for inference rules.
1512
1528
  */
1513
1529
  customMerge: z
1514
- .function()
1515
- .args(z.record(z.unknown()), z.record(z.unknown()))
1516
- .returns(z.record(z.unknown()))
1530
+ .function({
1531
+ input: [
1532
+ z.record(z.string(), z.unknown()),
1533
+ z.record(z.string(), z.unknown()),
1534
+ ],
1535
+ output: z.record(z.string(), z.unknown()),
1536
+ })
1517
1537
  .optional(),
1518
1538
  /**
1519
1539
  * Returns command + args for launching the service process.
1520
1540
  * Consumed by `start` CLI command and `service install`.
1521
1541
  */
1522
- startCommand: z.function().args(z.string()).returns(z.array(z.string())),
1542
+ startCommand: z.function({
1543
+ input: [z.string()],
1544
+ output: z.array(z.string()),
1545
+ }),
1523
1546
  /** TOOLS.md section name (e.g., 'Watcher'). */
1524
1547
  sectionId: z.string().min(1, 'sectionId must be a non-empty string'),
1525
1548
  /** Refresh interval in seconds (must be a prime number). */
@@ -1527,25 +1550,23 @@ const jeevesComponentDescriptorSchema = z.object({
1527
1550
  message: 'refreshIntervalSeconds must be a prime number',
1528
1551
  }),
1529
1552
  /** Produce the component's TOOLS.md section content. */
1530
- generateToolsContent: z.function().returns(z.string()),
1553
+ generateToolsContent: z.function({ input: [], output: z.string() }),
1531
1554
  /** Component dependencies for HEARTBEAT alert suppression. */
1532
1555
  dependencies: z
1533
1556
  .object({
1557
+ /** Components that must be healthy for this component to function. */
1534
1558
  hard: z.array(z.string()),
1559
+ /** Components that improve behavior but are not strictly required. */
1535
1560
  soft: z.array(z.string()),
1536
1561
  })
1537
1562
  .optional(),
1538
1563
  /** Extension point: add custom CLI commands to the service CLI. */
1539
1564
  customCliCommands: z
1540
- .function()
1541
- .args(z.custom())
1542
- .returns(z.void())
1565
+ .function({ input: [z.custom()], output: z.void() })
1543
1566
  .optional(),
1544
1567
  /** Extension point: return additional plugin tool descriptors. */
1545
1568
  customPluginTools: z
1546
- .function()
1547
- .args(z.custom())
1548
- .returns(z.array(z.unknown()))
1569
+ .function({ input: [z.custom()], output: z.array(z.unknown()) })
1549
1570
  .optional(),
1550
1571
  });
1551
1572
  /**
@@ -2005,7 +2026,7 @@ function createServiceManager(descriptor) {
2005
2026
  * Standard commands:
2006
2027
  * - `start -c <path>` - Launch the service process (foreground)
2007
2028
  * - `status [-p port]` - Probe service health
2008
- * - `config [jsonpath] [-p port]` - Query running config
2029
+ * - `config [jsonpath] [-p port]` - Query running config (jsonpath optional)
2009
2030
  * - `config validate -c <path>` - Validate a config file
2010
2031
  * - `config apply [-p port] [--file path] [--replace]` - Apply config patch
2011
2032
  * - `init [-o path]` - Generate default config
@@ -2061,10 +2082,7 @@ function createServiceCli(descriptor) {
2061
2082
  // --- config ---
2062
2083
  const configCmd = program
2063
2084
  .command('config')
2064
- .description('Query or manage service configuration');
2065
- configCmd
2066
- .command('query')
2067
- .description('Query running service config via JSONPath')
2085
+ .description('Query or manage service configuration')
2068
2086
  .argument('[jsonpath]', 'JSONPath expression')
2069
2087
  .option('-p, --port <port>', 'Service port', String(descriptor.defaultPort))
2070
2088
  .action(async (jsonpath, opts) => {
@@ -2121,9 +2139,12 @@ function createServiceCli(descriptor) {
2121
2139
  patch = JSON.parse(input);
2122
2140
  }
2123
2141
  }
2124
- const qs = opts.replace ? '?replace=true' : '';
2142
+ const body = { patch };
2143
+ if (opts.replace) {
2144
+ body.replace = true;
2145
+ }
2125
2146
  try {
2126
- const result = await postJson(`${url}/config/apply${qs}`, patch);
2147
+ const result = await postJson(`${url}/config/apply`, body);
2127
2148
  console.log(JSON.stringify(result, null, 2));
2128
2149
  }
2129
2150
  catch (err) {
@@ -2972,7 +2993,7 @@ async function refreshPlatformContent(options) {
2972
2993
  /** Zod schema for a service entry in core config. */
2973
2994
  const serviceEntrySchema = z.object({
2974
2995
  /** Service URL (must be a valid URL). */
2975
- url: z.string().url().describe('Service URL'),
2996
+ url: z.url().describe('Service URL'),
2976
2997
  });
2977
2998
  /** Default bind address for all Jeeves services. */
2978
2999
  const DEFAULT_BIND_ADDRESS = '0.0.0.0';
@@ -3006,7 +3027,7 @@ const coreConfigSchema = z.object({
3006
3027
  .default(3600)
3007
3028
  .describe('Cache TTL in seconds'),
3008
3029
  })
3009
- .default({})
3030
+ .prefault({})
3010
3031
  .describe('Registry cache settings'),
3011
3032
  });
3012
3033
  /**
@@ -3859,7 +3880,9 @@ function createPluginToolset(descriptor) {
3859
3880
  return fail('Missing required parameter: config');
3860
3881
  }
3861
3882
  try {
3862
- const result = await postJson(`${baseUrl}/config/apply`, config);
3883
+ const result = await postJson(`${baseUrl}/config/apply`, {
3884
+ patch: config,
3885
+ });
3863
3886
  return ok(result);
3864
3887
  }
3865
3888
  catch (err) {
package/package.json CHANGED
@@ -13,52 +13,52 @@
13
13
  "url": "https://github.com/karmaniverous/jeeves/issues"
14
14
  },
15
15
  "dependencies": {
16
- "commander": "^14.0.2",
16
+ "commander": "^14.0.3",
17
17
  "jsonpath-plus": "^10.4.0",
18
18
  "package-directory": "^8.2.0",
19
19
  "proper-lockfile": "^4.1.2",
20
- "semver": "^7.7.2",
21
- "zod": "^3.25.67"
20
+ "semver": "^7.7.4",
21
+ "zod": "^4.3.6"
22
22
  },
23
23
  "description": "Shared library and CLI for the Jeeves AI assistant platform.",
24
24
  "devDependencies": {
25
25
  "@commander-js/extra-typings": "^14.0.0",
26
- "@dotenvx/dotenvx": "^1.51.4",
27
- "@eslint/js": "^9.39.2",
26
+ "@dotenvx/dotenvx": "^1.59.1",
27
+ "@eslint/js": "^10.0.1",
28
28
  "@rollup/plugin-alias": "^6.0.0",
29
- "@rollup/plugin-commonjs": "^29.0.0",
29
+ "@rollup/plugin-commonjs": "^29.0.2",
30
30
  "@rollup/plugin-json": "^6.1.0",
31
31
  "@rollup/plugin-node-resolve": "^16.0.3",
32
32
  "@rollup/plugin-replace": "^6.0.3",
33
33
  "@rollup/plugin-typescript": "^12.3.0",
34
34
  "@types/fs-extra": "^11.0.4",
35
- "@types/node": "^25.0.3",
35
+ "@types/node": "^25.5.0",
36
36
  "@types/proper-lockfile": "^4.1.4",
37
- "@types/semver": "^7.7.0",
38
- "@vitest/coverage-v8": "^4.0.16",
39
- "@vitest/eslint-plugin": "^1.6.4",
37
+ "@types/semver": "^7.7.1",
38
+ "@vitest/coverage-v8": "^4.1.2",
39
+ "@vitest/eslint-plugin": "^1.6.13",
40
40
  "auto-changelog": "^2.5.0",
41
41
  "cross-env": "^10.1.0",
42
- "eslint": "^9.39.2",
42
+ "eslint": "^10.1.0",
43
43
  "eslint-config-prettier": "^10.1.8",
44
- "eslint-plugin-prettier": "^5.5.4",
44
+ "eslint-plugin-prettier": "^5.5.5",
45
45
  "eslint-plugin-simple-import-sort": "^12.1.1",
46
- "eslint-plugin-tsdoc": "^0.5.0",
47
- "fs-extra": "^11.3.3",
48
- "knip": "^5.79.0",
49
- "lefthook": "^2.0.13",
50
- "prettier": "^3.7.4",
51
- "release-it": "^19.2.2",
52
- "rimraf": "^6.1.2",
53
- "rollup": "^4.54.0",
54
- "rollup-plugin-dts": "^6.3.0",
46
+ "eslint-plugin-tsdoc": "^0.5.2",
47
+ "fs-extra": "^11.3.4",
48
+ "knip": "^6.1.0",
49
+ "lefthook": "^2.1.4",
50
+ "prettier": "^3.8.1",
51
+ "release-it": "^19.2.4",
52
+ "rimraf": "^6.1.3",
53
+ "rollup": "^4.60.0",
54
+ "rollup-plugin-dts": "^6.4.1",
55
55
  "tslib": "^2.8.1",
56
- "typedoc": "^0.28.15",
57
- "typedoc-plugin-mdn-links": "^5.0.10",
56
+ "typedoc": "^0.28.18",
57
+ "typedoc-plugin-mdn-links": "^5.1.1",
58
58
  "typedoc-plugin-replace-text": "^4.2.0",
59
59
  "typescript": "^5.9.3",
60
- "typescript-eslint": "^8.51.0",
61
- "vitest": "^4.0.16"
60
+ "typescript-eslint": "^8.57.2",
61
+ "vitest": "^4.1.2"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=20"
@@ -139,5 +139,5 @@
139
139
  },
140
140
  "type": "module",
141
141
  "types": "dist/index.d.ts",
142
- "version": "0.4.2"
142
+ "version": "0.4.4"
143
143
  }