cli-jaw 2.2.10 → 2.2.12

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 (124) hide show
  1. package/README.ja.md +15 -0
  2. package/README.ko.md +15 -0
  3. package/README.md +15 -0
  4. package/dist/bin/cli-jaw.js +5 -1
  5. package/dist/bin/cli-jaw.js.map +1 -1
  6. package/dist/bin/commands/doctor.js +22 -2
  7. package/dist/bin/commands/doctor.js.map +1 -1
  8. package/dist/bin/commands/init.js +7 -0
  9. package/dist/bin/commands/init.js.map +1 -1
  10. package/dist/bin/commands/slack.js +232 -0
  11. package/dist/bin/commands/slack.js.map +1 -0
  12. package/dist/bin/commands/tui/slash-command-runner.js +11 -2
  13. package/dist/bin/commands/tui/slash-command-runner.js.map +1 -1
  14. package/dist/server.js +18 -2
  15. package/dist/server.js.map +1 -1
  16. package/dist/src/cli/commands.js +68 -15
  17. package/dist/src/cli/commands.js.map +1 -1
  18. package/dist/src/cli/opencodex-models.js +67 -14
  19. package/dist/src/cli/opencodex-models.js.map +1 -1
  20. package/dist/src/cli/registry-live.js +55 -2
  21. package/dist/src/cli/registry-live.js.map +1 -1
  22. package/dist/src/cli/registry.js +8 -2
  23. package/dist/src/cli/registry.js.map +1 -1
  24. package/dist/src/core/config.js +26 -1
  25. package/dist/src/core/config.js.map +1 -1
  26. package/dist/src/messaging/channel-health.js +6 -0
  27. package/dist/src/messaging/channel-health.js.map +1 -1
  28. package/dist/src/messaging/channel-validate.js +85 -0
  29. package/dist/src/messaging/channel-validate.js.map +1 -0
  30. package/dist/src/routes/messaging.js +9 -0
  31. package/dist/src/routes/messaging.js.map +1 -1
  32. package/dist/src/routes/system.js +5 -0
  33. package/dist/src/routes/system.js.map +1 -1
  34. package/dist/src/slack/api.js +32 -3
  35. package/dist/src/slack/api.js.map +1 -1
  36. package/dist/src/slack/bot.js +37 -1
  37. package/dist/src/slack/bot.js.map +1 -1
  38. package/dist/src/slack/events.js +23 -0
  39. package/dist/src/slack/events.js.map +1 -1
  40. package/dist/src/slack/hot-notify.js +33 -0
  41. package/dist/src/slack/hot-notify.js.map +1 -0
  42. package/dist/src/slack/manifest.js +85 -0
  43. package/dist/src/slack/manifest.js.map +1 -0
  44. package/dist/src/slack/progress.js +98 -0
  45. package/dist/src/slack/progress.js.map +1 -0
  46. package/dist/src/slack/send-only-client.js +3 -2
  47. package/dist/src/slack/send-only-client.js.map +1 -1
  48. package/dist/src/slack/slack-file.js +4 -2
  49. package/dist/src/slack/slack-file.js.map +1 -1
  50. package/package.json +3 -1
  51. package/public/css/sidebar.css +200 -0
  52. package/public/dist/assets/Agent-BjbZVZ9p.js +1 -0
  53. package/public/dist/assets/{Heartbeat-vDGhPfct.js → Heartbeat-DDmZARWw.js} +1 -1
  54. package/public/dist/assets/{Memory-B_UZ-iCA.js → Memory-CUbviW7N.js} +1 -1
  55. package/public/dist/assets/ModelProvider-ngg8xTuB.js +1 -0
  56. package/public/dist/assets/{agent-meta-C8DqYXM6.js → agent-meta-Y3DE6sxh.js} +1 -1
  57. package/public/dist/assets/app-BzD_ZLvt.js +55 -0
  58. package/public/dist/assets/{app-Nu2VPaO5.css → app-DX4e6isX.css} +1 -1
  59. package/public/dist/assets/bgtask-badge-Dqbp-Y4e.js +1 -0
  60. package/public/dist/assets/{bgtask-badge-DXca8E7c.js → bgtask-badge-qMkKOZx0.js} +1 -1
  61. package/public/dist/assets/{constants-CSZ011-j.js → constants-Df7-7Rky.js} +1 -1
  62. package/public/dist/assets/{employees-BL4qMxJj.js → employees-BnobWM_2.js} +1 -1
  63. package/public/dist/assets/{iframe-renderer-DUI0LYBy.js → iframe-renderer-C5vRbbl9.js} +2 -2
  64. package/public/dist/assets/iframe-renderer-Df4KIVyx.js +1 -0
  65. package/public/dist/assets/{manager-B-gpf-3j.js → manager-DL1GygQL.js} +1 -1
  66. package/public/dist/assets/memory-D-bULTHl.js +1 -0
  67. package/public/dist/assets/{memory-B_Y0L0KD.js → memory-DF4kPwwC.js} +1 -1
  68. package/public/dist/assets/{message-history-Dl8dJzez.js → message-history-C4WOisGx.js} +1 -1
  69. package/public/dist/assets/message-history-ZBoza6ew.js +1 -0
  70. package/public/dist/assets/{render-BI1aqKoc.js → render-DYX6GRQ0.js} +1 -1
  71. package/public/dist/assets/{settings-B0Sj4sUH.js → settings-DbJpGoaX.js} +1 -1
  72. package/public/dist/assets/settings-SreihUje.js +1 -0
  73. package/public/dist/assets/settings-core-B1Y5Nb4Z.js +1 -0
  74. package/public/dist/assets/settings-core-DobZWIZ7.js +140 -0
  75. package/public/dist/assets/{sidebar-DOR9fTgN.js → sidebar-Cp22g0Gv.js} +3 -3
  76. package/public/dist/assets/skills-DwPIP08e.js +1 -0
  77. package/public/dist/assets/{skills-wt0fkV9Y.js → skills-IGquen_j.js} +1 -1
  78. package/public/dist/assets/slash-commands-BBVfwcnW.js +1 -0
  79. package/public/dist/assets/{slash-commands-BZbqTg3v.js → slash-commands-ZRDOL99E.js} +1 -1
  80. package/public/dist/assets/{trace-drawer-kMYzaVFW.js → trace-drawer-D8To8fYt.js} +1 -1
  81. package/public/dist/assets/ui-CAicpUZc.js +1 -0
  82. package/public/dist/assets/{ui-D4D5HUfo.js → ui-Hx_mNoDf.js} +1 -1
  83. package/public/dist/index.html +90 -47
  84. package/public/dist/manager/index.html +1 -1
  85. package/public/index.html +88 -45
  86. package/public/js/constants.ts +50 -0
  87. package/public/js/features/channel-onboarding-flow.ts +197 -0
  88. package/public/js/features/channel-onboarding.ts +322 -0
  89. package/public/js/features/channel-setup-guide.ts +26 -0
  90. package/public/js/features/channel-setup-rules.ts +32 -0
  91. package/public/js/features/notifications.ts +38 -0
  92. package/public/js/features/settings-channel.ts +4 -0
  93. package/public/js/features/settings-core.ts +73 -9
  94. package/public/js/features/settings-discord.ts +2 -0
  95. package/public/js/features/settings-slack.ts +60 -2
  96. package/public/js/features/settings-telegram.ts +2 -0
  97. package/public/js/features/settings-types.ts +1 -1
  98. package/public/js/features/settings.ts +1 -1
  99. package/public/js/features/transport-status-row.ts +7 -3
  100. package/public/js/main.ts +5 -1
  101. package/public/locales/en.json +65 -3
  102. package/public/locales/ja.json +76 -3
  103. package/public/locales/ko.json +65 -3
  104. package/public/locales/zh.json +76 -3
  105. package/public/manager/src/settings/pages/Agent.tsx +25 -4
  106. package/public/manager/src/settings/pages/components/PerCliRow.tsx +13 -5
  107. package/public/manager/src/settings/pages/components/agent/agent-meta.ts +98 -0
  108. package/scripts/bundle-sidecar.sh +13 -4
  109. package/scripts/check-sidecar-prune-safety.mjs +167 -0
  110. package/scripts/pick-gyp-python.sh +61 -0
  111. package/scripts/release-gates.mjs +14 -0
  112. package/public/dist/assets/Agent-BgkP4EnS.js +0 -1
  113. package/public/dist/assets/ModelProvider-CgUvs6pE.js +0 -1
  114. package/public/dist/assets/app-BGIAy4he.js +0 -55
  115. package/public/dist/assets/bgtask-badge-GRSiNILQ.js +0 -1
  116. package/public/dist/assets/iframe-renderer-BUSk0Rw9.js +0 -1
  117. package/public/dist/assets/memory-CX6YahTc.js +0 -1
  118. package/public/dist/assets/message-history-Det7u76S.js +0 -1
  119. package/public/dist/assets/settings-C7NutP6X.js +0 -1
  120. package/public/dist/assets/settings-core-CyX-px6_.js +0 -100
  121. package/public/dist/assets/settings-core-DVpFmrhY.js +0 -1
  122. package/public/dist/assets/skills-BWhKreQ2.js +0 -1
  123. package/public/dist/assets/slash-commands-Di4NDA4w.js +0 -1
  124. package/public/dist/assets/ui-BghexWrB.js +0 -1
@@ -4,6 +4,7 @@ import { useState } from 'react';
4
4
  import { TextField, SelectField, ToggleField } from '../../fields';
5
5
  import type { SettingsClient } from '../../types';
6
6
  import type { DirtyEntry } from '../../types';
7
+ import { coerceEffortForModel, effortChoicesForModel } from './agent/agent-meta';
7
8
  import type { CliMeta, PerCliEntry } from './agent/agent-meta';
8
9
  import { PiProfileDialog } from './PiProfileDialog';
9
10
  import { piModelOptions, piProfileOptions, type PiSettingsView } from './pi-profile';
@@ -36,9 +37,10 @@ export function PerCliRow({ cli, meta, original, value, setValue, setEntry, clie
36
37
  : hasProviders
37
38
  ? (meta.modelsByProvider?.[provider] ?? meta.models)
38
39
  : meta.models;
39
- const effortOptions = hasProviders
40
- ? (meta.effortsByProvider?.[provider] ?? meta.efforts)
41
- : meta.efforts;
40
+ const providerEfforts = hasProviders ? (meta.effortsByProvider?.[provider] ?? meta.efforts) : undefined;
41
+ // Per-model effort sets from a live opencodex catalog win over the
42
+ // provider/registry lists; the chosen value is forwarded to the wire.
43
+ const effortOptions = effortChoicesForModel(meta, value.model || '', providerEfforts, hasProviders ? provider : undefined);
42
44
 
43
45
  return (
44
46
  <div className="settings-percli-row" data-cli={cli}>
@@ -68,7 +70,9 @@ export function PerCliRow({ cli, meta, original, value, setValue, setEntry, clie
68
70
  const nextModels = meta.modelsByProvider?.[next] ?? [];
69
71
  const nextEfforts = meta.effortsByProvider?.[next] ?? [];
70
72
  const nextModel = nextModels.includes(value.model || '') ? (value.model || '') : (nextModels[0] || '');
71
- const nextEffort = nextEfforts.includes(value.effort || '') ? (value.effort || '') : '';
73
+ // Resolve against the NEW provider+model pair: the same
74
+ // model id can allow different efforts per provider.
75
+ const nextEffort = coerceEffortForModel(meta, nextModel, value.effort ?? '', nextEfforts, next);
72
76
  setValue({ ...value, provider: next, model: nextModel, effort: nextEffort });
73
77
  setEntry(`perCli.${cli}.provider`, entryFor(next, original.provider ?? meta.defaultProvider ?? 'claude'));
74
78
  setEntry(`perCli.${cli}.model`, entryFor(nextModel, original.model ?? ''));
@@ -84,8 +88,12 @@ export function PerCliRow({ cli, meta, original, value, setValue, setEntry, clie
84
88
  value={value.model ?? modelOptions[0] ?? ''}
85
89
  options={modelOptions.map((m) => ({ value: m, label: m }))}
86
90
  onChange={(next) => {
87
- setValue({ ...value, model: next });
91
+ const nextEffort = coerceEffortForModel(meta, next, value.effort ?? '', providerEfforts, hasProviders ? provider : undefined);
92
+ setValue({ ...value, model: next, effort: nextEffort });
88
93
  setEntry(`perCli.${cli}.model`, entryFor(next, original.model ?? ''));
94
+ if (nextEffort !== (value.effort ?? '')) {
95
+ setEntry(`perCli.${cli}.effort`, entryFor(nextEffort, original.effort ?? ''));
96
+ }
89
97
  }}
90
98
  />
91
99
  ) : (
@@ -6,6 +6,22 @@ export type CliMeta = {
6
6
  providers?: ReadonlyArray<string>;
7
7
  modelsByProvider?: Record<string, ReadonlyArray<string>>;
8
8
  effortsByProvider?: Record<string, ReadonlyArray<string>>;
9
+ /**
10
+ * Per-model reasoning-effort sets from a live opencodex catalog. An entry
11
+ * that exists but is EMPTY means the model takes no effort at all, so it
12
+ * must not fall back to `efforts`.
13
+ */
14
+ effortsByModel?: Record<string, ReadonlyArray<string>>;
15
+ defaultEffortByModel?: Record<string, string>;
16
+ /**
17
+ * Provider-scoped per-model efforts, used by runtimes whose model list is
18
+ * split by provider (`ai-e`). A flat map would collide on ids shared across
19
+ * providers — `gpt-5.6-sol` exists under both codex and kiro with different
20
+ * allowed efforts.
21
+ */
22
+ effortsByModelByProvider?: Record<string, Record<string, ReadonlyArray<string>>>;
23
+ defaultEffortByModelByProvider?: Record<string, Record<string, string>>;
24
+ modelSource?: string;
9
25
  effortNote?: string;
10
26
  modelNote?: string;
11
27
  };
@@ -230,6 +246,35 @@ function stringArrayRecord(value: unknown): Record<string, string[]> | undefined
230
246
  return out;
231
247
  }
232
248
 
249
+ function stringRecord(value: unknown): Record<string, string> | undefined {
250
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
251
+ const out: Record<string, string> = {};
252
+ for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
253
+ if (typeof raw === 'string') out[key] = raw;
254
+ }
255
+ return out;
256
+ }
257
+
258
+ function nestedStringArrayRecord(value: unknown): Record<string, Record<string, string[]>> | undefined {
259
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
260
+ const out: Record<string, Record<string, string[]>> = {};
261
+ for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
262
+ const inner = stringArrayRecord(raw);
263
+ if (inner) out[key] = inner;
264
+ }
265
+ return out;
266
+ }
267
+
268
+ function nestedStringRecord(value: unknown): Record<string, Record<string, string>> | undefined {
269
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
270
+ const out: Record<string, Record<string, string>> = {};
271
+ for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
272
+ const inner = stringRecord(raw);
273
+ if (inner) out[key] = inner;
274
+ }
275
+ return out;
276
+ }
277
+
233
278
  export function normalizeCliMetaRegistry(raw: unknown): Record<string, CliMeta> {
234
279
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
235
280
  const out: Record<string, CliMeta> = {};
@@ -238,6 +283,10 @@ export function normalizeCliMetaRegistry(raw: unknown): Record<string, CliMeta>
238
283
  const record = value as Record<string, unknown>;
239
284
  const modelsByProvider = stringArrayRecord(record['modelsByProvider']);
240
285
  const effortsByProvider = stringArrayRecord(record['effortsByProvider']);
286
+ const effortsByModel = stringArrayRecord(record['effortsByModel']);
287
+ const defaultEffortByModel = stringRecord(record['defaultEffortByModel']);
288
+ const effortsByModelByProvider = nestedStringArrayRecord(record['effortsByModelByProvider']);
289
+ const defaultEffortByModelByProvider = nestedStringRecord(record['defaultEffortByModelByProvider']);
241
290
  out[key] = {
242
291
  label: typeof record['label'] === 'string' ? record['label'] : key,
243
292
  models: stringArray(record['models']),
@@ -246,6 +295,11 @@ export function normalizeCliMetaRegistry(raw: unknown): Record<string, CliMeta>
246
295
  ...(record['providers'] ? { providers: stringArray(record['providers']) } : {}),
247
296
  ...(modelsByProvider ? { modelsByProvider } : {}),
248
297
  ...(effortsByProvider ? { effortsByProvider } : {}),
298
+ ...(effortsByModel ? { effortsByModel } : {}),
299
+ ...(defaultEffortByModel ? { defaultEffortByModel } : {}),
300
+ ...(effortsByModelByProvider ? { effortsByModelByProvider } : {}),
301
+ ...(defaultEffortByModelByProvider ? { defaultEffortByModelByProvider } : {}),
302
+ ...(typeof record['modelSource'] === 'string' ? { modelSource: record['modelSource'] } : {}),
249
303
  ...(typeof record['effortNote'] === 'string' ? { effortNote: record['effortNote'] } : {}),
250
304
  ...(typeof record['modelNote'] === 'string' ? { modelNote: record['modelNote'] } : {}),
251
305
  };
@@ -253,6 +307,50 @@ export function normalizeCliMetaRegistry(raw: unknown): Record<string, CliMeta>
253
307
  return out;
254
308
  }
255
309
 
310
+ /**
311
+ * Effort choices for one model.
312
+ *
313
+ * A live opencodex catalog advertises a different effort set per model, and the
314
+ * chosen value reaches the wire as `-c model_reasoning_effort=`, so the
315
+ * per-model set wins. An entry that exists but is EMPTY means the model takes
316
+ * no effort and must not fall back to the provider/registry lists.
317
+ */
318
+ export function effortChoicesForModel(
319
+ meta: CliMeta,
320
+ model: string,
321
+ providerEfforts?: ReadonlyArray<string>,
322
+ provider?: string,
323
+ ): ReadonlyArray<string> {
324
+ const key = (model || '').trim();
325
+ // Provider-scoped map wins for provider-split runtimes. When the map exists
326
+ // but the model is absent (e.g. a Claude model under ai-e/claude), fall back
327
+ // to the provider list rather than inventing an empty "no effort" set.
328
+ if (provider) {
329
+ const scoped = meta.effortsByModelByProvider?.[provider]?.[key];
330
+ if (scoped) return scoped;
331
+ }
332
+ const byModel = meta.effortsByModel?.[key];
333
+ if (byModel) return byModel;
334
+ return providerEfforts ?? meta.efforts;
335
+ }
336
+
337
+ /** Keep a saved effort only when the model still advertises it. */
338
+ export function coerceEffortForModel(
339
+ meta: CliMeta,
340
+ model: string,
341
+ effort: string,
342
+ providerEfforts?: ReadonlyArray<string>,
343
+ provider?: string,
344
+ ): string {
345
+ const choices = effortChoicesForModel(meta, model, providerEfforts, provider);
346
+ if (effort && choices.includes(effort)) return effort;
347
+ const key = (model || '').trim();
348
+ const fallback = (provider ? meta.defaultEffortByModelByProvider?.[provider]?.[key] : undefined)
349
+ ?? meta.defaultEffortByModel?.[key]
350
+ ?? '';
351
+ return choices.includes(fallback) ? fallback : '';
352
+ }
353
+
256
354
  export function metaFor(cli: string, registry?: Record<string, CliMeta> | null): CliMeta {
257
355
  return registry?.[cli] || CLI_META[cli] || { label: cli, models: [], efforts: [] };
258
356
  }
@@ -18,6 +18,11 @@ install_locked_production_dependencies() {
18
18
 
19
19
  echo "=== Bundling sidecar: $PLATFORM-$ARCH ==="
20
20
 
21
+ # Refuse to build a sidecar whose prune list would strip a runtime dependency.
22
+ # Failing here costs a few seconds; failing later costs a shipped app that dies
23
+ # on first use, which is exactly what happened with node-fetch.
24
+ node "$PROJECT_ROOT/scripts/check-sidecar-prune-safety.mjs"
25
+
21
26
  rm -rf "$SIDECAR_DIR"
22
27
  mkdir -p "$SIDECAR_DIR/bin"
23
28
 
@@ -57,14 +62,20 @@ cd "$SIDECAR_DIR"
57
62
  install_locked_production_dependencies
58
63
 
59
64
  echo "Pruning frontend-only dependencies..."
65
+ # Every entry here is deleted from the bundled sidecar, so a package the server
66
+ # imports must never appear. It did: node-fetch sat in this list from the commit
67
+ # that created this script while src/telegram/bot.ts imports it, and every
68
+ # packaged desktop app died with ERR_MODULE_NOT_FOUND the moment that module
69
+ # loaded. check-sidecar-prune-safety.mjs now fails the build when this list and
70
+ # the server's real imports disagree.
60
71
  PRUNE_PKGS=(
61
72
  "@codemirror/autocomplete" "@codemirror/lang-markdown" "@codemirror/language"
62
73
  "@codemirror/language-data" "@codemirror/state" "@codemirror/view"
63
74
  "@lezer/highlight" "@lucide/icons" "@milkdown/kit" "@replit/codemirror-vim"
64
75
  "@tanstack/virtual-core" "@uiw/react-codemirror" "@xterm/addon-fit" "@xterm/xterm"
65
- "d3" "dompurify" "highlight.js" "katex" "marked-highlight" "mermaid"
76
+ "d3" "dompurify" "katex" "marked-highlight" "mermaid"
66
77
  "react" "react-dom" "react-markdown" "rehype-katex" "rehype-sanitize"
67
- "remark-breaks" "remark-gfm" "remark-math" "node-fetch"
78
+ "remark-breaks" "remark-gfm" "remark-math"
68
79
  )
69
80
  for pkg in "${PRUNE_PKGS[@]}"; do
70
81
  rm -rf "$SIDECAR_DIR/node_modules/$pkg" 2>/dev/null || true
@@ -77,8 +88,6 @@ rm -rf "$SIDECAR_DIR/node_modules/@vue" 2>/dev/null || true
77
88
  rm -rf "$SIDECAR_DIR/node_modules/cytoscape" 2>/dev/null || true
78
89
  rm -rf "$SIDECAR_DIR/node_modules/cytoscape-fcose" 2>/dev/null || true
79
90
  rm -rf "$SIDECAR_DIR/node_modules/es-toolkit" 2>/dev/null || true
80
- rm -rf "$SIDECAR_DIR/node_modules/lodash" 2>/dev/null || true
81
- rm -rf "$SIDECAR_DIR/node_modules/web-streams-polyfill" 2>/dev/null || true
82
91
 
83
92
  echo "Removing stale .bin symlinks after dependency pruning..."
84
93
  find "$SIDECAR_DIR/node_modules/.bin" -type l ! -exec test -e {} \; -print -delete 2>/dev/null || true
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fail when the sidecar prune list would delete something the server imports.
4
+ *
5
+ * `scripts/bundle-sidecar.sh` ships a hand-written list of "frontend-only"
6
+ * packages and `rm -rf`s them out of the bundled sidecar. The list was wrong
7
+ * from the commit that introduced it: `node-fetch` sat in it while
8
+ * `src/telegram/bot.ts` imports it, so any packaged desktop app died the
9
+ * moment that module loaded --
10
+ *
11
+ * ERR_MODULE_NOT_FOUND: Cannot find package 'node-fetch'
12
+ * imported from .../Resources/server/dist/src/telegram/bot.js
13
+ *
14
+ * The dependency graph cannot answer this on its own: frontend and server
15
+ * packages both live in `dependencies`, so "reachable from package.json" marks
16
+ * 34 of the 37 pruned entries as unsafe and tells you nothing. What actually
17
+ * separates them is whether SERVER source imports the package -- so that is
18
+ * what this checks, and it also follows each kept package's own dependencies,
19
+ * because deleting a transitive one breaks the import just as thoroughly.
20
+ * `web-streams-polyfill` is exactly that case: nothing in src/ names it, but
21
+ * node-fetch -> fetch-blob -> web-streams-polyfill, and it was on the list too.
22
+ */
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
+
29
+ /**
30
+ * Trees whose imports must survive pruning.
31
+ *
32
+ * `dist` is what the sidecar actually runs, so it is the authority; the
33
+ * TypeScript sources are scanned too because a fresh checkout may not have
34
+ * built yet and a checker that silently passes on a missing directory is worse
35
+ * than one that over-approximates.
36
+ */
37
+ const SERVER_SOURCES = ['dist', 'src', 'bin', 'server.ts'];
38
+
39
+ /**
40
+ * Packages loaded by a computed specifier, which no regex over the source can
41
+ * find. `src/agent/jwc-runtime.ts` does `import(spec)` where spec comes from an
42
+ * env var or a default constant, and the audit was right that the scan misses
43
+ * it. Listing them here is a deliberate escape hatch: each entry names why it
44
+ * cannot be discovered automatically.
45
+ */
46
+ const RUNTIME_LOADED = [
47
+ // JWC SDK: `import(spec)` with spec from JWC_SDK_PATH or DEFAULT_JWC_SDK_SPEC.
48
+ '@jawcode/engine-sdk',
49
+ ];
50
+
51
+ /** Packages the prune step removes, read from the shell script itself. */
52
+ function prunedPackages() {
53
+ const script = fs.readFileSync(path.join(repoRoot, 'scripts/bundle-sidecar.sh'), 'utf8');
54
+
55
+ const listBlock = script.match(/PRUNE_PKGS=\(([\s\S]*?)\n\)/);
56
+ if (!listBlock) throw new Error('PRUNE_PKGS array not found in bundle-sidecar.sh');
57
+ const listed = [...listBlock[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
58
+
59
+ // The script also removes a handful of packages with individual rm -rf
60
+ // lines below the loop. Those are just as capable of breaking the sidecar,
61
+ // so they are checked with the same rule rather than trusted.
62
+ const individual = [...script.matchAll(
63
+ /rm -rf "\$SIDECAR_DIR\/node_modules\/([^"$]+)"/g)].map((m) => m[1]);
64
+
65
+ return [...new Set([...listed, ...individual])];
66
+ }
67
+
68
+ /** Every file under the server source trees. */
69
+ function serverFiles() {
70
+ const files = [];
71
+ const walk = (rel) => {
72
+ const abs = path.join(repoRoot, rel);
73
+ if (!fs.existsSync(abs)) return;
74
+ if (fs.statSync(abs).isFile()) { files.push(abs); return; }
75
+ for (const entry of fs.readdirSync(abs)) {
76
+ if (entry === 'node_modules' || entry.startsWith('.')) continue;
77
+ walk(path.join(rel, entry));
78
+ }
79
+ };
80
+ SERVER_SOURCES.forEach(walk);
81
+ return files.filter((f) => /\.(ts|tsx|mts|cts|js|mjs|cjs)$/.test(f));
82
+ }
83
+
84
+ /** Bare package specifiers imported anywhere in the server sources. */
85
+ function serverImports() {
86
+ const found = new Set();
87
+ const patterns = [
88
+ /\bfrom\s+['"]([^'".][^'"]*)['"]/g,
89
+ /\brequire\(\s*['"]([^'".][^'"]*)['"]\s*\)/g,
90
+ /\bimport\(\s*['"]([^'".][^'"]*)['"]\s*\)/g,
91
+ ];
92
+ for (const file of serverFiles()) {
93
+ const text = fs.readFileSync(file, 'utf8');
94
+ for (const re of patterns) {
95
+ for (const m of text.matchAll(re)) {
96
+ const spec = m[1];
97
+ if (spec.startsWith('node:')) continue;
98
+ // Scoped names keep two segments, plain names keep one.
99
+ const name = spec.startsWith('@')
100
+ ? spec.split('/').slice(0, 2).join('/')
101
+ : spec.split('/')[0];
102
+ found.add(name);
103
+ }
104
+ }
105
+ }
106
+ return found;
107
+ }
108
+
109
+ /** Everything reachable from `roots` through installed package dependencies. */
110
+ function closure(roots) {
111
+ const seen = new Set();
112
+ const stack = [...roots];
113
+ while (stack.length > 0) {
114
+ const name = stack.pop();
115
+ if (seen.has(name)) continue;
116
+ seen.add(name);
117
+ const manifest = path.join(repoRoot, 'node_modules', name, 'package.json');
118
+ if (!fs.existsSync(manifest)) continue;
119
+ let manifestJson;
120
+ try {
121
+ manifestJson = JSON.parse(fs.readFileSync(manifest, 'utf8'));
122
+ } catch { continue; }
123
+ // Follow optional and peer edges too. A package that resolves one of
124
+ // them at runtime fails identically to a missing hard dependency, and
125
+ // over-keeping costs disk while under-keeping ships a broken app.
126
+ const deps = {
127
+ ...(manifestJson.dependencies || {}),
128
+ ...(manifestJson.optionalDependencies || {}),
129
+ ...(manifestJson.peerDependencies || {}),
130
+ };
131
+ for (const dep of Object.keys(deps)) if (!seen.has(dep)) stack.push(dep);
132
+ }
133
+ return seen;
134
+ }
135
+
136
+ export function findUnsafePrunes() {
137
+ const pruned = prunedPackages();
138
+ const imported = serverImports();
139
+ for (const pkg of RUNTIME_LOADED) imported.add(pkg);
140
+ // Only walk from packages the server actually imports; the closure of every
141
+ // dependency would sweep the frontend back in.
142
+ const required = closure([...imported]);
143
+
144
+ return pruned
145
+ .filter((pkg) => required.has(pkg))
146
+ .map((pkg) => ({
147
+ pkg,
148
+ direct: imported.has(pkg),
149
+ }));
150
+ }
151
+
152
+ if (import.meta.url === `file://${process.argv[1]}`) {
153
+ const unsafe = findUnsafePrunes();
154
+ if (unsafe.length === 0) {
155
+ console.log('✅ sidecar prune list removes nothing the server needs');
156
+ process.exit(0);
157
+ }
158
+ console.error(`❌ sidecar prune list — ${unsafe.length} package(s) the server needs:`);
159
+ for (const { pkg, direct } of unsafe) {
160
+ console.error(` ${pkg} — ${direct
161
+ ? 'imported directly by server source'
162
+ : 'required transitively by a package the server imports'}`);
163
+ }
164
+ console.error('\nRemove them from scripts/bundle-sidecar.sh, or the packaged app will');
165
+ console.error('die with ERR_MODULE_NOT_FOUND the first time that code path runs.');
166
+ process.exit(1);
167
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ # Print a python3 that node-gyp can actually run under.
3
+ #
4
+ # node-gyp's bundled gyp still does `from distutils.version import StrictVersion`,
5
+ # and distutils was removed in Python 3.12. On a machine whose default python3 is
6
+ # Homebrew 3.12+ the native rebuild dies with:
7
+ #
8
+ # ModuleNotFoundError: No module named 'distutils'
9
+ # ⨯ node-gyp failed to rebuild '.../node_modules/node-pty'
10
+ #
11
+ # Observed on macOS 27 with Homebrew Python 3.14.6 while building the desktop app
12
+ # on a second machine. macOS still ships /usr/bin/python3 (3.9.6), which has
13
+ # distutils, so a working interpreter is usually present -- it just is not the
14
+ # one on PATH.
15
+ #
16
+ # CI already knew: .github/workflows/desktop-release.yml pins actions/setup-python
17
+ # to 3.11 with exactly this reasoning in a comment. The local build path never got
18
+ # the same treatment, so `npm run electron:dist:mac` on a developer machine was the
19
+ # one route into node-gyp with no defense.
20
+ #
21
+ # Candidates are TESTED, not assumed: a hardcoded path is the same bet that broke
22
+ # here, one Python release later. An explicit $PYTHON still wins, because the
23
+ # caller may know something this script does not.
24
+ set -euo pipefail
25
+
26
+ has_distutils() {
27
+ [ -x "$1" ] || return 1
28
+ "$1" -c 'import distutils' >/dev/null 2>&1
29
+ }
30
+
31
+ if [ -n "${PYTHON:-}" ]; then
32
+ printf '%s\n' "$PYTHON"
33
+ exit 0
34
+ fi
35
+
36
+ for candidate in \
37
+ /usr/bin/python3 \
38
+ "$(command -v python3.11 2>/dev/null || true)" \
39
+ "$(command -v python3.10 2>/dev/null || true)" \
40
+ "$(command -v python3.9 2>/dev/null || true)" \
41
+ "$(command -v python3 2>/dev/null || true)"
42
+ do
43
+ [ -n "$candidate" ] || continue
44
+ if has_distutils "$candidate"; then
45
+ printf '%s\n' "$candidate"
46
+ exit 0
47
+ fi
48
+ done
49
+
50
+ # Nothing usable. Fall back to whatever python3 exists so the caller fails with
51
+ # node-gyp's own message rather than an empty variable, and say why.
52
+ fallback="$(command -v python3 2>/dev/null || true)"
53
+ if [ -n "$fallback" ]; then
54
+ echo "[gyp-python] no python3 with distutils found; falling back to $fallback" >&2
55
+ echo "[gyp-python] node-gyp will likely fail. Install python@3.11 or set PYTHON=<path>." >&2
56
+ printf '%s\n' "$fallback"
57
+ exit 0
58
+ fi
59
+
60
+ echo "[gyp-python] no python3 on PATH at all" >&2
61
+ exit 1
@@ -67,6 +67,10 @@ const GATES = {
67
67
  'tests/unit/browser-web-ai-source-audit.test.ts',
68
68
  'tests/unit/browser-web-ai-cli-contract.test.ts',
69
69
  'tests/unit/release-gates.test.ts',
70
+ // Guards the release/build scripts themselves, so it belongs in
71
+ // the suite gate:all actually runs rather than npm test only.
72
+ 'tests/unit/electron-version-sync.test.ts',
73
+ 'tests/unit/gyp-python-pick.test.ts',
70
74
  ].filter((p) => fs.existsSync(path.join(repoRoot, p)));
71
75
  if (targets.length === 0) {
72
76
  return { ok: false, detail: 'no Phase 22 mirror tests found' };
@@ -414,6 +418,16 @@ const GATES = {
414
418
  return { ok: true, detail: 'desktop artifacts will carry the release version' };
415
419
  },
416
420
  },
421
+ 'sidecar-prune-safety': {
422
+ description: 'the sidecar prune list never deletes a package the server imports',
423
+ check() {
424
+ const r = run('node', ['scripts/check-sidecar-prune-safety.mjs'], { timeout: 60_000 });
425
+ if (r.status !== 0) {
426
+ return { ok: false, detail: (r.stdout || r.stderr || '').trim().slice(-800) };
427
+ }
428
+ return { ok: true, detail: 'packaged sidecar keeps every runtime dependency' };
429
+ },
430
+ },
417
431
  'gate-docs': {
418
432
  description: 'structure/INDEX.md documents exactly the gates that exist, and each is npm-addressable',
419
433
  check() {
@@ -1 +0,0 @@
1
- import{r as e}from"./rolldown-runtime-XQCOJYun.js";import{n as t,t as n}from"./jsx-runtime-gFMgZ0TQ.js";import{a as r,i,n as a,r as o,t as s}from"./page-shell-LF7vVVfG.js";import{i as c,s as l}from"./fields-zkeByFQW.js";import{a as u,n as d,o as f}from"./Permissions-CnKt44dN.js";import{a as p,i as m,n as h,o as g,r as _,s as v,t as y}from"./agent-meta-C8DqYXM6.js";import{t as b}from"./path-utils-LsJeDCKF.js";var x=e(t(),1),S=n();function C({cli:e,cliOptions:t,provider:n=``,providerOptions:r=[],model:a,modelOptions:o,effort:s,effortOptions:u,workingDir:d,workingDirError:f,cliMeta:m,onCliChange:g,onProviderChange:_,onModelChange:v,onEffortChange:b,onWorkingDirChange:x}){let C=p(t),w=C.filter(e=>y.includes(e)).length;return(0,S.jsx)(i,{title:`Agent runtime`,hint:`Active CLI, model, effort, and workspace used by this instance.`,children:(0,S.jsxs)(`div`,{className:`settings-agent-runtime-grid`,children:[(0,S.jsx)(c,{id:`agent-cli`,label:`Active CLI`,value:e,options:C.map(e=>({value:e,label:h(e,m).label||e})),collapsedAfter:w,onChange:g}),r.length>0?(0,S.jsx)(c,{id:`agent-${e}-provider`,label:`Provider`,value:n,options:r.map(e=>({value:e,label:e})),onChange:e=>_?.(e)}):null,(0,S.jsx)(c,{id:`agent-model`,label:`Active model`,value:a,options:o.length>0?o:[{value:``,label:`(default)`}],onChange:v}),(0,S.jsx)(c,{id:`agent-effort`,label:`Effort`,value:s,options:[{value:``,label:`(default)`},...u.map(e=>({value:e,label:e}))],disabled:u.length===0,onChange:b}),(0,S.jsx)(l,{id:`agent-workingDir`,label:`Working directory`,value:d,error:f,placeholder:`/path/to/project`,onChange:x})]})})}var w=[{value:`auto`,label:`Auto`},{value:`custom`,label:`Custom allowlist`}];function ee({value:e,onChange:t}){let n=u(e),r=n.mode===`custom`?`custom`:`auto`,a=n.mode===`custom`?n.tokens:[],o=n.mode===`custom`?`${a.length} explicit token${a.length===1?``:`s`}`:`Runtime resolves allowed capabilities`;return(0,S.jsxs)(i,{title:`Permissions`,hint:`Quick runtime mode. Open Permissions for detailed allowlist editing.`,children:[(0,S.jsx)(c,{id:`agent-permissions-mode`,label:`Mode`,value:r,options:w,onChange:e=>{t(e===`auto`?`auto`:a.length>0&&d(a)?a:f(null))}}),(0,S.jsx)(`p`,{className:`settings-agent-note`,children:o})]})}function te({activeCli:e,flushCli:t,flushModel:n,cliOptions:r,modelOptions:a,cliMeta:o,loading:s,error:l,onFlushCliChange:u,onFlushModelChange:d}){let f=t||e,p=f?h(f,o).label||f:`active`;return(0,S.jsx)(i,{title:`Flush Agent`,hint:`Separate summary agent used for context flush and compact work.`,children:(0,S.jsxs)(`details`,{className:`settings-agent-flush`,children:[(0,S.jsxs)(`summary`,{children:[(0,S.jsx)(`span`,{children:`Flush runtime`}),(0,S.jsxs)(`code`,{children:[p,n&&n!==`default`?` / ${n}`:``]})]}),s?(0,S.jsx)(`p`,{className:`settings-agent-note`,children:`Loading flush settings...`}):null,l?(0,S.jsx)(`p`,{className:`settings-field-error`,role:`alert`,children:l}):null,(0,S.jsxs)(`div`,{className:`settings-agent-runtime-grid`,children:[(0,S.jsx)(c,{id:`agent-flush-cli`,label:`Flush CLI`,value:t,options:[{value:``,label:`(active CLI)`},...r.map(e=>({value:e,label:h(e,o).label||e}))],onChange:u}),(0,S.jsx)(c,{id:`agent-flush-model`,label:`Flush model`,value:n,options:[{value:``,label:`(default)`},...a],onChange:d})]})]})})}function T(e){return e.source===`static`||e.id.startsWith(`static:`)}function E(){return`new:${Date.now().toString(36)}:${Math.random().toString(36).slice(2,8)}`}function D(e,t){let n=e[0]||`claude`,r=t?.[n]?.models?.[0]||`default`;return{id:E(),name:`New Agent`,cli:n,model:r,role:``,status:`idle`,source:`db`}}function ne(e){return Array.isArray(e)?e.map(O).filter(Boolean):e.data.map(O).filter(Boolean)}function O(e){if(!e||typeof e!=`object`)return null;let t=e,n=typeof t.id==`string`?t.id:``,r=typeof t.name==`string`?t.name:``;if(!n||!r)return null;let i=t.source===`static`||n.startsWith(`static:`)?`static`:`db`,a={id:n,name:r,cli:typeof t.cli==`string`&&t.cli?t.cli:`claude`,model:typeof t.model==`string`&&t.model?t.model:`default`,role:typeof t.role==`string`?t.role:``,source:i};return typeof t.status==`string`&&(a.status=t.status),a}function k(e){return e.name.trim()?e.cli.trim()?e.model.trim()?null:`Model is required`:`CLI is required`:`Name is required`}function A(e){return e.some(e=>k(e)!==null)}function re(e,t){return JSON.stringify(e.map(I))===JSON.stringify(t.map(I))}function j(e,t){let n=new Map(e.map(e=>[e.id,e])),r=new Set(t.map(e=>e.id)),i=[],a=[],o=[];for(let e of t){let t=n.get(e.id);if(!t||e.id.startsWith(`new:`)){i.push(e);continue}let r=P(t,e);Object.keys(r).length>0&&a.push({before:t,after:e,patch:r})}for(let t of e)!r.has(t.id)&&!T(t)&&o.push(t);return{added:i,updated:a,removed:o}}function M(e,t){let n=j(e,t);return{added:n.added.length,updated:n.updated.length,removed:n.removed.length}}async function N(e,t,n){let r=j(t,n);for(let t of r.removed)await e.delete(`/api/employees/${encodeURIComponent(t.id)}`);for(let t of r.added)await e.post(`/api/employees`,F(t));for(let t of r.updated)await e.put(`/api/employees/${encodeURIComponent(t.after.id)}`,t.patch)}function P(e,t){if(T(t))return e.model===t.model?{}:{model:t.model};let n={};for(let r of[`name`,`cli`,`model`,`role`,`status`])(e[r]||``)!==(t[r]||``)&&(n[r]=t[r]||``);return n}function F(e){return{name:e.name.trim()||`New Agent`,cli:e.cli.trim()||`claude`,model:e.model.trim()||`default`,role:e.role.trim()}}function I(e){let t={id:e.id,name:e.name,cli:e.cli,model:e.model,role:e.role,source:e.source};return e.status!==void 0&&(t.status=e.status),t}function L({employee:e,index:t,cliOptions:n,cliMeta:r,onChange:i,onRemove:a}){let o=T(e),s=m(h(e.cli,r).models,e.model),u=k(e),d=Array.from(new Set([...n,e.cli,`claude`])).filter(Boolean);return(0,S.jsxs)(`fieldset`,{className:`settings-runtime-employee-row`,children:[(0,S.jsxs)(`legend`,{children:[e.name||`Employee ${t+1}`,o?(0,S.jsx)(`span`,{children:`static`}):null]}),(0,S.jsxs)(`div`,{className:`settings-runtime-employee-grid`,children:[(0,S.jsx)(l,{id:`runtime-employee-${e.id}-name`,label:`Name`,value:e.name,disabled:o,error:u===`Name is required`?u:null,onChange:e=>i({name:e})}),(0,S.jsx)(c,{id:`runtime-employee-${e.id}-cli`,label:`CLI`,value:e.cli,disabled:o,options:d.map(e=>({value:e,label:h(e,r).label||e})),onChange:e=>{i({cli:e,model:h(e,r).models[0]||`default`})}}),(0,S.jsx)(c,{id:`runtime-employee-${e.id}-model`,label:`Model`,value:e.model,options:s.length>0?s:[{value:`default`,label:`default`}],onChange:e=>i({model:e})}),(0,S.jsx)(l,{id:`runtime-employee-${e.id}-role`,label:`Role`,value:e.role,disabled:o,onChange:e=>i({role:e})})]}),(0,S.jsxs)(`div`,{className:`settings-runtime-employee-footer`,children:[(0,S.jsxs)(`span`,{children:[e.source,e.status?` / ${e.status}`:``]}),(0,S.jsx)(`button`,{type:`button`,className:`settings-action settings-action-discard`,disabled:o,onClick:a,"aria-label":`Remove employee ${e.name||t+1}`,children:`Remove`})]})]})}function ie({roster:e,original:t,cliOptions:n,cliMeta:r,loading:a,error:o,onRosterChange:s}){let c=M(t,e),l=A(e);function u(t,n){s(e.map((e,r)=>r===t?{...e,...n}:e))}function d(t){s(e.filter((e,n)=>n!==t))}return(0,S.jsxs)(i,{title:`Employees`,hint:`Runtime dispatch roster. Static employees keep their locked identity; database employees can be edited.`,children:[(0,S.jsxs)(`div`,{className:`settings-runtime-employee-summary`,children:[(0,S.jsxs)(`span`,{children:[`+`,c.added]}),(0,S.jsxs)(`span`,{children:[`~`,c.updated]}),(0,S.jsxs)(`span`,{children:[`-`,c.removed]}),l?(0,S.jsx)(`strong`,{children:`Fix invalid rows before saving.`}):null]}),a?(0,S.jsx)(`p`,{className:`settings-agent-note`,children:`Loading employees...`}):null,o?(0,S.jsx)(`p`,{className:`settings-field-error`,role:`alert`,children:o}):null,e.length===0?(0,S.jsx)(`p`,{className:`settings-empty`,children:`No runtime employees configured.`}):(0,S.jsx)(`div`,{className:`settings-runtime-employee-list`,children:e.map((e,t)=>(0,S.jsx)(L,{employee:e,index:t,cliOptions:n,cliMeta:r??null,onChange:e=>u(t,e),onRemove:()=>d(t)},e.id))}),(0,S.jsx)(`div`,{className:`settings-employee-footer-bar`,children:(0,S.jsx)(`button`,{type:`button`,className:`settings-action settings-action-discard`,onClick:()=>s([...e,D(n,r)]),children:`+ Add employee`})})]})}var R=new Set([`runtimeEmployees`,`flushCli`,`flushModel`]);function z(e){let t={},n=null,r=null;for(let[i,a]of Object.entries(e))i===`runtimeEmployees`?r=a:i===`flushCli`||i===`flushModel`?(n??={},n[i===`flushCli`?`cli`:`model`]=String(a??``)):R.has(i)||(t[i]=a);return{settingsBundle:t,flushPatch:n,runtimeEmployeesNext:r}}async function ae(e){let{client:t,bundle:n,employeeDraft:r,employeeOriginal:i}=e;if(A(r))throw Error(`runtime employees: fix invalid rows before saving`);let{settingsBundle:a,flushPatch:o,runtimeEmployeesNext:s}=z(n),c=null;try{if(Object.keys(a).length>0){let e=await t.put(`/api/settings`,b(a));c=e&&typeof e==`object`&&`data`in e?e.data:e}}catch(e){throw Error(`settings save failed: ${e instanceof Error?e.message:String(e)}`)}try{o&&await t.put(`/api/memory-files/settings`,o)}catch(e){throw Error(`flush settings save failed: ${e instanceof Error?e.message:String(e)}`)}try{s&&await N(t,i,s)}catch(e){throw Error(`runtime employees save failed: ${e instanceof Error?e.message:String(e)}`)}return c}function B({port:e,client:t,dirty:n,registerSave:i}){let{state:c,refresh:l,setData:d}=r(t,`/api/settings`),[f,p]=(0,x.useState)({cli:``,provider:``,model:``,effort:``,workingDir:``,permissions:`auto`}),[y,b]=(0,x.useState)({}),[w,T]=(0,x.useState)({}),[E,D]=(0,x.useState)(!0),[O,k]=(0,x.useState)(null),[j,M]=(0,x.useState)([]),[N,P]=(0,x.useState)([]),[F,I]=(0,x.useState)(!0),[L,R]=(0,x.useState)(null),[z,B]=(0,x.useState)(null),V=(0,x.useCallback)(async()=>{try{let e=await t.get(`/api/cli-registry`);B(_(e&&typeof e==`object`&&`data`in e?e.data:e))}catch{B(null)}},[t]),H=(0,x.useCallback)(async()=>{D(!0),k(null);try{let e=await t.get(`/api/memory-files`),n={cli:e.cli||``,model:e.model||``};b(n),T(n)}catch(e){k(e instanceof Error?e.message:String(e))}finally{D(!1)}},[t]),U=(0,x.useCallback)(async()=>{I(!0),R(null);try{let e=ne(await t.get(`/api/employees`));M(e),P(e)}catch(e){R(e instanceof Error?e.message:String(e))}finally{I(!1)}},[t]);(0,x.useEffect)(()=>{V(),H(),U()},[V,U,H]),(0,x.useEffect)(()=>{if(c.kind!==`ready`)return;let e=Object.keys(c.data.perCli||{}),t=c.data.cli||e[0]||``,n=u(c.data.permissions),r=h(t,z);p({cli:t,provider:c.data.perCli?.[t]?.provider||r.defaultProvider||``,model:v(t,c.data.perCli,c.data.activeOverrides),effort:g(t,c.data.perCli,c.data.activeOverrides),workingDir:c.data.workingDir||``,permissions:n.mode===`custom`?n.tokens:`auto`})},[z,c]),(0,x.useEffect)(()=>()=>{for(let e of Array.from(n.pending.keys()))(e===`cli`||e===`workingDir`||e===`permissions`||e===`runtimeEmployees`||e===`flushCli`||e===`flushModel`||e.startsWith(`activeOverrides.`))&&n.remove(e)},[n]);let W=(0,x.useCallback)((e,t)=>n.set(e,t),[n]),G=(0,x.useCallback)(async()=>{let e=n.saveBundle();if(Object.keys(e).length===0)return;let r=await ae({client:t,bundle:e,employeeDraft:N,employeeOriginal:j});n.clear(),r&&d(r),await l(),await V(),await H(),await U()},[t,n,N,j,V,U,H,l,d]);(0,x.useEffect)(()=>{if(i)return i(G),()=>i(null)},[i,G]);let K=c.kind===`ready`?c.data:{},q=K.perCli||{},J=K.activeOverrides||{},Y=(0,x.useMemo)(()=>Object.keys(q),[q]),X=h(f.cli,z),Z=f.provider||X.defaultProvider||X.providers?.[0]||``,Q=f.cli===`pi`,oe=!Q&&(X.providers?.length??0)>0,se=m(oe&&X.modelsByProvider?.[Z]||X.models,f.model),ce=oe&&X.effortsByProvider?.[Z]||X.efforts,le=f.workingDir.trim()?null:`Required`;if(c.kind===`loading`)return(0,S.jsx)(a,{});if(c.kind===`offline`)return(0,S.jsx)(o,{port:e});if(c.kind===`error`)return(0,S.jsx)(s,{message:c.message});function ue(){for(let e of Array.from(n.pending.keys()))e.startsWith(`activeOverrides.`)&&n.remove(e)}function $(e){p(e)}return(0,S.jsxs)(`form`,{className:`settings-page-form`,onSubmit:e=>{e.preventDefault(),G()},children:[(0,S.jsx)(C,{cli:f.cli,cliOptions:Y.length>0?Y:[f.cli||`claude`],provider:Z,providerOptions:Q?[]:X.providers||[],model:f.model,modelOptions:se,effort:f.effort,effortOptions:ce,workingDir:f.workingDir,workingDirError:le,cliMeta:z,onCliChange:e=>{let t=h(e,z),n={...f,cli:e,provider:q[e]?.provider||t.defaultProvider||``,model:v(e,q,J),effort:g(e,q,J)};ue(),$(n),W(`cli`,{value:e,original:K.cli||``,valid:!0})},onProviderChange:e=>{let t=X.modelsByProvider?.[e]||[],n=X.effortsByProvider?.[e]||[],r=t.includes(f.model)?f.model:t[0]||``,i=n.includes(f.effort)?f.effort:``;$({...f,provider:e,model:r,effort:i}),W(`perCli.${f.cli}.provider`,{value:e,original:q[f.cli]?.provider||X.defaultProvider||``,valid:!0}),W(`activeOverrides.${f.cli}.model`,{value:r,original:v(f.cli,q,J),valid:r.trim().length>0}),W(`activeOverrides.${f.cli}.effort`,{value:i,original:g(f.cli,q,J),valid:!0})},onModelChange:e=>{$({...f,model:e}),W(`activeOverrides.${f.cli}.model`,{value:e,original:v(f.cli,q,J),valid:e.trim().length>0})},onEffortChange:e=>{$({...f,effort:e}),W(`activeOverrides.${f.cli}.effort`,{value:e,original:g(f.cli,q,J),valid:!0})},onWorkingDirChange:e=>{$({...f,workingDir:e}),W(`workingDir`,{value:e,original:K.workingDir||``,valid:e.trim().length>0})}}),(0,S.jsx)(ee,{value:f.permissions,onChange:e=>{$({...f,permissions:e}),W(`permissions`,{value:e,original:K.permissions??`auto`,valid:e===`auto`||e.length>0})}}),(0,S.jsx)(te,{activeCli:f.cli,flushCli:w.cli||``,flushModel:w.model||``,cliOptions:Y,cliMeta:z,modelOptions:m(h(w.cli||f.cli,z).models,w.model||``),loading:E,error:O,onFlushCliChange:e=>{let t=e&&h(e,z).models[0]||``;T({cli:e,model:t}),W(`flushCli`,{value:e,original:y.cli||``,valid:!0}),W(`flushModel`,{value:t,original:y.model||``,valid:!0})},onFlushModelChange:e=>{T({...w,model:e}),W(`flushModel`,{value:e,original:y.model||``,valid:!0})}}),(0,S.jsx)(ie,{roster:N,original:j,cliOptions:Y,cliMeta:z,loading:F,error:L,onRosterChange:e=>{if(P(e),re(e,j)){n.remove(`runtimeEmployees`);return}n.set(`runtimeEmployees`,{value:e,original:j,valid:!A(e)})}})]})}export{B as default,z as splitAgentSaveBundle};
@@ -1 +0,0 @@
1
- import{r as e}from"./rolldown-runtime-XQCOJYun.js";import{n as t,t as n}from"./jsx-runtime-gFMgZ0TQ.js";import{a as r,i,n as a,r as o,t as s}from"./page-shell-LF7vVVfG.js";import{a as c,i as l,n as u,r as d,s as f}from"./fields-zkeByFQW.js";import{n as p,r as m}from"./agent-meta-C8DqYXM6.js";import{t as h}from"./path-utils-LsJeDCKF.js";var g=e(t(),1),_=n(),v=[`basic`,`openai`,`anthropic`,`vertex`];function y(e){return e===`anthropic`?`https://api.anthropic.com/v1/messages`:e===`vertex`?`https://LOCATION-aiplatform.googleapis.com/v1`:`http://127.0.0.1:18645/v1`}function b({client:e,pi:t,provider:n,model:r,onClose:i,onRegistered:a}){let o=(0,g.useMemo)(()=>t?.profiles?.find(e=>e.id===n)||t?.profiles?.[0],[t,n]),[s,c]=(0,g.useState)(o?.mode||`basic`),[u,d]=(0,g.useState)(o?.id||n||`progrok`),[p,m]=(0,g.useState)(o?.endpoint||y(s)),[h,b]=(0,g.useState)(r||o?.model||`grok-composer-2.5-fast`),[x,S]=(0,g.useState)(``),[C,w]=(0,g.useState)(!1),[T,E]=(0,g.useState)(null);(0,g.useEffect)(()=>{m(e=>e||y(s))},[s]);let D=async()=>{w(!0),E(null);try{let t=await e.post(`/api/pi/profiles/register`,{id:u,label:u,mode:s,endpoint:p,model:h,apiKey:x});a({provider:u,model:h,models:t.models||[],pi:t.settings?.pi}),i()}catch(e){E(e instanceof Error?e.message:String(e))}finally{w(!1)}};return(0,_.jsx)(`div`,{className:`settings-memory-modal`,role:`dialog`,"aria-modal":`true`,"aria-labelledby":`pi-profile-title`,children:(0,_.jsxs)(`div`,{className:`settings-memory-modal-card`,children:[(0,_.jsxs)(`header`,{className:`settings-memory-modal-header`,children:[(0,_.jsx)(`h3`,{id:`pi-profile-title`,children:`Pi Settings`}),(0,_.jsx)(`button`,{type:`button`,className:`settings-action settings-action-discard`,onClick:i,children:`Close`})]}),(0,_.jsxs)(`div`,{className:`settings-page-form`,children:[(0,_.jsx)(l,{id:`pi-profile-mode`,label:`Mode`,value:s,options:v.map(e=>({value:e,label:e})),onChange:e=>c(e)}),(0,_.jsx)(f,{id:`pi-profile-id`,label:`Provider`,value:u,onChange:d,placeholder:`progrok`}),(0,_.jsx)(f,{id:`pi-profile-endpoint`,label:`Endpoint`,value:p,onChange:m,placeholder:y(s)}),(0,_.jsx)(f,{id:`pi-profile-model`,label:`Model`,value:h,onChange:b,placeholder:`grok-composer-2.5-fast`}),(0,_.jsx)(f,{id:`pi-profile-key`,label:`API Key`,value:x,onChange:S,placeholder:o?.apiKeySet?`set (${o.apiKeyLast4||`****`})`:`empty for local proxy`}),(0,_.jsx)(`p`,{className:`settings-empty`,children:`Default: grok-composer-2.5-fast. Bare grok-composer-2.5 currently has no verified team access.`}),T?(0,_.jsx)(`p`,{className:`settings-field-error`,children:T}):null,(0,_.jsx)(`button`,{type:`button`,className:`settings-action settings-action-save`,disabled:C,onClick:()=>void D(),children:C?`Registering...`:`Register`})]})]})})}function x(e,t,n=``){let r=new Set;n&&r.add(n);for(let n of e?.discoveredModels?.[t]||[])r.add(n);let i=e?.profiles?.find(e=>e.id===t);return i?.model&&r.add(i.model),[...r]}function S(e,t=``){let n=new Set;t&&n.add(t);for(let t of e?.profiles||[])n.add(t.id);return[...n]}function C(e,t,n=!0){return{value:e,original:t,valid:n}}function w({cli:e,meta:t,original:n,value:r,setValue:i,setEntry:a,client:o,pi:s,setPi:u}){let d=`percli-${e}-models`,p=e===`pi`,[m,h]=(0,g.useState)(!1),v=r.provider||t.defaultProvider||t.providers?.[0]||`claude`,y=p?x(s,v,r.model||``):[],w=!p&&(t.providers?.length??0)>0,T=p?y:w?t.modelsByProvider?.[v]??t.models:t.models,E=w?t.effortsByProvider?.[v]??t.efforts:t.efforts;return(0,_.jsxs)(`div`,{className:`settings-percli-row`,"data-cli":e,children:[(0,_.jsx)(`h3`,{className:`settings-percli-title`,children:t.label}),(0,_.jsxs)(`div`,{className:`settings-percli-grid`,children:[p&&S(s,v).length?(0,_.jsx)(l,{id:`percli-${e}-provider`,label:`Provider`,value:v,options:S(s,v).map(e=>({value:e,label:e})),onChange:t=>{let o=x(s,t),c=o.includes(r.model||``)?r.model||``:o[0]||``;i({...r,provider:t,model:c}),a(`perCli.${e}.provider`,C(t,n.provider??`progrok`)),a(`perCli.${e}.model`,C(c,n.model??``))}}):w?(0,_.jsx)(l,{id:`percli-${e}-provider`,label:`Provider`,value:v,options:(t.providers??[]).map(e=>({value:e,label:e})),onChange:o=>{let s=t.modelsByProvider?.[o]??[],c=t.effortsByProvider?.[o]??[],l=s.includes(r.model||``)?r.model||``:s[0]||``,u=c.includes(r.effort||``)&&r.effort||``;i({...r,provider:o,model:l,effort:u}),a(`perCli.${e}.provider`,C(o,n.provider??t.defaultProvider??`claude`)),a(`perCli.${e}.model`,C(l,n.model??``)),a(`perCli.${e}.effort`,C(u,n.effort??``))}}):null,(0,_.jsxs)(`div`,{className:`settings-percli-model`,children:[p&&T.length>0?(0,_.jsx)(l,{id:`percli-${e}-model`,label:`Model`,value:r.model??T[0]??``,options:T.map(e=>({value:e,label:e})),onChange:t=>{i({...r,model:t}),a(`perCli.${e}.model`,C(t,n.model??``))}}):(0,_.jsx)(f,{id:`percli-${e}-model`,label:`Model`,value:r.model??``,onChange:t=>{i({...r,model:t}),a(`perCli.${e}.model`,C(t,n.model??``))},placeholder:T[0]??`model id`}),t.modelNote?(0,_.jsx)(`p`,{className:`settings-percli-note`,children:t.modelNote}):null,T.length>0?(0,_.jsx)(`datalist`,{id:d,children:T.map(e=>(0,_.jsx)(`option`,{value:e},e))}):null]}),E.length>0?(0,_.jsxs)(`div`,{className:`settings-percli-effort`,children:[(0,_.jsx)(l,{id:`percli-${e}-effort`,label:`Effort`,value:r.effort??``,options:[{value:``,label:`(default)`},...E.map(e=>({value:e,label:e}))],onChange:t=>{i({...r,effort:t}),a(`perCli.${e}.effort`,C(t,n.effort??``))}}),t.effortNote?(0,_.jsx)(`p`,{className:`settings-percli-note`,children:t.effortNote}):null]}):t.effortNote?(0,_.jsx)(`p`,{className:`settings-percli-note settings-percli-note--effort`,children:t.effortNote}):null,(0,_.jsx)(c,{id:`percli-${e}-fastmode`,label:`Fast mode`,value:!!r.fastMode,onChange:t=>{i({...r,fastMode:t}),a(`perCli.${e}.fastMode`,C(t,!!n.fastMode))}}),p&&o?(0,_.jsx)(`button`,{type:`button`,className:`settings-action`,onClick:()=>h(!0),children:`Settings`}):null]}),p&&o&&m?(0,_.jsx)(b,{client:o,pi:s,provider:v,model:r.model||`grok-composer-2.5-fast`,onClose:()=>h(!1),onRegistered:({provider:t,model:o,pi:s})=>{s&&u&&u(s),i({...r,provider:t,model:o}),a(`perCli.${e}.provider`,C(t,n.provider??`progrok`)),a(`perCli.${e}.model`,C(o,n.model??``))}}):null]})}function T(e){return[...e].sort((e,t)=>e===`pi`?-1:t===`pi`?1:e===`ai-e`?t===`pi`?1:-1:t===`ai-e`?e===`pi`?-1:1:0)}function E(e){let t=new Set;for(let n of Object.keys(e.activeOverrides||{}))t.add(n);for(let n of Object.keys(e.perCli||{}))t.add(n);let n={};for(let e of t)n[e]={model:``,effort:``};return{activeOverrides:n}}function D({port:e,client:t,dirty:n,registerSave:c}){let{state:l,refresh:f,setData:v}=r(t,`/api/settings`),[y,b]=(0,g.useState)({}),[x,S]=(0,g.useState)(void 0),[C,D]=(0,g.useState)([]),[O,k]=(0,g.useState)({}),[A,j]=(0,g.useState)(!1),[M,N]=(0,g.useState)(null),[P,F]=(0,g.useState)(null),I=(0,g.useCallback)(async()=>{try{let e=await t.get(`/api/cli-registry`);F(m(e&&typeof e==`object`&&`data`in e?e.data:e))}catch{F(null)}},[t]);(0,g.useEffect)(()=>{I()},[I]),(0,g.useEffect)(()=>{if(l.kind!==`ready`)return;b({...l.data.perCli||{}}),S(l.data.pi),D([...l.data.fallbackOrder||[]]);let e=l.data.perCli?.codex||{},t={};typeof e.contextWindowSize==`number`&&(t.contextWindowSize=e.contextWindowSize),typeof e.contextWindowCompactLimit==`number`&&(t.contextWindowCompactLimit=e.contextWindowCompactLimit),k(t)},[l]),(0,g.useEffect)(()=>()=>{for(let e of Array.from(n.pending.keys()))(e===`fallbackOrder`||e.startsWith(`perCli.`))&&n.remove(e)},[n]);let L=(0,g.useCallback)((e,t)=>n.set(e,t),[n]),R=(0,g.useCallback)(async()=>{let e=n.saveBundle();if(Object.keys(e).length===0)return;let r=h(e),i=await t.put(`/api/settings`,r),a=i&&typeof i==`object`&&`data`in i?i.data:i;n.clear(),v(a),b({...a.perCli||{}}),D([...a.fallbackOrder||[]]),await f(),await I()},[t,n,I,f,v]);(0,g.useEffect)(()=>{if(c)return c(R),()=>c(null)},[c,R]);let z=(0,g.useCallback)(async()=>{if(l.kind===`ready`&&window.confirm(`Reset all active overrides?`)){j(!0),N(null);try{let e=E(l.data),n=await t.put(`/api/settings`,e);v(n&&typeof n==`object`&&`data`in n?n.data:n),await f()}catch(e){N(e instanceof Error?e.message:String(e))}finally{j(!1)}}},[t,f,v,l]);if(l.kind===`loading`)return(0,_.jsx)(a,{});if(l.kind===`offline`)return(0,_.jsx)(o,{port:e});if(l.kind===`error`)return(0,_.jsx)(s,{message:l.message});let B=l.data,V=B.perCli||{},H=T(Object.keys(V)),U=V.codex||{},W=B.activeOverrides||{},G=Object.entries(W);return(0,_.jsxs)(`form`,{className:`settings-page-form`,onSubmit:e=>{e.preventDefault(),R()},children:[(0,_.jsx)(i,{title:`Model defaults`,hint:`Per-CLI defaults applied when no active override is set on the Agent page.`,children:H.length===0?(0,_.jsx)(`p`,{className:`settings-empty`,children:`No CLIs registered for this instance.`}):H.map(e=>(0,_.jsx)(w,{cli:e,meta:p(e,P),original:V[e]||{},value:y[e]||V[e]||{},setValue:t=>b({...y,[e]:t}),setEntry:L,client:t,pi:x,setPi:S},e))}),V.codex?(0,_.jsxs)(i,{title:`Codex context window`,hint:`Codex-only sliders. Other CLIs ignore these values.`,children:[(0,_.jsx)(d,{id:`model-codex-ctx`,label:`Context window size`,value:O.contextWindowSize??(typeof U.contextWindowSize==`number`?U.contextWindowSize:1e6),min:0,step:1e4,onChange:e=>{k({...O,contextWindowSize:e}),L(`perCli.codex.contextWindowSize`,{value:e,original:U.contextWindowSize??1e6,valid:Number.isFinite(e)&&e>=0})}}),(0,_.jsx)(d,{id:`model-codex-compact`,label:`Compact limit`,value:O.contextWindowCompactLimit??(typeof U.contextWindowCompactLimit==`number`?U.contextWindowCompactLimit:9e5),min:0,step:1e4,onChange:e=>{k({...O,contextWindowCompactLimit:e}),L(`perCli.codex.contextWindowCompactLimit`,{value:e,original:U.contextWindowCompactLimit??9e5,valid:Number.isFinite(e)&&e>=0})}})]}):null,(0,_.jsx)(i,{title:`Fallback order`,hint:`Order of CLIs used when the active CLI fails. Press Enter to add a chip; Backspace to remove the last.`,children:(0,_.jsx)(u,{id:`model-fallbackOrder`,label:`Fallback order`,value:C,onChange:e=>{D(e),L(`fallbackOrder`,{value:e,original:B.fallbackOrder||[],valid:!0})},placeholder:`cli name`})}),(0,_.jsxs)(i,{title:`Active overrides`,hint:`Per-session overrides applied on top of per-CLI defaults.`,children:[G.length===0?(0,_.jsx)(`p`,{className:`settings-empty`,children:`No active overrides.`}):(0,_.jsxs)(`table`,{className:`settings-overrides-table`,children:[(0,_.jsx)(`thead`,{children:(0,_.jsxs)(`tr`,{children:[(0,_.jsx)(`th`,{scope:`col`,children:`CLI`}),(0,_.jsx)(`th`,{scope:`col`,children:`Model`}),(0,_.jsx)(`th`,{scope:`col`,children:`Effort`})]})}),(0,_.jsx)(`tbody`,{children:G.map(([e,t])=>(0,_.jsxs)(`tr`,{children:[(0,_.jsx)(`td`,{children:e}),(0,_.jsx)(`td`,{children:(0,_.jsx)(`code`,{children:t?.model||`—`})}),(0,_.jsx)(`td`,{children:(0,_.jsx)(`code`,{children:t?.effort||`—`})})]},e))})]}),(0,_.jsxs)(`div`,{className:`settings-overrides-actions`,children:[(0,_.jsx)(`button`,{type:`button`,className:`settings-action settings-action-discard`,onClick:()=>void z(),disabled:A||G.length===0,children:A?`Resetting…`:`Reset overrides`}),M?(0,_.jsx)(`span`,{className:`settings-field-error`,role:`alert`,children:M}):null]})]})]})}export{E as buildResetOverridesPatch,D as default,T as orderModelCliKeys};