@open-mercato/cezar 0.9.0-pr613.619 → 0.9.0-pr613.643

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 (41) hide show
  1. package/README.md +1 -0
  2. package/dist/server/server.d.ts +7 -0
  3. package/dist/server/server.js +96 -4
  4. package/dist/server/server.js.map +1 -1
  5. package/dist/skills-update.d.ts +88 -0
  6. package/dist/skills-update.js +392 -0
  7. package/dist/skills-update.js.map +1 -0
  8. package/dist/workspace/config.d.ts +11 -0
  9. package/dist/workspace/config.js +13 -0
  10. package/dist/workspace/config.js.map +1 -1
  11. package/package.json +1 -1
  12. package/web/dist/assets/{centered-state-NF79blq7.js → centered-state-Cbkbpf8N.js} +11 -11
  13. package/web/dist/assets/circle-check-ClxqLJCI.js +1 -0
  14. package/web/dist/assets/{commit-list-CL8vSWGw.js → commit-list-BMcVHyAW.js} +1 -1
  15. package/web/dist/assets/{compare-variants-CSlZhNkL.js → compare-variants-DkoNyxFm.js} +1 -1
  16. package/web/dist/assets/{diff-view-CQHHFGFs.js → diff-view-C2upXV6G.js} +1 -1
  17. package/web/dist/assets/{dist-COA_NSud.js → dist-DwHezjuX.js} +1 -1
  18. package/web/dist/assets/{git-toolbar-DYcTQXMw.js → git-toolbar-CJ7N7_oE.js} +1 -1
  19. package/web/dist/assets/github-B6NhXGVY.js +1 -0
  20. package/web/dist/assets/index-C6QS6Ehw.js +30 -0
  21. package/web/dist/assets/index-h7UyZlMB.css +2 -0
  22. package/web/dist/assets/{project-router-CBzGUqvS.js → project-router-EIJm5QPB.js} +1 -1
  23. package/web/dist/assets/repo-git-DNycvKWr.js +1 -0
  24. package/web/dist/assets/{run-diff-CkBfZAmd.js → run-diff-CiXR4rs0.js} +2 -2
  25. package/web/dist/assets/run-header-BB71WawC.js +1 -0
  26. package/web/dist/assets/skills-CjeBe7jq.js +1 -0
  27. package/web/dist/assets/{tab-link-DTQ9FBZY.js → tab-link-ByGxILtf.js} +1 -1
  28. package/web/dist/assets/{task-changes-BJ-Z_6E-.js → task-changes-DubwrEv1.js} +1 -1
  29. package/web/dist/assets/{task-commits-BDt6B_rd.js → task-commits-BlJ0Vp2F.js} +1 -1
  30. package/web/dist/assets/{task-files-Cf8tq1Bm.js → task-files-CfbGcY1R.js} +2 -2
  31. package/web/dist/assets/task-thread-Cxk0OUb-.js +9 -0
  32. package/web/dist/assets/{use-desktop-CIaK6C4E.js → use-desktop-CJn6q9w5.js} +2 -2
  33. package/web/dist/assets/{workflows-Bwz9MmC1.js → workflows-C3LEesQu.js} +3 -3
  34. package/web/dist/index.html +5 -5
  35. package/web/dist/assets/github-Bsa5A7cy.js +0 -1
  36. package/web/dist/assets/index-3xCBmiOj.js +0 -30
  37. package/web/dist/assets/index-Mov2Rrbw.css +0 -2
  38. package/web/dist/assets/repo-git-CeOp4SuF.js +0 -1
  39. package/web/dist/assets/run-header-DynOcA_S.js +0 -1
  40. package/web/dist/assets/skills-DeNJs1CQ.js +0 -1
  41. package/web/dist/assets/task-thread-C91GpKlJ.js +0 -9
package/README.md CHANGED
@@ -434,6 +434,7 @@ Useful environment variables:
434
434
  | `CEZ_OPENCODE_BIN=/path/to/opencode` | Override which `opencode` binary is used. |
435
435
  | `CEZ_BROWSE_ROOT=~/` | Default root for **Add project → Open local folder…**. The picker cannot navigate above it; a saved workspace value overrides the environment default and must name an existing folder. |
436
436
  | `CEZ_PROJECTS_DIR=~/cezar/projects` | Default destination for **Clone from GitHub**. Saved workspace settings override it, and missing directories are created recursively. |
437
+ | `CEZ_SKILLS_AUTO_UPDATE=0` | Disable automatic checks and updates for upstream-CLI-tracked Open Mercato skill installations. On by default; a saved global Skills setting overrides this environment default. Checks are delayed, bounded, cached, and non-blocking. |
437
438
  | `GITHUB_TOKEN` | Fallback for GitHub reads/PRs when `gh` isn't authenticated. |
438
439
  | `CEZ_ENV_PASSTHROUGH=A,B` | Forward these extra host env vars to spawned agents. By default agents get a least-privilege env (safe shell/toolchain vars + the backend's own auth + `GITHUB_TOKEN` + `CEZ_*`), not your full environment — use this to add a var an agent needs. |
439
440
  | `CEZ_AGENT_ENV_FULL=1` | Escape hatch: give spawned agents the full host environment (pre-hardening behavior). Off by default; only set it if you understand that this hands every host secret to the agent process. |
@@ -1,6 +1,7 @@
1
1
  import { Hono } from 'hono';
2
2
  import { type ServerType } from '@hono/node-server';
3
3
  import { RunnerModelCatalog } from '../core/runner-model-catalog.js';
4
+ import { SkillsUpdateService } from '../skills-update.js';
4
5
  import type { RunRecord, RunStatus, RunStore } from '../runs/store.js';
5
6
  import type { RunManager } from '../workflows/run.js';
6
7
  import { type ProjectListEntry } from '../workspace/projects.js';
@@ -51,6 +52,9 @@ export interface ServerDeps {
51
52
  cloneRunner?: CloneRunner;
52
53
  /** Host-wide model discovery service. Tests inject a deterministic adapter. */
53
54
  modelCatalog?: RunnerModelCatalog;
55
+ /** Process-wide Open Mercato skills update detector. Injected in tests and
56
+ * shared by every workspace route/project; createApp owns the default. */
57
+ skillsUpdate?: SkillsUpdateService;
54
58
  }
55
59
  /** One row of the mirrored project-route table. */
56
60
  export interface ProjectRouteInfo {
@@ -122,6 +126,9 @@ export interface WorkspaceConfigResponse {
122
126
  browseRoot: string;
123
127
  /** Checkout root for GUI-cloned projects — stored as written (`~` kept). */
124
128
  projectsDir: string;
129
+ /** Stored override; null means inherit CEZ_SKILLS_AUTO_UPDATE, then true. */
130
+ skillsAutoUpdate: boolean | null;
131
+ effectiveSkillsAutoUpdate: boolean;
125
132
  resources: {
126
133
  maxParallel: number;
127
134
  memoryLimitMb: number | null;
@@ -16,6 +16,7 @@ import { WORKFLOWS_DIR, loadWorkflows } from '../workflows/load.js';
16
16
  import { QUICK_TASK_WORKFLOW, normalizeWorkflowDoc, skillStackOf, skillsToSteps, stepsIssue, workflowFileSchema, workflowStepSchema, } from '../workflows/types.js';
17
17
  import { planChain, slugify } from '../planner.js';
18
18
  import { discoverSkills } from '../skills.js';
19
+ import { SkillsUpdateConflictError, SkillsUpdateCoordinator, SkillsUpdateService } from '../skills-update.js';
19
20
  import { getTeamSkillsCached, refreshTeamSkills, waitForTeamSkills } from '../skills-remote.js';
20
21
  import { appendHandoffHeartbeat, handoffProgressExcerpt, readHandoff } from '../handoff.js';
21
22
  import { markStarted, onTodosChanged, readTodos, removeTodo, todoTaskText } from '../todos.js';
@@ -28,7 +29,7 @@ import { gatedSkillsRepos, loadConfig, resolveWorktreeRetention } from '../confi
28
29
  import { findConfigFile } from '../agent-config/catalog.js';
29
30
  import { readConfigFile, writeConfigFile } from '../agent-config/files.js';
30
31
  import { listAgentConfig } from '../agent-config/service.js';
31
- import { PROJECT_ID_RE, defaultWorkspaceConfig, loadWorkspaceConfig, mergeWriteWorkspaceConfig, } from '../workspace/config.js';
32
+ import { PROJECT_ID_RE, defaultWorkspaceConfig, effectiveSkillsAutoUpdate, loadWorkspaceConfig, mergeWriteWorkspaceConfig, } from '../workspace/config.js';
32
33
  import { allocateProjectSlug, listProjects, probeProjectStatus, registerProject, removeProject, shouldRegisterProject, } from '../workspace/projects.js';
33
34
  import { mergeWriteWorkspaceUiState, readWorkspaceUiState } from '../workspace/ui-state.js';
34
35
  import { checkoutRepo } from './checkout.js';
@@ -477,6 +478,7 @@ export function createApp(deps) {
477
478
  const modelCatalog = deps.modelCatalog ?? new RunnerModelCatalog({
478
479
  adapters: { codex: { discover: () => discoverCodexModels({ cwd: bootRoot }) } },
479
480
  });
481
+ const skillsUpdate = deps.skillsUpdate ?? new SkillsUpdateService();
480
482
  // ---- workspace boot-project identity (multi-project spec) ----------------
481
483
  // The boot flow (`initWorkspace` in src/index.ts) registers the boot repo
482
484
  // and plumbs its registry id in via `deps.bootProjectId`. Legacy callers and
@@ -1053,6 +1055,64 @@ export function createApp(deps) {
1053
1055
  const body = { removed: true, id };
1054
1056
  return c.json(body);
1055
1057
  });
1058
+ // Workspace-level by design: update state spans project and global installs,
1059
+ // but the selected registered project supplies the safe, server-owned cwd.
1060
+ const skillsUpdateInputSchema = z.object({ projectId: projectIdSchema }).strict();
1061
+ const resolveSkillsUpdateRoot = async (raw) => {
1062
+ if (!projectIdSchema.safeParse(raw).success)
1063
+ return { status: 404, error: `unknown project: ${raw}` };
1064
+ const bootId = await resolveBootProject();
1065
+ if (raw === 'default' || raw === bootId)
1066
+ return { root: bootRoot };
1067
+ const project = (await loadWorkspaceConfig()).projects.find((entry) => entry.id === raw);
1068
+ if (!project)
1069
+ return { status: 404, error: `unknown project: ${raw}` };
1070
+ if ((await probeProjectStatus(project.root)).status === 'missing') {
1071
+ return { status: 409, error: `project folder not found: ${raw}` };
1072
+ }
1073
+ return { root: project.root };
1074
+ };
1075
+ const skillsUpdateResponse = async (state) => {
1076
+ const config = await loadWorkspaceConfig();
1077
+ return { ...state, autoUpdateEnabled: effectiveSkillsAutoUpdate(config), inherited: config.skillsAutoUpdate === undefined };
1078
+ };
1079
+ app.get('/api/workspace/skills-update', async (c) => {
1080
+ const parsed = skillsUpdateInputSchema.safeParse({ projectId: c.req.query('projectId') });
1081
+ if (!parsed.success)
1082
+ return c.json({ error: 'projectId is required' }, 400);
1083
+ const resolved = await resolveSkillsUpdateRoot(parsed.data.projectId);
1084
+ if ('error' in resolved)
1085
+ return c.json({ error: resolved.error }, resolved.status);
1086
+ const state = skillsUpdate.snapshot(resolved.root);
1087
+ void skillsUpdate.check(resolved.root).catch(() => { });
1088
+ return c.json(await skillsUpdateResponse(state));
1089
+ });
1090
+ app.post('/api/workspace/skills-update/check', async (c) => {
1091
+ const parsed = skillsUpdateInputSchema.safeParse(await c.req.json().catch(() => null));
1092
+ if (!parsed.success)
1093
+ return c.json({ error: 'body must contain only projectId' }, 400);
1094
+ const resolved = await resolveSkillsUpdateRoot(parsed.data.projectId);
1095
+ if ('error' in resolved)
1096
+ return c.json({ error: resolved.error }, resolved.status);
1097
+ return c.json(await skillsUpdateResponse(await skillsUpdate.check(resolved.root, true)));
1098
+ });
1099
+ app.post('/api/workspace/skills-update/apply', async (c) => {
1100
+ const parsed = skillsUpdateInputSchema.safeParse(await c.req.json().catch(() => null));
1101
+ if (!parsed.success)
1102
+ return c.json({ error: 'body must contain only projectId' }, 400);
1103
+ const resolved = await resolveSkillsUpdateRoot(parsed.data.projectId);
1104
+ if ('error' in resolved)
1105
+ return c.json({ error: resolved.error }, resolved.status);
1106
+ try {
1107
+ return c.json(await skillsUpdateResponse(await skillsUpdate.update(resolved.root, true)));
1108
+ }
1109
+ catch (error) {
1110
+ if (error instanceof SkillsUpdateConflictError) {
1111
+ return c.json({ error: 'another skills update operation is running', state: await skillsUpdateResponse(skillsUpdate.snapshot(resolved.root)) }, 409);
1112
+ }
1113
+ throw error;
1114
+ }
1115
+ });
1056
1116
  // ---- GUI clone (multi-project spec, step 4.3) ----------------------------
1057
1117
  // "Add project → Clone from GitHub": clone into the checkout root, then
1058
1118
  // register the result through `registerFolder` above (same guards, same
@@ -1110,6 +1170,8 @@ export function createApp(deps) {
1110
1170
  const workspaceConfigBody = (config) => ({
1111
1171
  browseRoot: config.browseRoot,
1112
1172
  projectsDir: config.projectsDir,
1173
+ skillsAutoUpdate: config.skillsAutoUpdate ?? null,
1174
+ effectiveSkillsAutoUpdate: effectiveSkillsAutoUpdate(config),
1113
1175
  resources: {
1114
1176
  maxParallel: config.resources.maxParallel,
1115
1177
  memoryLimitMb: config.resources.memoryLimitMb,
@@ -1123,6 +1185,7 @@ export function createApp(deps) {
1123
1185
  const workspaceConfigUpdateSchema = z.object({
1124
1186
  browseRoot: z.string().trim().min(1).max(4096).optional(),
1125
1187
  projectsDir: z.string().trim().min(1).max(4096).optional(),
1188
+ skillsAutoUpdate: z.boolean().nullable().optional(),
1126
1189
  resources: z
1127
1190
  .object({
1128
1191
  maxParallel: z.number().int().min(1).max(16).optional(),
@@ -1136,7 +1199,7 @@ export function createApp(deps) {
1136
1199
  if (!parsed.success) {
1137
1200
  return c.json({ error: parsed.error.issues.map((i) => i.message).join('; ') }, 400);
1138
1201
  }
1139
- const { browseRoot, projectsDir, resources } = parsed.data;
1202
+ const { browseRoot, projectsDir, skillsAutoUpdate, resources } = parsed.data;
1140
1203
  for (const [configuredRoot, create] of [
1141
1204
  [browseRoot, false],
1142
1205
  [projectsDir, true],
@@ -1175,6 +1238,10 @@ export function createApp(deps) {
1175
1238
  config.browseRoot = browseRoot;
1176
1239
  if (projectsDir !== undefined)
1177
1240
  config.projectsDir = projectsDir;
1241
+ if (skillsAutoUpdate === null)
1242
+ delete config.skillsAutoUpdate;
1243
+ else if (skillsAutoUpdate !== undefined)
1244
+ config.skillsAutoUpdate = skillsAutoUpdate;
1178
1245
  if (resources?.maxParallel !== undefined)
1179
1246
  config.resources.maxParallel = resources.maxParallel;
1180
1247
  if (resources?.memoryLimitMb !== undefined)
@@ -2836,17 +2903,42 @@ export function createApp(deps) {
2836
2903
  return app;
2837
2904
  }
2838
2905
  export function startServer(deps, port) {
2839
- const app = createApp(deps);
2906
+ const workspaceEvents = deps.workspaceEvents ?? new WorkspaceEventBus();
2907
+ const skillsUpdate = deps.skillsUpdate ?? new SkillsUpdateService({ invalidateCatalog: refreshTeamSkills });
2908
+ const app = createApp({ ...deps, workspaceEvents, skillsUpdate });
2840
2909
  // SECURITY: default to loopback. This server executes agents locally and its endpoints are
2841
2910
  // same-origin-trusted (only /api/health is CORS-open); binding to a non-loopback host would
2842
2911
  // expose an agent-executing box to the network. `bindHost` exists only for a deliberate
2843
2912
  // hosted/VPS deployment (which also flips CEZ_REMOTE to gate the local-handoff endpoints) —
2844
2913
  // src/index.ts never passes it, so the loopback guarantee holds for the normal CLI.
2845
- return serve({
2914
+ const server = serve({
2846
2915
  fetch: app.fetch,
2847
2916
  port,
2848
2917
  hostname: deps.bindHost ?? '127.0.0.1',
2849
2918
  });
2919
+ const coordinator = new SkillsUpdateCoordinator(skillsUpdate, async () => effectiveSkillsAutoUpdate(await loadWorkspaceConfig()));
2920
+ const unsubscribe = workspaceEvents.on((event, data) => {
2921
+ if (event === 'project-added') {
2922
+ const project = data.project;
2923
+ if (project && typeof project.id === 'string' && typeof project.root === 'string' && project.status !== 'missing') {
2924
+ coordinator.add(project.id, project.root);
2925
+ }
2926
+ }
2927
+ else if (event === 'project-removed') {
2928
+ const id = data.id;
2929
+ if (typeof id === 'string')
2930
+ coordinator.remove(id);
2931
+ }
2932
+ });
2933
+ server.once('listening', () => {
2934
+ void listProjects().then((projects) => {
2935
+ const all = projects.some((project) => project.root === deps.repoRoot)
2936
+ ? projects : [{ id: deps.bootProjectId ?? 'default', root: deps.repoRoot, status: 'ok' }, ...projects];
2937
+ coordinator.start(all);
2938
+ }).catch(() => undefined);
2939
+ });
2940
+ server.once('close', () => { unsubscribe(); coordinator.stop(); });
2941
+ return server;
2850
2942
  }
2851
2943
  /** The bare hostname of a `Host` header — see `normalizeHostname`. `''` when the
2852
2944
  * header is absent, which no allowlist matches. */