@pleri/olam-cli 0.1.186 → 0.1.195

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 (189) hide show
  1. package/README.md +1 -1
  2. package/dist/ask/knowledge-pack-builder.d.ts.map +1 -1
  3. package/dist/ask/knowledge-pack-builder.js +5 -0
  4. package/dist/ask/knowledge-pack-builder.js.map +1 -1
  5. package/dist/ask/knowledge-pack.generated.d.ts.map +1 -1
  6. package/dist/ask/knowledge-pack.generated.js +442 -33
  7. package/dist/ask/knowledge-pack.generated.js.map +1 -1
  8. package/dist/commands/auth-status.js +2 -2
  9. package/dist/commands/auth-status.js.map +1 -1
  10. package/dist/commands/auth.js +1 -1
  11. package/dist/commands/auth.js.map +1 -1
  12. package/dist/commands/bootstrap.d.ts +4 -0
  13. package/dist/commands/bootstrap.d.ts.map +1 -1
  14. package/dist/commands/bootstrap.js +6 -9
  15. package/dist/commands/bootstrap.js.map +1 -1
  16. package/dist/commands/clean.js +1 -1
  17. package/dist/commands/clean.js.map +1 -1
  18. package/dist/commands/completion.d.ts.map +1 -1
  19. package/dist/commands/completion.js +1 -4
  20. package/dist/commands/completion.js.map +1 -1
  21. package/dist/commands/create.d.ts.map +1 -1
  22. package/dist/commands/create.js +10 -0
  23. package/dist/commands/create.js.map +1 -1
  24. package/dist/commands/crystallize.js +12 -14
  25. package/dist/commands/crystallize.js.map +1 -1
  26. package/dist/commands/destroy.d.ts +13 -1
  27. package/dist/commands/destroy.d.ts.map +1 -1
  28. package/dist/commands/destroy.js +52 -6
  29. package/dist/commands/destroy.js.map +1 -1
  30. package/dist/commands/dispatch.d.ts +9 -0
  31. package/dist/commands/dispatch.d.ts.map +1 -1
  32. package/dist/commands/dispatch.js +21 -2
  33. package/dist/commands/dispatch.js.map +1 -1
  34. package/dist/commands/doctor.d.ts +1 -1
  35. package/dist/commands/doctor.d.ts.map +1 -1
  36. package/dist/commands/doctor.js +29 -22
  37. package/dist/commands/doctor.js.map +1 -1
  38. package/dist/commands/enter.d.ts +3 -3
  39. package/dist/commands/enter.d.ts.map +1 -1
  40. package/dist/commands/enter.js +57 -44
  41. package/dist/commands/enter.js.map +1 -1
  42. package/dist/commands/flywheel/index.d.ts.map +1 -1
  43. package/dist/commands/flywheel/index.js +1 -1
  44. package/dist/commands/flywheel/index.js.map +1 -1
  45. package/dist/commands/host-cp.d.ts.map +1 -1
  46. package/dist/commands/host-cp.js +2 -1
  47. package/dist/commands/host-cp.js.map +1 -1
  48. package/dist/commands/implode.d.ts.map +1 -1
  49. package/dist/commands/implode.js +1 -1
  50. package/dist/commands/implode.js.map +1 -1
  51. package/dist/commands/init.d.ts +20 -0
  52. package/dist/commands/init.d.ts.map +1 -1
  53. package/dist/commands/init.js +102 -9
  54. package/dist/commands/init.js.map +1 -1
  55. package/dist/commands/install.js +2 -2
  56. package/dist/commands/install.js.map +1 -1
  57. package/dist/commands/kg-build.d.ts.map +1 -1
  58. package/dist/commands/kg-build.js +3 -0
  59. package/dist/commands/kg-build.js.map +1 -1
  60. package/dist/commands/kg-classify.d.ts +20 -0
  61. package/dist/commands/kg-classify.d.ts.map +1 -1
  62. package/dist/commands/kg-classify.js +59 -42
  63. package/dist/commands/kg-classify.js.map +1 -1
  64. package/dist/commands/kg-mirror.d.ts +40 -0
  65. package/dist/commands/kg-mirror.d.ts.map +1 -0
  66. package/dist/commands/kg-mirror.js +228 -0
  67. package/dist/commands/kg-mirror.js.map +1 -0
  68. package/dist/commands/mcp/index.js +1 -1
  69. package/dist/commands/mcp/index.js.map +1 -1
  70. package/dist/commands/memory/index.d.ts.map +1 -1
  71. package/dist/commands/memory/index.js +1 -1
  72. package/dist/commands/memory/index.js.map +1 -1
  73. package/dist/commands/resume.d.ts.map +1 -1
  74. package/dist/commands/resume.js +1 -1
  75. package/dist/commands/resume.js.map +1 -1
  76. package/dist/commands/services-tls.d.ts +120 -0
  77. package/dist/commands/services-tls.d.ts.map +1 -0
  78. package/dist/commands/services-tls.js +434 -0
  79. package/dist/commands/services-tls.js.map +1 -0
  80. package/dist/commands/services.d.ts.map +1 -1
  81. package/dist/commands/services.js +40 -1
  82. package/dist/commands/services.js.map +1 -1
  83. package/dist/commands/setup-linux-gate.d.ts.map +1 -1
  84. package/dist/commands/setup-linux-gate.js +1 -3
  85. package/dist/commands/setup-linux-gate.js.map +1 -1
  86. package/dist/commands/setup-metrics.d.ts.map +1 -1
  87. package/dist/commands/setup-metrics.js +1 -2
  88. package/dist/commands/setup-metrics.js.map +1 -1
  89. package/dist/commands/setup-phase-5a-skill-source.d.ts +17 -1
  90. package/dist/commands/setup-phase-5a-skill-source.d.ts.map +1 -1
  91. package/dist/commands/setup-phase-5a-skill-source.js +69 -6
  92. package/dist/commands/setup-phase-5a-skill-source.js.map +1 -1
  93. package/dist/commands/setup.d.ts +26 -1
  94. package/dist/commands/setup.d.ts.map +1 -1
  95. package/dist/commands/setup.js +189 -47
  96. package/dist/commands/setup.js.map +1 -1
  97. package/dist/commands/skills-onboard.d.ts.map +1 -1
  98. package/dist/commands/skills-onboard.js +4 -1
  99. package/dist/commands/skills-onboard.js.map +1 -1
  100. package/dist/commands/skills-source.d.ts.map +1 -1
  101. package/dist/commands/skills-source.js +20 -4
  102. package/dist/commands/skills-source.js.map +1 -1
  103. package/dist/commands/status.d.ts.map +1 -1
  104. package/dist/commands/status.js +5 -1
  105. package/dist/commands/status.js.map +1 -1
  106. package/dist/commands/upgrade.d.ts.map +1 -1
  107. package/dist/commands/upgrade.js +1 -3
  108. package/dist/commands/upgrade.js.map +1 -1
  109. package/dist/commands/yolo.d.ts.map +1 -1
  110. package/dist/commands/yolo.js +1 -1
  111. package/dist/commands/yolo.js.map +1 -1
  112. package/dist/context.d.ts +4 -0
  113. package/dist/context.d.ts.map +1 -1
  114. package/dist/context.js +3 -2
  115. package/dist/context.js.map +1 -1
  116. package/dist/image-digests.json +8 -8
  117. package/dist/index.js +4409 -2375
  118. package/dist/index.js.map +1 -1
  119. package/dist/lib/auth-refresh-kubernetes.d.ts.map +1 -1
  120. package/dist/lib/auth-refresh-kubernetes.js +14 -5
  121. package/dist/lib/auth-refresh-kubernetes.js.map +1 -1
  122. package/dist/lib/bootstrap-kubernetes.d.ts +41 -0
  123. package/dist/lib/bootstrap-kubernetes.d.ts.map +1 -1
  124. package/dist/lib/bootstrap-kubernetes.js +289 -36
  125. package/dist/lib/bootstrap-kubernetes.js.map +1 -1
  126. package/dist/lib/cf-access-token.d.ts.map +1 -1
  127. package/dist/lib/cf-access-token.js +2 -3
  128. package/dist/lib/cf-access-token.js.map +1 -1
  129. package/dist/lib/health-probes.d.ts +14 -0
  130. package/dist/lib/health-probes.d.ts.map +1 -1
  131. package/dist/lib/health-probes.js +41 -3
  132. package/dist/lib/health-probes.js.map +1 -1
  133. package/dist/lib/help-groups.d.ts +36 -0
  134. package/dist/lib/help-groups.d.ts.map +1 -0
  135. package/dist/lib/help-groups.js +124 -0
  136. package/dist/lib/help-groups.js.map +1 -0
  137. package/dist/lib/k8s-bootstrap.d.ts +6 -0
  138. package/dist/lib/k8s-bootstrap.d.ts.map +1 -1
  139. package/dist/lib/k8s-bootstrap.js +15 -2
  140. package/dist/lib/k8s-bootstrap.js.map +1 -1
  141. package/dist/lib/k8s-secret-render.d.ts.map +1 -1
  142. package/dist/lib/k8s-secret-render.js +17 -10
  143. package/dist/lib/k8s-secret-render.js.map +1 -1
  144. package/dist/lib/memory-secret.d.ts +15 -2
  145. package/dist/lib/memory-secret.d.ts.map +1 -1
  146. package/dist/lib/memory-secret.js +25 -8
  147. package/dist/lib/memory-secret.js.map +1 -1
  148. package/dist/lib/upgrade-check.d.ts +60 -0
  149. package/dist/lib/upgrade-check.d.ts.map +1 -0
  150. package/dist/lib/upgrade-check.js +169 -0
  151. package/dist/lib/upgrade-check.js.map +1 -0
  152. package/dist/lib/upgrade-kubernetes.d.ts +17 -0
  153. package/dist/lib/upgrade-kubernetes.d.ts.map +1 -1
  154. package/dist/lib/upgrade-kubernetes.js +125 -1
  155. package/dist/lib/upgrade-kubernetes.js.map +1 -1
  156. package/dist/mcp-server.js +2687 -2818
  157. package/hermes-bundle/version.json +1 -1
  158. package/host-cp/k8s/manifests/30-configmap.yaml +8 -1
  159. package/host-cp/k8s/manifests/50-deployment.yaml +1 -1
  160. package/host-cp/k8s/manifests/60-service.yaml +12 -4
  161. package/host-cp/k8s/manifests/70-ingressroute.yaml +58 -0
  162. package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +1 -1
  163. package/host-cp/k8s/manifests/chunks-electric/10-serviceaccount.yaml +8 -0
  164. package/host-cp/k8s/manifests/chunks-electric/20-rbac.yaml +27 -0
  165. package/host-cp/k8s/manifests/chunks-electric/30-configmap.yaml +23 -0
  166. package/host-cp/k8s/manifests/chunks-electric/45-pvc.yaml +19 -0
  167. package/host-cp/k8s/manifests/chunks-electric/50-deployment.yaml +84 -0
  168. package/host-cp/k8s/manifests/chunks-electric/60-service.yaml +17 -0
  169. package/host-cp/k8s/manifests/chunks-postgres/10-serviceaccount.yaml +8 -0
  170. package/host-cp/k8s/manifests/chunks-postgres/20-rbac.yaml +29 -0
  171. package/host-cp/k8s/manifests/chunks-postgres/30-configmap.yaml +185 -0
  172. package/host-cp/k8s/manifests/chunks-postgres/45-pvc.yaml +24 -0
  173. package/host-cp/k8s/manifests/chunks-postgres/50-deployment.yaml +101 -0
  174. package/host-cp/k8s/manifests/chunks-postgres/60-service.yaml +24 -0
  175. package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +1 -1
  176. package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +1 -1
  177. package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +1 -1
  178. package/host-cp/k8s/manifests/plan-chat-service/10-serviceaccount.yaml +8 -0
  179. package/host-cp/k8s/manifests/plan-chat-service/20-rbac.yaml +29 -0
  180. package/host-cp/k8s/manifests/plan-chat-service/30-configmap.yaml +36 -0
  181. package/host-cp/k8s/manifests/plan-chat-service/45-pvc.yaml +24 -0
  182. package/host-cp/k8s/manifests/plan-chat-service/50-deployment.yaml +135 -0
  183. package/host-cp/k8s/manifests/plan-chat-service/60-service.yaml +17 -0
  184. package/host-cp/src/plan-chat-secret.mjs +16 -1
  185. package/host-cp/src/plan-chat-service.mjs +709 -11
  186. package/host-cp/src/planning-sessions.mjs +252 -0
  187. package/host-cp/src/pr-cache.mjs +11 -2
  188. package/host-cp/src/server.mjs +128 -22
  189. package/package.json +2 -1
@@ -175,6 +175,216 @@ export async function setCrystallizeStatus({ pool, sessionId, status, worldId =
175
175
  );
176
176
  }
177
177
 
178
+ /**
179
+ * Create a new multi-turn DISPATCH session (multi-turn-cloud-sandbox-dispatch
180
+ * Phase A2 — distinct from createPlanningSession which is for planning-flow
181
+ * crystallization sessions under world_id='_planning').
182
+ *
183
+ * Allocates a UUID session_id, INSERTs a planning_sessions row with
184
+ * session_type='dispatch' + caller-supplied world_id, applies operator-supplied
185
+ * budget_usd_cap / allow_unpriced_models defaults, returns the session_id.
186
+ *
187
+ * No seed chunk: dispatch sessions accumulate chunks from the agent runtime
188
+ * (via /v1/chunks); we don't pre-seed a system chunk because Electric shape
189
+ * subscribers for dispatch sessions can wait for the first real agent chunk.
190
+ *
191
+ * @param {object} opts
192
+ * @param {object} opts.pool
193
+ * @param {string} opts.actorId
194
+ * @param {string} opts.worldId — operator-supplied; identifies the dispatch
195
+ * target world (NOT the '_planning' sentinel used by createPlanningSession).
196
+ * @param {number | null} [opts.budgetUsdCap=null] — per-session budget cap;
197
+ * null = uncapped. When null AND `OLAM_SESSION_BUDGET_DEFAULT_USD` is set,
198
+ * the env-default applies at /v1/dispatch-turn check time (Phase D); here
199
+ * we record the row exactly as supplied.
200
+ * @param {boolean} [opts.allowUnpricedModels=false] — opt session into the
201
+ * pricingForModel-returns-null fallback (Plan A T11 mitigation; default
202
+ * refuses unknown models with 502).
203
+ * @returns {Promise<{ session_id: string }>}
204
+ */
205
+ export async function createDispatchSession({
206
+ pool,
207
+ actorId,
208
+ worldId,
209
+ budgetUsdCap = null,
210
+ allowUnpricedModels = false,
211
+ sessionId: providedSessionId = null,
212
+ }) {
213
+ if (!actorId || typeof actorId !== 'string') {
214
+ throw new Error('createDispatchSession: actorId required');
215
+ }
216
+ if (!worldId || typeof worldId !== 'string') {
217
+ throw new Error('createDispatchSession: worldId required');
218
+ }
219
+ // A6 (Decision 9 always-on threading): callers MAY supply session_id to
220
+ // upsert an existing planning_sessions row (e.g. /api/cloud-dispatch
221
+ // pre-creating the thread before forwarding to plan-DO). When omitted,
222
+ // we generate a UUID. ON CONFLICT DO NOTHING handles the race where
223
+ // the SPA called /v1/sessions/create concurrently AND server-side
224
+ // cloud-dispatch tried to pre-create the same row.
225
+ const sessionId = providedSessionId ?? randomUUID();
226
+ await pool.query(
227
+ `INSERT INTO planning_sessions
228
+ (session_id, actor_id, session_type, world_id, budget_usd_cap, allow_unpriced_models)
229
+ VALUES ($1, $2, 'dispatch', $3, $4, $5)
230
+ ON CONFLICT (session_id) DO NOTHING`,
231
+ [sessionId, actorId, worldId, budgetUsdCap, allowUnpricedModels],
232
+ );
233
+ return { session_id: sessionId };
234
+ }
235
+
236
+ /**
237
+ * Atomic test-and-set lock claim on a dispatch session
238
+ * (multi-turn-cloud-sandbox-dispatch Phase A3 — Decision 4 + T5 mitigation).
239
+ *
240
+ * Pattern: single-statement UPDATE ... WHERE in_flight_turn_id IS NULL RETURNING.
241
+ * Two concurrent attempts: first claim wins (RETURNING yields 1 row); second
242
+ * sees empty result + must return 409 to caller. Matches the established
243
+ * planning-sessions.mjs:169 setCrystallizeStatus atomic-write idiom.
244
+ *
245
+ * @param {object} opts
246
+ * @param {object} opts.pool
247
+ * @param {string} opts.sessionId
248
+ * @param {string} opts.turnId — operator-or-server-generated turn UUID
249
+ * @returns {Promise<boolean>} true if lock claimed, false if already held
250
+ */
251
+ export async function claimDispatchTurnLock({ pool, sessionId, turnId }) {
252
+ const result = await pool.query(
253
+ `UPDATE planning_sessions
254
+ SET in_flight_turn_id = $1,
255
+ in_flight_turn_started_at = NOW(),
256
+ last_turn_at = NOW()
257
+ WHERE session_id = $2
258
+ AND session_type = 'dispatch'
259
+ AND in_flight_turn_id IS NULL
260
+ RETURNING session_id`,
261
+ [turnId, sessionId],
262
+ );
263
+ return (result.rows?.length ?? 0) > 0;
264
+ }
265
+
266
+ /**
267
+ * Clear the in-flight turn lock after dispatch completes (success OR failure).
268
+ *
269
+ * @param {object} opts
270
+ * @param {object} opts.pool
271
+ * @param {string} opts.sessionId
272
+ */
273
+ export async function clearDispatchTurnLock({ pool, sessionId }) {
274
+ await pool.query(
275
+ `UPDATE planning_sessions
276
+ SET in_flight_turn_id = NULL,
277
+ in_flight_turn_started_at = NULL
278
+ WHERE session_id = $1
279
+ AND session_type = 'dispatch'`,
280
+ [sessionId],
281
+ );
282
+ }
283
+
284
+ /**
285
+ * Halt a dispatch session — operator-driven "block next turn" state (T13).
286
+ *
287
+ * Sets halted_at to NOW() AND clears in_flight_turn_id. Future /v1/dispatch-turn
288
+ * calls return 409 'session_halted' until reactivateDispatchSession clears
289
+ * halted_at. Does NOT stop an in-flight container — the running container
290
+ * completes its current turn naturally. UX is "Block next turn" not "Stop"
291
+ * (Plan A Phase C C6).
292
+ *
293
+ * Scoped by actor_id for ownership isolation.
294
+ *
295
+ * @param {object} opts
296
+ * @param {object} opts.pool
297
+ * @param {string} opts.sessionId
298
+ * @param {string} opts.actorId
299
+ * @returns {Promise<boolean>} true if a session row was updated; false if
300
+ * the session_id was not found / not owned by actorId.
301
+ */
302
+ export async function haltDispatchSession({ pool, sessionId, actorId }) {
303
+ const result = await pool.query(
304
+ `UPDATE planning_sessions
305
+ SET halted_at = NOW(),
306
+ in_flight_turn_id = NULL,
307
+ in_flight_turn_started_at = NULL
308
+ WHERE session_id = $1
309
+ AND session_type = 'dispatch'
310
+ AND actor_id = $2
311
+ RETURNING session_id`,
312
+ [sessionId, actorId],
313
+ );
314
+ return (result.rows?.length ?? 0) > 0;
315
+ }
316
+
317
+ /**
318
+ * Reactivate a halted dispatch session — clears halted_at so subsequent
319
+ * /v1/dispatch-turn calls can claim the lock again. Idempotent (clearing an
320
+ * already-null halted_at is a no-op).
321
+ *
322
+ * @param {object} opts
323
+ * @param {object} opts.pool
324
+ * @param {string} opts.sessionId
325
+ * @param {string} opts.actorId
326
+ * @returns {Promise<boolean>} true if a session row was updated; false if
327
+ * the session_id was not found / not owned by actorId.
328
+ */
329
+ export async function reactivateDispatchSession({ pool, sessionId, actorId }) {
330
+ const result = await pool.query(
331
+ `UPDATE planning_sessions
332
+ SET halted_at = NULL
333
+ WHERE session_id = $1
334
+ AND session_type = 'dispatch'
335
+ AND actor_id = $2
336
+ RETURNING session_id`,
337
+ [sessionId, actorId],
338
+ );
339
+ return (result.rows?.length ?? 0) > 0;
340
+ }
341
+
342
+ /**
343
+ * Read a dispatch session by session_id + scope to caller's actor_id
344
+ * (ownership check). Returns the session metadata needed for budget check
345
+ * + plan-DO forward, OR null when not found / not owned.
346
+ *
347
+ * @param {object} opts
348
+ * @param {object} opts.pool
349
+ * @param {string} opts.sessionId
350
+ * @param {string} opts.actorId
351
+ * @returns {Promise<null | {
352
+ * session_id: string,
353
+ * world_id: string | null,
354
+ * actor_id: string,
355
+ * total_usd: number,
356
+ * budget_usd_cap: number | null,
357
+ * allow_unpriced_models: boolean,
358
+ * halted_at: string | null,
359
+ * }>}
360
+ */
361
+ export async function getDispatchSession({ pool, sessionId, actorId }) {
362
+ const result = await pool.query(
363
+ `SELECT session_id, world_id, actor_id,
364
+ total_usd, budget_usd_cap, allow_unpriced_models,
365
+ halted_at
366
+ FROM planning_sessions
367
+ WHERE session_id = $1
368
+ AND session_type = 'dispatch'
369
+ AND actor_id = $2`,
370
+ [sessionId, actorId],
371
+ );
372
+ const row = result.rows?.[0];
373
+ if (!row) return null;
374
+ return {
375
+ session_id: row.session_id,
376
+ world_id: row.world_id ?? null,
377
+ actor_id: row.actor_id,
378
+ total_usd: Number(row.total_usd ?? 0),
379
+ budget_usd_cap:
380
+ row.budget_usd_cap === null || row.budget_usd_cap === undefined
381
+ ? null
382
+ : Number(row.budget_usd_cap),
383
+ allow_unpriced_models: Boolean(row.allow_unpriced_models),
384
+ halted_at: row.halted_at ?? null,
385
+ };
386
+ }
387
+
178
388
  /**
179
389
  * List planning sessions for a given actorId, ordered by created_at DESC.
180
390
  *
@@ -204,6 +414,48 @@ export async function listPlanningSessions({ pool, actorId, limit = 50 }) {
204
414
  return result.rows;
205
415
  }
206
416
 
417
+ /**
418
+ * List multi-turn DISPATCH sessions for a given actorId, ordered by
419
+ * last_turn_at DESC (most recently active first), excluding archived sessions.
420
+ *
421
+ * Distinct from listPlanningSessions: this returns only `session_type='dispatch'`
422
+ * rows + projects the multi-turn-specific columns (total_usd, in_flight_turn_id,
423
+ * halted_at, etc.) that the SPA's SessionsListView (Phase C C3) renders.
424
+ *
425
+ * @param {object} opts
426
+ * @param {object} opts.pool
427
+ * @param {string} opts.actorId
428
+ * @param {number} [opts.limit=50]
429
+ * @returns {Promise<Array<{
430
+ * session_id: string,
431
+ * world_id: string | null,
432
+ * total_usd: string,
433
+ * budget_usd_cap: string | null,
434
+ * in_flight_turn_id: string | null,
435
+ * halted_at: string | null,
436
+ * last_turn_at: string | null,
437
+ * created_at: string,
438
+ * summary: string | null,
439
+ * }>>}
440
+ */
441
+ export async function listDispatchSessions({ pool, actorId, limit = 50 }) {
442
+ const result = await pool.query(
443
+ `SELECT session_id, world_id,
444
+ total_usd, budget_usd_cap,
445
+ in_flight_turn_id, halted_at,
446
+ last_turn_at, created_at,
447
+ summary
448
+ FROM planning_sessions
449
+ WHERE actor_id = $1
450
+ AND session_type = 'dispatch'
451
+ AND archived_at IS NULL
452
+ ORDER BY last_turn_at DESC NULLS LAST, created_at DESC
453
+ LIMIT $2`,
454
+ [actorId, limit],
455
+ );
456
+ return result.rows;
457
+ }
458
+
207
459
  /**
208
460
  * Load lightweight metadata for an existing in-flight planning session.
209
461
  *
@@ -127,7 +127,7 @@ async function fetchPrData(prUrl, getToken) {
127
127
  /**
128
128
  * Create a PR data cache with TTL and concurrent-fetch coalescing.
129
129
  *
130
- * @returns {{ getPr: (prUrl: string, getToken: () => Promise<string|null>) => Promise<PrData|null> }}
130
+ * @returns {{ getPr: (prUrl: string, getToken: () => Promise<string|null>) => Promise<PrData|null>, deletePr: (prUrl: string) => void }}
131
131
  */
132
132
  export function createPrCache() {
133
133
  /** @type {Map<string, PrCacheEntry>} */
@@ -197,5 +197,14 @@ export function createPrCache() {
197
197
  }
198
198
  }
199
199
 
200
- return { getPr };
200
+ /**
201
+ * Evict a PR entry from the cache (call on world destroy).
202
+ *
203
+ * @param {string} prUrl
204
+ */
205
+ function deletePr(prUrl) {
206
+ cache.delete(prUrl);
207
+ }
208
+
209
+ return { getPr, deletePr };
201
210
  }
@@ -354,6 +354,50 @@ function readDogfoodRepoUrl() {
354
354
  return '';
355
355
  }
356
356
 
357
+ /**
358
+ * Resolve the cloud-kg-mirror classifier proxy URL the runner forwards
359
+ * into spawned CF Sandbox child worlds. When set, host-cp enriches
360
+ * cloud-dispatch bodies with `kgProxyUrl` + `kgProxyBearer`; the runner
361
+ * injects those into the sandbox env so the PreToolUse hook's
362
+ * cloud-sandbox flavor can POST /v1/classify against the
363
+ * kg-mirror-worker. Source order: OLAM_KG_PROXY_URL env, then
364
+ * ~/.olam/kg-proxy-url file. Absent → no injection (dispatched worlds
365
+ * see no proxy URL → hook falls through to grep via existing fail-open).
366
+ * Mirrors readAnthropicBaseUrl() — operators have ONE pattern.
367
+ *
368
+ * Phase 1 (cloud-kg-mirror): docs/plans/cloud-kg-mirror/README.md §6-7.
369
+ */
370
+ function readKgProxyUrl() {
371
+ const fromOlamEnv = process.env['OLAM_KG_PROXY_URL'];
372
+ if (fromOlamEnv && fromOlamEnv.length > 0) return fromOlamEnv.trim();
373
+ try {
374
+ const file = path.join(os.homedir(), '.olam', 'kg-proxy-url');
375
+ const content = fs.readFileSync(file, 'utf-8').trim();
376
+ if (content.length > 0) return content;
377
+ } catch {
378
+ // file absent — fall through
379
+ }
380
+ return '';
381
+ }
382
+
383
+ /**
384
+ * Bearer that pairs with readKgProxyUrl(). Stored in
385
+ * ~/.olam/kg-proxy-bearer (chmod 600 by operator). Mirrors the
386
+ * source-order convention; absent → no injection.
387
+ */
388
+ function readKgProxyBearer() {
389
+ const fromOlamEnv = process.env['OLAM_KG_PROXY_BEARER'];
390
+ if (fromOlamEnv && fromOlamEnv.length > 0) return fromOlamEnv.trim();
391
+ try {
392
+ const file = path.join(os.homedir(), '.olam', 'kg-proxy-bearer');
393
+ const content = fs.readFileSync(file, 'utf-8').trim();
394
+ if (content.length > 0) return content;
395
+ } catch {
396
+ // file absent — fall through
397
+ }
398
+ return '';
399
+ }
400
+
357
401
  /** @type {Record<string, number>} */
358
402
  let WORLDS = {};
359
403
 
@@ -510,6 +554,9 @@ const prPoller = createPrMergePoller({
510
554
  WORLDS = next;
511
555
  persistRegistry();
512
556
  }
557
+ const prState = prStateStore.get(worldId);
558
+ if (prState?.pr_url) prCache.deletePr(prState.pr_url);
559
+ progressCache.delete(worldId);
513
560
  },
514
561
  pollIntervalMs: PR_POLL_INTERVAL_MS,
515
562
  gracePeriodMs: MERGE_GRACE_MS,
@@ -1366,6 +1413,9 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
1366
1413
  WORLDS = next;
1367
1414
  persistRegistry();
1368
1415
  }
1416
+ const prStateForDelete = prStateStore.get(id);
1417
+ if (prStateForDelete?.pr_url) prCache.deletePr(prStateForDelete.pr_url);
1418
+ progressCache.delete(id);
1369
1419
  return jsonReply(res, 200, { worlds: WORLDS });
1370
1420
  }
1371
1421
 
@@ -1526,7 +1576,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
1526
1576
  if (url.pathname === '/api/repos' && req.method === 'GET') {
1527
1577
  const config = loadGlobalConfig();
1528
1578
  if ('error' in config) {
1529
- return jsonReply(res, 500, { error: config.error });
1579
+ return jsonReply(res, 500, { error: config.error, message: config.error });
1530
1580
  }
1531
1581
  return jsonReply(res, 200, { repos: config.repos });
1532
1582
  }
@@ -1536,7 +1586,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
1536
1586
  if (url.pathname === '/api/runbooks' && req.method === 'GET') {
1537
1587
  const config = loadGlobalConfig();
1538
1588
  if ('error' in config) {
1539
- return jsonReply(res, 500, { error: config.error });
1589
+ return jsonReply(res, 500, { error: config.error, message: config.error });
1540
1590
  }
1541
1591
  return jsonReply(res, 200, { runbooks: config.runbooks });
1542
1592
  }
@@ -1679,7 +1729,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
1679
1729
  const body = await readRequestBody(req);
1680
1730
  const provider = body && typeof body === 'object' && body.provider ? String(body.provider) : 'claude';
1681
1731
  const label = body && typeof body === 'object' && body.label ? String(body.label) : '';
1682
- if (!label) return jsonReply(res, 400, { error: 'label_required' });
1732
+ if (!label) return jsonReply(res, 400, { error: 'label_required', message: 'label field is required' });
1683
1733
  const upstream = await authServiceFetch('POST', '/credentials/add', { provider, label });
1684
1734
  const data = await upstream.json();
1685
1735
  return jsonReply(res, upstream.status, data);
@@ -1996,9 +2046,9 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
1996
2046
  req.on('data', (c) => { body += c; });
1997
2047
  req.on('end', () => {
1998
2048
  let parsed;
1999
- try { parsed = JSON.parse(body || '{}'); } catch { return jsonReply(res, 400, { error: 'invalid_json' }); }
2049
+ try { parsed = JSON.parse(body || '{}'); } catch (e) { return jsonReply(res, 400, { error: 'invalid_json', message: e.message }); }
2000
2050
  const { worldId, prUrl, prNumber, prRepo } = parsed ?? {};
2001
- if (!worldId || !prUrl) return jsonReply(res, 400, { error: 'worldId and prUrl required' });
2051
+ if (!worldId || !prUrl) return jsonReply(res, 400, { error: 'missing_fields', message: 'worldId and prUrl required' });
2002
2052
  prStateStore.set(worldId, {
2003
2053
  pr_url: prUrl,
2004
2054
  pr_number: prNumber ?? null,
@@ -2023,9 +2073,9 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2023
2073
  req.on('data', (c) => { body += c; });
2024
2074
  req.on('end', () => {
2025
2075
  let parsed;
2026
- try { parsed = JSON.parse(body || '{}'); } catch { return jsonReply(res, 400, { error: 'invalid_json' }); }
2076
+ try { parsed = JSON.parse(body || '{}'); } catch (e) { return jsonReply(res, 400, { error: 'invalid_json', message: e.message }); }
2027
2077
  const existing = prStateStore.get(worldId);
2028
- if (!existing) return jsonReply(res, 404, { error: 'world not in PR state store' });
2078
+ if (!existing) return jsonReply(res, 404, { error: 'not_found', message: 'world not in PR state store' });
2029
2079
  const updates = {};
2030
2080
  if (typeof parsed.autoDestroyOnMerge === 'boolean') updates.auto_destroy_on_merge = parsed.autoDestroyOnMerge;
2031
2081
  prStateStore.set(worldId, updates);
@@ -2108,7 +2158,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2108
2158
  const id = decodeURIComponent(planConvMatch[1]);
2109
2159
  try {
2110
2160
  const conv = planOrchestrator.getConversation(id);
2111
- if (!conv) return jsonReply(res, 404, { error: 'not_found', id });
2161
+ if (!conv) return jsonReply(res, 404, { error: 'not_found', id, message: 'conversation not found' });
2112
2162
  // Return persisted turns alongside conversation metadata.
2113
2163
  const turns = planOrchestrator.getTurns(id);
2114
2164
  return jsonReply(res, 200, { ...conv, turns });
@@ -2128,7 +2178,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2128
2178
  const content = (body && typeof body === 'object' && typeof body.content === 'string')
2129
2179
  ? body.content.trim()
2130
2180
  : '';
2131
- if (!content) return jsonReply(res, 400, { error: 'content_required' });
2181
+ if (!content) return jsonReply(res, 400, { error: 'content_required', message: 'turn content must be a non-empty string' });
2132
2182
  const personaOverride = (body && typeof body === 'object' && typeof body.personaOverride === 'string')
2133
2183
  ? body.personaOverride
2134
2184
  : undefined;
@@ -2139,7 +2189,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2139
2189
  const result = await planOrchestrator.submitTurn({ conversationId, content, personaOverride, mentionedPersonas });
2140
2190
  return jsonReply(res, 202, result);
2141
2191
  } catch (err) {
2142
- if (err.code === 'NOT_FOUND') return jsonReply(res, 404, { error: 'not_found', id: conversationId });
2192
+ if (err.code === 'NOT_FOUND') return jsonReply(res, 404, { error: 'not_found', id: conversationId, message: 'conversation not found' });
2143
2193
  return jsonReply(res, 500, { error: 'submit_failed', message: err.message });
2144
2194
  }
2145
2195
  }
@@ -2154,7 +2204,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2154
2204
  }
2155
2205
  const toPersona = body?.toPersona;
2156
2206
  if (typeof toPersona !== 'string' || !toPersona) {
2157
- return jsonReply(res, 400, { error: 'toPersona_required' });
2207
+ return jsonReply(res, 400, { error: 'toPersona_required', message: 'toPersona field is required' });
2158
2208
  }
2159
2209
  const mode = ['full', 'distilled', 'quoted'].includes(body?.mode) ? body.mode : 'full';
2160
2210
  const selectedTurnIds = Array.isArray(body?.selectedTurnIds) ? body.selectedTurnIds : [];
@@ -2164,7 +2214,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2164
2214
  });
2165
2215
  return jsonReply(res, 200, result);
2166
2216
  } catch (err) {
2167
- if (err.code === 'NOT_FOUND') return jsonReply(res, 404, { error: 'not_found', id: conversationId });
2217
+ if (err.code === 'NOT_FOUND') return jsonReply(res, 404, { error: 'not_found', id: conversationId, message: 'conversation not found' });
2168
2218
  return jsonReply(res, 500, { error: 'handoff_failed', message: err.message });
2169
2219
  }
2170
2220
  }
@@ -2174,7 +2224,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2174
2224
  if (!await requirePlanCredential(res)) return;
2175
2225
  const conversationId = decodeURIComponent(planStreamMatch[1]);
2176
2226
  const conv = planOrchestrator.getConversation(conversationId);
2177
- if (!conv) return jsonReply(res, 404, { error: 'not_found', id: conversationId });
2227
+ if (!conv) return jsonReply(res, 404, { error: 'not_found', id: conversationId, message: 'conversation not found' });
2178
2228
 
2179
2229
  res.writeHead(200, {
2180
2230
  'Content-Type': 'text/event-stream; charset=utf-8',
@@ -2214,7 +2264,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2214
2264
  const conversationId = decodeURIComponent(planAgentInviteMatch[1]);
2215
2265
  const personaId = decodeURIComponent(planAgentInviteMatch[2]);
2216
2266
  const conv = planOrchestrator.getConversation(conversationId);
2217
- if (!conv) return jsonReply(res, 404, { error: 'not_found', id: conversationId });
2267
+ if (!conv) return jsonReply(res, 404, { error: 'not_found', id: conversationId, message: 'conversation not found' });
2218
2268
  const agent = planOrchestrator.inviteLookout(conversationId, personaId);
2219
2269
  return jsonReply(res, 200, agent);
2220
2270
  }
@@ -2232,7 +2282,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2232
2282
  if (typeof body?.muted === 'boolean') updates.muted = body.muted;
2233
2283
  if (typeof body?.mode === 'string') updates.mode = body.mode;
2234
2284
  const agent = planOrchestrator.updateLookout(conversationId, personaId, updates);
2235
- if (!agent) return jsonReply(res, 404, { error: 'not_found' });
2285
+ if (!agent) return jsonReply(res, 404, { error: 'not_found', message: 'lookout agent not found' });
2236
2286
  return jsonReply(res, 200, agent);
2237
2287
  }
2238
2288
 
@@ -2253,7 +2303,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2253
2303
  const changed = action === 'dismiss'
2254
2304
  ? planOrchestrator.dismissSignal(conversationId, signalId)
2255
2305
  : planOrchestrator.useSignal(conversationId, signalId);
2256
- if (!changed) return jsonReply(res, 404, { error: 'not_found' });
2306
+ if (!changed) return jsonReply(res, 404, { error: 'not_found', message: 'signal not found' });
2257
2307
  return jsonReply(res, 200, { ok: true });
2258
2308
  }
2259
2309
 
@@ -2578,6 +2628,35 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2578
2628
  // Keep `messages` too — plan-DO may use it for multi-turn fidelity later.
2579
2629
  }
2580
2630
 
2631
+ // multi-turn-cloud-sandbox-dispatch Phase A6 (Decision 9 always-on
2632
+ // threading): pre-create the planning_sessions row server-side so
2633
+ // EVERY cloud dispatch surfaces as a SPA thread — including pre-PR
2634
+ // #1182-style one-shot calls that never invoked /v1/sessions/create
2635
+ // directly. Idempotent (ON CONFLICT DO NOTHING in createDispatchSession).
2636
+ // Fail-soft: thread-creation failure does NOT block the dispatch
2637
+ // (the operator's task still ships); we log + continue.
2638
+ try {
2639
+ if (parsed.world_id && parsed.session_id) {
2640
+ const planChatBearer = readPlanChatSecret();
2641
+ const planChatBase = process.env.PLAN_CHAT_SERVICE_URL || 'http://127.0.0.1:3200';
2642
+ await fetch(`${planChatBase}/v1/sessions/create`, {
2643
+ method: 'POST',
2644
+ headers: {
2645
+ 'content-type': 'application/json',
2646
+ authorization: `Bearer ${planChatBearer}`,
2647
+ },
2648
+ body: JSON.stringify({
2649
+ world_id: parsed.world_id,
2650
+ session_id: parsed.session_id,
2651
+ }),
2652
+ });
2653
+ }
2654
+ } catch (threadErr) {
2655
+ console.warn(
2656
+ `[cloud-dispatch] thread pre-create failed (continuing): ${threadErr?.message ?? threadErr}`,
2657
+ );
2658
+ }
2659
+
2581
2660
  // Gap 3: enrich the dispatch body with the operator's anthropicBaseUrl
2582
2661
  // so plan-DO can propagate it to spawned CF Sandbox child worlds.
2583
2662
  // Only injected when not already set by the SPA (SPA has no auth-worker
@@ -2590,9 +2669,18 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2590
2669
  // ~/.olam/dogfood-repo-url file. Absent → no injection (the dispatch
2591
2670
  // runs text-only, like before this change).
2592
2671
  const dogfoodRepoUrl = readDogfoodRepoUrl();
2672
+ // cloud-kg-mirror Phase 1: propagate the classifier Worker URL +
2673
+ // bearer so the runner can inject them into the sandbox env. When
2674
+ // either is missing the dispatched world's PreToolUse hook falls
2675
+ // through to grep via the existing fail-open path. Operator opts
2676
+ // in by writing ~/.olam/kg-proxy-url + ~/.olam/kg-proxy-bearer.
2677
+ const kgProxyUrl = readKgProxyUrl();
2678
+ const kgProxyBearer = readKgProxyBearer();
2593
2679
  let enrichedObj = null;
2594
2680
  if (anthropicBaseUrl && !parsed.anthropicBaseUrl) enrichedObj = { ...(enrichedObj ?? parsed), anthropicBaseUrl };
2595
2681
  if (dogfoodRepoUrl && !parsed.repoUrl) enrichedObj = { ...(enrichedObj ?? parsed), repoUrl: dogfoodRepoUrl };
2682
+ if (kgProxyUrl && !parsed.kgProxyUrl) enrichedObj = { ...(enrichedObj ?? parsed), kgProxyUrl };
2683
+ if (kgProxyBearer && !parsed.kgProxyBearer) enrichedObj = { ...(enrichedObj ?? parsed), kgProxyBearer };
2596
2684
  // Use `parsed` (not raw `body`) as the no-enrichment fallback so that the
2597
2685
  // messages→prompt normalisation above is always forwarded even when no
2598
2686
  // env-var enrichments are applied.
@@ -2661,10 +2749,18 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2661
2749
  // bearer registration without additional plan metadata.
2662
2750
  }
2663
2751
 
2664
- // Enrich with anthropicBaseUrl from the host config.
2752
+ // Enrich with anthropicBaseUrl + kgProxy from the host config.
2665
2753
  const anthropicBaseUrl = readAnthropicBaseUrl();
2754
+ const kgProxyUrl = readKgProxyUrl();
2755
+ const kgProxyBearer = readKgProxyBearer();
2666
2756
  const planId = parsed.planId ?? parsed.session_id ?? `plan-${Date.now()}`;
2667
- const requestBody = { ...parsed, planId, ...(anthropicBaseUrl ? { anthropicBaseUrl } : {}) };
2757
+ const requestBody = {
2758
+ ...parsed,
2759
+ planId,
2760
+ ...(anthropicBaseUrl ? { anthropicBaseUrl } : {}),
2761
+ ...(kgProxyUrl ? { kgProxyUrl } : {}),
2762
+ ...(kgProxyBearer ? { kgProxyBearer } : {}),
2763
+ };
2668
2764
 
2669
2765
  const basicAuth = Buffer.from(`operator:${showcasePw}`).toString('base64');
2670
2766
  // Phase H h2: attach CF Access service-token headers when configured.
@@ -2717,7 +2813,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2717
2813
  if (serversMatch && req.method === 'GET') {
2718
2814
  const worldId = decodeURIComponent(serversMatch[1]);
2719
2815
  if (!(worldId in WORLDS)) {
2720
- return jsonReply(res, 404, { error: 'unknown_world', worldId });
2816
+ return jsonReply(res, 404, { error: 'unknown_world', worldId, message: 'world not in registry' });
2721
2817
  }
2722
2818
  return handleListServers(req, res, worldId);
2723
2819
  }
@@ -2727,7 +2823,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2727
2823
  const worldId = decodeURIComponent(bridgesMatch[1]);
2728
2824
  const portSegment = bridgesMatch[3] ? parseInt(bridgesMatch[3], 10) : null;
2729
2825
  if (!(worldId in WORLDS)) {
2730
- return jsonReply(res, 404, { error: 'unknown_world', worldId });
2826
+ return jsonReply(res, 404, { error: 'unknown_world', worldId, message: 'world not in registry' });
2731
2827
  }
2732
2828
  return handleServerBridges(req, res, worldId, portSegment);
2733
2829
  }
@@ -2798,7 +2894,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2798
2894
  if (v1WorldsStatusMatch && req.method === 'GET') {
2799
2895
  const worldId = decodeURIComponent(v1WorldsStatusMatch[1]);
2800
2896
  if (!V1_WORLD_ID.test(worldId)) {
2801
- return jsonReply(res, 400, { error: 'invalid_world_id', worldId });
2897
+ return jsonReply(res, 400, { error: 'invalid_world_id', worldId, message: 'worldId contains invalid characters' });
2802
2898
  }
2803
2899
  try {
2804
2900
  const report = await hostCpEngine.getWorldStatus(worldId);
@@ -2816,7 +2912,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
2816
2912
  if (v1WorldsLogsMatch && req.method === 'GET') {
2817
2913
  const worldId = decodeURIComponent(v1WorldsLogsMatch[1]);
2818
2914
  if (!V1_WORLD_ID.test(worldId)) {
2819
- return jsonReply(res, 400, { error: 'invalid_world_id', worldId });
2915
+ return jsonReply(res, 400, { error: 'invalid_world_id', worldId, message: 'worldId contains invalid characters' });
2820
2916
  }
2821
2917
  const tailParam = url.searchParams.get('tail');
2822
2918
  const followParam = url.searchParams.get('follow');
@@ -3466,6 +3562,7 @@ async function renderSpaShell(filePath, pathname) {
3466
3562
  // block comment above (Phase E5 cutover).
3467
3563
  void skipBootstrap; // wildcard invariant: always true; documents intent
3468
3564
  html = html.replace(/<head>/i, `<head>\n ${bearerInjection}${cloudInjection}`);
3565
+ if (_spaCacheByKey.size > 10) _spaCacheByKey.clear();
3469
3566
  _spaCacheByKey.set(cacheKey, html);
3470
3567
  return html;
3471
3568
  }
@@ -3660,6 +3757,15 @@ server.listen(PORT, '0.0.0.0', () => {
3660
3757
  } else if (AUTH_SERVICE_URL) {
3661
3758
  console.log(` auth-service=${AUTH_SERVICE_URL} (X-Olam-Secret configured)`);
3662
3759
  }
3760
+ // Warn when only one of the two cloud env vars is set — the most common
3761
+ // misconfiguration that silently disables the Cloud toggle in the SPA.
3762
+ const hasCloudUrl = Boolean(process.env.OLAM_CLOUD_URL);
3763
+ const hasShowcasePw = Boolean(process.env.OLAM_SHOWCASE_PASSWORD);
3764
+ if (hasCloudUrl && !hasShowcasePw) {
3765
+ console.warn(' [cloud] OLAM_CLOUD_URL set but OLAM_SHOWCASE_PASSWORD missing — cloud_enabled will be false');
3766
+ } else if (hasShowcasePw && !hasCloudUrl) {
3767
+ console.warn(' [cloud] OLAM_SHOWCASE_PASSWORD set but OLAM_CLOUD_URL missing — cloud_enabled will be false');
3768
+ }
3663
3769
  });
3664
3770
 
3665
3771
  // Graceful shutdown so docker compose down → SIGTERM → flush + close.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pleri/olam-cli",
3
- "version": "0.1.186",
3
+ "version": "0.1.195",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "olam": "./bin/olam.cjs"
@@ -32,6 +32,7 @@
32
32
  "test": "vitest run --passWithNoTests",
33
33
  "test:ci": "vitest run --reporter=basic --passWithNoTests",
34
34
  "test:docker": "vitest run --config vitest.config.docker.ts",
35
+ "test:e2e:k3d-https": "node e2e/k3d-https-e2e.spec.mjs",
35
36
  "audit:publish-deps": "node scripts/audit-publish-deps.mjs",
36
37
  "audit:cli-bundle-k8s": "node scripts/audit-cli-bundle-k8s.mjs",
37
38
  "audit:cli-package-contents": "node scripts/audit-cli-package-contents.mjs"