@nextclaw/kernel 0.15.1 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4404,7 +4404,7 @@ var AppPackageDependencyCoordinator = class {
4404
4404
  });
4405
4405
  resolveStoredProviderIds = async (target) => await this.dependencyService.resolveStoredProviderIds(target);
4406
4406
  listBindings = async (target) => await this.dependencyService.listBindings(target);
4407
- listActiveComponentSourcesWithDiagnostics = async () => {
4407
+ loadActiveComponentSourcesWithDiagnostics = async () => {
4408
4408
  const records = await this.params.registryService.listApps();
4409
4409
  const results = await Promise.all(records.filter((record) => record.enabled).map(async (record) => {
4410
4410
  try {
@@ -4542,6 +4542,54 @@ var AppPackageDependencyCoordinator = class {
4542
4542
  };
4543
4543
  };
4544
4544
  //#endregion
4545
+ //#region src/services/app-package-component-catalog.service.ts
4546
+ const EMPTY_COMPONENT_CATALOG = {
4547
+ sources: [],
4548
+ unavailablePackages: []
4549
+ };
4550
+ /**
4551
+ * Owns the verified projection of active package components.
4552
+ *
4553
+ * Expensive package integrity checks happen while refreshing this catalog at
4554
+ * lifecycle boundaries. Product reads consume the last complete snapshot and
4555
+ * never trigger a package-tree scan after initialization.
4556
+ */
4557
+ var AppPackageComponentCatalogService = class {
4558
+ snapshot = EMPTY_COMPONENT_CATALOG;
4559
+ requestedRevision = 0;
4560
+ appliedRevision = 0;
4561
+ refreshPromise;
4562
+ constructor(load) {
4563
+ this.load = load;
4564
+ }
4565
+ read = async () => {
4566
+ if (this.appliedRevision === 0) await this.refresh();
4567
+ return cloneCatalog(this.snapshot);
4568
+ };
4569
+ refresh = async () => {
4570
+ this.requestedRevision += 1;
4571
+ this.refreshPromise ??= this.runRefreshLoop();
4572
+ await this.refreshPromise;
4573
+ };
4574
+ runRefreshLoop = async () => {
4575
+ try {
4576
+ while (this.appliedRevision < this.requestedRevision) {
4577
+ const targetRevision = this.requestedRevision;
4578
+ this.snapshot = cloneCatalog(await this.load());
4579
+ this.appliedRevision = targetRevision;
4580
+ }
4581
+ } finally {
4582
+ this.refreshPromise = void 0;
4583
+ }
4584
+ };
4585
+ };
4586
+ function cloneCatalog(catalog) {
4587
+ return {
4588
+ sources: catalog.sources.map((source) => ({ ...source })),
4589
+ unavailablePackages: catalog.unavailablePackages.map((diagnostic) => ({ ...diagnostic }))
4590
+ };
4591
+ }
4592
+ //#endregion
4545
4593
  //#region src/services/app-package-document-access.service.ts
4546
4594
  var AppPackageDocumentAccessService = class {
4547
4595
  constructor(params) {
@@ -4709,6 +4757,7 @@ var AppPackageManager = class {
4709
4757
  hostTargetService = new AppPackageHostTargetService();
4710
4758
  presentationService = new AppPackagePresentationService();
4711
4759
  dependencyCoordinator;
4760
+ componentCatalog;
4712
4761
  runtimeActivationService = new AppPackageRuntimeActivationService();
4713
4762
  registryService;
4714
4763
  grantService;
@@ -4736,6 +4785,7 @@ var AppPackageManager = class {
4736
4785
  listCapabilityProviders: async () => await this.runtimeHooks.listCapabilityProviders(),
4737
4786
  resolveSecurity: this.resolveSecurity
4738
4787
  });
4788
+ this.componentCatalog = new AppPackageComponentCatalogService(this.dependencyCoordinator.loadActiveComponentSourcesWithDiagnostics);
4739
4789
  this.readinessManager = new AppPackageReadinessManager({
4740
4790
  manifestService: this.manifestService,
4741
4791
  installationService: this.installationService,
@@ -4759,7 +4809,10 @@ var AppPackageManager = class {
4759
4809
  ...hooks
4760
4810
  };
4761
4811
  };
4762
- start = async () => await this.ensureBuiltInPackages();
4812
+ start = async () => {
4813
+ await this.ensureBuiltInPackages();
4814
+ await this.componentCatalog.refresh();
4815
+ };
4763
4816
  listPackages = async (options = {}) => {
4764
4817
  const [records, providers] = await Promise.all([this.registryService.listApps(), this.runtimeHooks.listCapabilityProviders()]);
4765
4818
  const entries = (await Promise.all(records.map(async (record) => await this.installationService.withAppOperation(record.appId, async () => await this.readListedPackageView(record.appId, options.includeStorageUsage !== false, providers))))).filter((entry) => entry !== void 0);
@@ -4784,9 +4837,21 @@ var AppPackageManager = class {
4784
4837
  };
4785
4838
  inspectDependencies = async (appId) => await this.installationService.withAppOperation(appId, async () => await this.dependencyCoordinator.inspect(appId));
4786
4839
  verifyDependencies = async (appId) => await this.installationService.withAppOperation(appId, async () => await this.dependencyCoordinator.verify(appId));
4787
- setupDependencies = async (appId) => await this.dependencyCoordinator.setup(appId);
4788
- bindDependency = async (appId, input) => await this.dependencyCoordinator.bind(appId, input);
4789
- unbindDependency = async (appId, input) => await this.dependencyCoordinator.unbind(appId, input);
4840
+ setupDependencies = async (appId) => {
4841
+ const result = await this.dependencyCoordinator.setup(appId);
4842
+ await this.componentCatalog.refresh();
4843
+ return result;
4844
+ };
4845
+ bindDependency = async (appId, input) => {
4846
+ const result = await this.dependencyCoordinator.bind(appId, input);
4847
+ await this.componentCatalog.refresh();
4848
+ return result;
4849
+ };
4850
+ unbindDependency = async (appId, input) => {
4851
+ const result = await this.dependencyCoordinator.unbind(appId, input);
4852
+ await this.componentCatalog.refresh();
4853
+ return result;
4854
+ };
4790
4855
  inspectSecrets = async (appId) => await this.installationService.withAppOperation(appId, async () => await this.readinessManager.inspectSecrets(await this.installationService.info(appId), false));
4791
4856
  verifySecrets = async (appId) => await this.installationService.withAppOperation(appId, async () => await this.readinessManager.inspectSecrets(await this.installationService.info(appId), true));
4792
4857
  bindSecret = async (appId, input) => await this.installationService.withAppOperation(appId, async () => {
@@ -4810,7 +4875,7 @@ var AppPackageManager = class {
4810
4875
  grantDocumentAccess = async (appId, input) => await this.documentAccessService.grant(appId, input);
4811
4876
  revokeDocumentAccess = async (appId, scopeId) => await this.documentAccessService.revoke(appId, scopeId);
4812
4877
  listActiveComponentSources = async () => (await this.listActiveComponentSourcesWithDiagnostics()).sources;
4813
- listActiveComponentSourcesWithDiagnostics = async () => await this.dependencyCoordinator.listActiveComponentSourcesWithDiagnostics();
4878
+ listActiveComponentSourcesWithDiagnostics = async () => await this.componentCatalog.read();
4814
4879
  listOperations = async () => await this.operationManager.list();
4815
4880
  startOperation = async (input) => {
4816
4881
  await this.ensureBuiltInPackages();
@@ -4822,6 +4887,7 @@ var AppPackageManager = class {
4822
4887
  onProgress
4823
4888
  });
4824
4889
  if (await this.isBuiltInAppId(result.appId)) await this.registryService.setBuiltInSuppressed(result.appId, false);
4890
+ await this.componentCatalog.refresh();
4825
4891
  return await this.getPackage(result.appId);
4826
4892
  };
4827
4893
  enable = async (appId) => {
@@ -4849,6 +4915,7 @@ var AppPackageManager = class {
4849
4915
  });
4850
4916
  throw error;
4851
4917
  }
4918
+ await this.componentCatalog.refresh();
4852
4919
  return await this.getPackage(appId);
4853
4920
  });
4854
4921
  };
@@ -4859,6 +4926,7 @@ var AppPackageManager = class {
4859
4926
  await this.dependencyCoordinator.assertNoEnabledDependents(app);
4860
4927
  await this.runtimeHooks.beforeDeactivate(this.toComponentSources(app));
4861
4928
  await this.installationService.setEnabled(appId, false);
4929
+ await this.componentCatalog.refresh();
4862
4930
  return await this.getPackage(appId);
4863
4931
  });
4864
4932
  };
@@ -4888,6 +4956,7 @@ var AppPackageManager = class {
4888
4956
  }
4889
4957
  activated = (await this.installationService.rollback(appId, result.version)).rolledBack;
4890
4958
  if (current.enabled) await this.runtimeHooks.afterActivate(this.toComponentSources(candidate));
4959
+ await this.componentCatalog.refresh();
4891
4960
  return {
4892
4961
  package: await this.getPackage(appId),
4893
4962
  result
@@ -4932,6 +5001,7 @@ var AppPackageManager = class {
4932
5001
  }
4933
5002
  result = await this.installationService.rollback(appId, version);
4934
5003
  if (current.enabled) await this.runtimeHooks.afterActivate(this.toComponentSources(candidate));
5004
+ await this.componentCatalog.refresh();
4935
5005
  return {
4936
5006
  package: await this.getPackage(appId),
4937
5007
  result
@@ -4957,7 +5027,9 @@ var AppPackageManager = class {
4957
5027
  try {
4958
5028
  const sources = this.toComponentSources(current);
4959
5029
  rollbackRuntimeState = await this.runtimeHooks.beforeUninstall(sources) || void 0;
4960
- return await this.installationService.uninstall(appId, purgeData);
5030
+ const result = await this.installationService.uninstall(appId, purgeData);
5031
+ await this.componentCatalog.refresh();
5032
+ return result;
4961
5033
  } catch (error) {
4962
5034
  const recoveryErrors = [];
4963
5035
  if (rollbackRuntimeState) try {
@@ -10696,7 +10768,7 @@ var PanelAppPackageStateManager = class {
10696
10768
  try {
10697
10769
  return await this.params.sourceService.resolveSource(this.params.getPanelsPath(), id);
10698
10770
  } catch (error) {
10699
- if (!isPanelAppError(error) || error.code !== "PANEL_APP_NOT_FOUND") throw error;
10771
+ if (!isPanelAppError(error) || error.code !== "PANEL_APP_NOT_FOUND" && error.code !== "PANEL_APP_INVALID_ID") throw error;
10700
10772
  }
10701
10773
  const match = (await this.listSources()).find(({ source, packageSource }) => packageSource && (encodePanelAppId(source.sourceName) === id || packageSource.id === id));
10702
10774
  if (!match) throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
@@ -10707,16 +10779,34 @@ var PanelAppPackageStateManager = class {
10707
10779
  };
10708
10780
  readContentSourceByIdOrAppId = async (id) => {
10709
10781
  const panelsPath = this.params.getPanelsPath();
10710
- for (const { source, packageSource } of await this.listSources()) {
10711
- const manifest = source.manifest ?? parsePanelAppManifest(await readFile(source.entryPath, "utf8"));
10712
- if (encodePanelAppId(source.sourceName) !== id && resolvePanelAppAppId(source, manifest) !== id && packageSource?.id !== id) continue;
10713
- return await readPanelAppContentSourceByIdOrPath({
10714
- createAssetBaseHref: this.params.createAssetBaseHref,
10715
- id,
10716
- panelsPath,
10717
- sourcePath: source.sourcePath,
10718
- sourceService: this.params.sourceService
10719
- });
10782
+ const { source } = await this.resolveSourceByIdOrAppId(id);
10783
+ return await readPanelAppContentSourceByIdOrPath({
10784
+ createAssetBaseHref: this.params.createAssetBaseHref,
10785
+ id,
10786
+ panelsPath,
10787
+ sourcePath: source.sourcePath,
10788
+ sourceService: this.params.sourceService
10789
+ });
10790
+ };
10791
+ resolveSourceByIdOrAppId = async (id) => {
10792
+ const panelsPath = this.params.getPanelsPath();
10793
+ try {
10794
+ return await this.toResolvedTarget(await this.params.sourceService.resolveSource(panelsPath, id));
10795
+ } catch (error) {
10796
+ if (!isPanelAppError(error) || error.code !== "PANEL_APP_NOT_FOUND" && error.code !== "PANEL_APP_INVALID_ID") throw error;
10797
+ }
10798
+ const packageSources = (await this.listPackageComponentSources()).filter((component) => component.kind === "panel");
10799
+ const exactPackageSource = packageSources.find((component) => component.id === id);
10800
+ if (exactPackageSource) return await this.toResolvedTarget(await this.params.sourceService.resolveSourcePath(exactPackageSource.sourcePath), exactPackageSource);
10801
+ const workspaceSources = await this.params.sourceService.listSources(panelsPath);
10802
+ for (const source of workspaceSources) {
10803
+ const target = await this.toResolvedTarget(source);
10804
+ if (resolvePanelAppAppId(source, target.manifest) === id) return target;
10805
+ }
10806
+ for (const packageSource of packageSources) {
10807
+ const source = await this.params.sourceService.resolveSourcePath(packageSource.sourcePath);
10808
+ const target = await this.toResolvedTarget(source, packageSource);
10809
+ if (encodePanelAppId(source.sourceName) === id || resolvePanelAppAppId(source, target.manifest) === id) return target;
10720
10810
  }
10721
10811
  throw new PanelAppError("PANEL_APP_NOT_FOUND", "panel app not found");
10722
10812
  };
@@ -10795,6 +10885,11 @@ var PanelAppPackageStateManager = class {
10795
10885
  return errors;
10796
10886
  };
10797
10887
  listPackageComponentSources = async () => await this.params.listPackageComponentSources?.() ?? [];
10888
+ toResolvedTarget = async (source, packageSource) => ({
10889
+ source,
10890
+ packageSource,
10891
+ manifest: source.manifest ?? parsePanelAppManifest(await readFile(source.entryPath, "utf8"))
10892
+ });
10798
10893
  };
10799
10894
  //#endregion
10800
10895
  //#region src/utils/panel-app-time.utils.ts
@@ -11964,6 +12059,12 @@ var PanelAppManager = class {
11964
12059
  unavailablePackages: await this.params.listPackageComponentDiagnostics?.() ?? []
11965
12060
  };
11966
12061
  };
12062
+ getPanelApp = async (id) => {
12063
+ const panelsPath = this.getPanelsPath(this.getWorkspacePath());
12064
+ const resolved = await this.packageStateManager.resolveSourceByIdOrAppId(id);
12065
+ const appState = await this.createStateStore(panelsPath).load();
12066
+ return await this.entryPresenter.build(resolved.source, appState.apps[encodePanelAppId(resolved.source.sourceName)] ?? {}, resolved.packageSource, appState.mainSidebarAppIds);
12067
+ };
11967
12068
  getPanelAppContent = async (id, sourcePath) => {
11968
12069
  try {
11969
12070
  const resolved = sourcePath ? await readPanelAppContentSourceByIdOrPath({
@@ -12101,19 +12202,19 @@ var PanelAppManager = class {
12101
12202
  return await this.agentBridgeService.grantAgentCapability(bridgeSession, capability);
12102
12203
  };
12103
12204
  updatePanelAppPreferences = async (id, preferences) => {
12104
- const fileName = await this.resolvePanelAppFileName(id);
12105
12205
  const panelsPath = this.getPanelsPath(this.getWorkspacePath());
12106
- const source = await this.packageStateManager.resolveSource(encodePanelAppId(fileName));
12107
- const packageSource = await this.packageStateManager.findPackageSourceBySourceName(fileName);
12108
- const appId = resolvePanelAppAppId(source, source.manifest ?? parsePanelAppManifest(await readFile(source.entryPath, "utf8")));
12206
+ const resolved = await this.packageStateManager.resolveSourceByIdOrAppId(id);
12207
+ const fileName = resolved.source.sourceName;
12208
+ const appId = resolvePanelAppAppId(resolved.source, resolved.manifest);
12109
12209
  const result = await this.createStateStore(panelsPath).updatePreferences(encodePanelAppId(fileName), appId, preferences);
12110
- return await this.entryPresenter.build(source, result.entry, packageSource, result.mainSidebarAppIds);
12210
+ return await this.entryPresenter.build(resolved.source, result.entry, resolved.packageSource, result.mainSidebarAppIds);
12111
12211
  };
12112
12212
  recordPanelAppOpened = async (id) => {
12113
- const fileName = await this.resolvePanelAppFileName(id);
12114
12213
  const panelsPath = this.getPanelsPath(this.getWorkspacePath());
12214
+ const resolved = await this.packageStateManager.resolveSourceByIdOrAppId(id);
12215
+ const fileName = resolved.source.sourceName;
12115
12216
  const result = await this.createStateStore(panelsPath).recordOpened(encodePanelAppId(fileName));
12116
- return await this.entryPresenter.build(await this.packageStateManager.resolveSource(encodePanelAppId(fileName)), result.entry, await this.packageStateManager.findPackageSourceBySourceName(fileName), result.mainSidebarAppIds);
12217
+ return await this.entryPresenter.build(resolved.source, result.entry, resolved.packageSource, result.mainSidebarAppIds);
12117
12218
  };
12118
12219
  deletePanelApp = async (id) => {
12119
12220
  const panelsPath = this.getPanelsPath(this.getWorkspacePath());
@@ -12146,9 +12247,6 @@ var PanelAppManager = class {
12146
12247
  return `${PANEL_APP_TOKENIZED_ASSET_BASE_PATH}/${encodeURIComponent(token)}/`;
12147
12248
  };
12148
12249
  createStateStore = (panelsPath) => new PanelAppStateStore(panelsPath);
12149
- resolvePanelAppFileName = async (id) => {
12150
- return (await this.packageStateManager.resolveSource(id)).sourceName;
12151
- };
12152
12250
  assertCanActivatePackageComponents = async (components) => await this.packageStateManager.assertCanActivate(components);
12153
12251
  deactivatePackageComponents = (components) => this.packageStateManager.deactivate(components);
12154
12252
  preparePackageComponentDeactivation = (components) => {
@@ -20329,7 +20427,7 @@ var ObservationManager = class {
20329
20427
  };
20330
20428
  //#endregion
20331
20429
  //#region src/features/projects/stores/project.store.ts
20332
- const PROJECT_STORE_VERSION = 2;
20430
+ const PROJECT_STORE_VERSION = 3;
20333
20431
  function createProjectId() {
20334
20432
  return randomBytes(9).toString("base64url");
20335
20433
  }
@@ -20368,17 +20466,18 @@ var ProjectStore = class {
20368
20466
  throw error;
20369
20467
  }
20370
20468
  if (storeFile.version === PROJECT_STORE_VERSION) return false;
20371
- await this.save(storeFile.projects.map((project) => ({
20469
+ await this.save(storeFile.version === 1 ? storeFile.projects.map((project) => ({
20372
20470
  id: createProjectId(),
20373
20471
  ...project
20374
- })));
20472
+ })) : storeFile.projects);
20375
20473
  return true;
20376
20474
  };
20377
- save = async (projects) => {
20475
+ save = async (projects, removedProjects = []) => {
20378
20476
  const tempPath = `${this.storePath}.${randomUUID()}.tmp`;
20379
20477
  const storeFile = {
20380
20478
  version: PROJECT_STORE_VERSION,
20381
- projects
20479
+ projects,
20480
+ removedProjects
20382
20481
  };
20383
20482
  await mkdir(dirname(this.storePath), { recursive: true });
20384
20483
  try {
@@ -20389,19 +20488,57 @@ var ProjectStore = class {
20389
20488
  throw error;
20390
20489
  }
20391
20490
  };
20491
+ remove = async (projectId) => {
20492
+ const storeFile = await this.readStoreFile();
20493
+ const project = storeFile.projects.find((entry) => entry.id === projectId);
20494
+ if (!project) return null;
20495
+ await this.save(storeFile.projects.filter((entry) => entry.id !== projectId), [...storeFile.removedProjects.filter((entry) => entry.id !== projectId), project]);
20496
+ return structuredClone(project);
20497
+ };
20498
+ restoreByRootPath = async (rootPath, updatedAt) => {
20499
+ const storeFile = await this.readStoreFile();
20500
+ const project = storeFile.removedProjects.find((entry) => entry.rootPath === rootPath);
20501
+ if (!project) return null;
20502
+ const restored = {
20503
+ ...project,
20504
+ updatedAt
20505
+ };
20506
+ await this.save([...storeFile.projects, restored], storeFile.removedProjects.filter((entry) => entry.id !== project.id));
20507
+ return structuredClone(restored);
20508
+ };
20509
+ isRemovedRootPath = async (rootPath) => (await this.readStoreFile()).removedProjects.some((entry) => entry.rootPath === rootPath);
20510
+ readStoreFile = async () => {
20511
+ try {
20512
+ return this.parseStoreFile(await readFile(this.storePath, "utf8"));
20513
+ } catch (error) {
20514
+ if (this.isMissingFileError(error)) return {
20515
+ version: PROJECT_STORE_VERSION,
20516
+ projects: [],
20517
+ removedProjects: []
20518
+ };
20519
+ if (error instanceof SyntaxError) throw new ProjectStoreError("project registry contains invalid JSON");
20520
+ throw error;
20521
+ }
20522
+ };
20392
20523
  parseStoreFile = (source) => {
20393
20524
  const value = this.parseStoredFile(source);
20394
- if (value.version !== PROJECT_STORE_VERSION || !value.projects.every(this.isProjectRecord)) throw new ProjectStoreError("project registry has an unsupported structure");
20525
+ if (value.version !== PROJECT_STORE_VERSION || !value.projects.every(this.isProjectRecord) || !value.removedProjects.every(this.isProjectRecord)) throw new ProjectStoreError("project registry has an unsupported structure");
20395
20526
  return {
20396
20527
  version: PROJECT_STORE_VERSION,
20397
- projects: value.projects.map((project) => structuredClone(project))
20528
+ projects: value.projects.map((project) => structuredClone(project)),
20529
+ removedProjects: value.removedProjects.map((project) => structuredClone(project))
20398
20530
  };
20399
20531
  };
20400
20532
  parseStoredFile = (source) => {
20401
20533
  const value = JSON.parse(source);
20402
20534
  if (!this.isRecord(value) || !Array.isArray(value.projects)) throw new ProjectStoreError("project registry has an unsupported structure");
20403
- if (value.version === PROJECT_STORE_VERSION && value.projects.every(this.isProjectRecord)) return {
20535
+ if (value.version === PROJECT_STORE_VERSION && Array.isArray(value.removedProjects) && value.projects.every(this.isProjectRecord) && value.removedProjects.every(this.isProjectRecord)) return {
20404
20536
  version: PROJECT_STORE_VERSION,
20537
+ projects: value.projects.map((project) => structuredClone(project)),
20538
+ removedProjects: value.removedProjects.map((project) => structuredClone(project))
20539
+ };
20540
+ if (value.version === 2 && value.projects.every(this.isProjectRecord)) return {
20541
+ version: 2,
20405
20542
  projects: value.projects.map((project) => structuredClone(project))
20406
20543
  };
20407
20544
  if (value.version === 1 && value.projects.every(this.isLegacyProjectRecord)) return {
@@ -20487,7 +20624,15 @@ var ProjectManager = class {
20487
20624
  name,
20488
20625
  rootPath,
20489
20626
  template
20490
- });
20627
+ }, { restoreRemoved: true });
20628
+ };
20629
+ addExistingProject = async (rootPath, name) => {
20630
+ const canonicalPath = await this.resolveExistingProjectRoot(rootPath);
20631
+ if (!canonicalPath) return null;
20632
+ return await this.upsertProject({
20633
+ name: name === void 0 ? basename(canonicalPath) : this.normalizeName(name),
20634
+ rootPath: canonicalPath
20635
+ }, { restoreRemoved: true });
20491
20636
  };
20492
20637
  registerExistingProject = async (rootPath, name) => {
20493
20638
  const canonicalPath = await this.resolveExistingProjectRoot(rootPath);
@@ -20495,7 +20640,13 @@ var ProjectManager = class {
20495
20640
  return await this.upsertProject({
20496
20641
  name: name === void 0 ? basename(canonicalPath) : this.normalizeName(name),
20497
20642
  rootPath: canonicalPath
20498
- });
20643
+ }, { restoreRemoved: true });
20644
+ };
20645
+ removeProject = async (projectId, confirmProjectId) => {
20646
+ if (confirmProjectId !== projectId) throw new ProjectError("PROJECT_REMOVE_CONFIRMATION_MISMATCH", "project removal confirmation must exactly match the project id");
20647
+ const removed = await this.store.remove(projectId);
20648
+ if (!removed) throw new ProjectError("PROJECT_NOT_FOUND", "project was not found");
20649
+ return removed;
20499
20650
  };
20500
20651
  normalizeSessionProjectRoot = async (value) => {
20501
20652
  return (await this.normalizeSessionProjectContext(value))?.rootPath ?? null;
@@ -20529,18 +20680,30 @@ var ProjectManager = class {
20529
20680
  for (const projectRoot of projectRoots) {
20530
20681
  if (projectRoot == null || typeof projectRoot === "string" && !projectRoot.trim()) continue;
20531
20682
  try {
20532
- await this.registerExistingProject(projectRoot);
20683
+ const canonicalPath = await this.resolveExistingProjectRoot(projectRoot);
20684
+ if (!canonicalPath || await this.store.isRemovedRootPath(canonicalPath)) continue;
20685
+ await this.upsertProject({
20686
+ name: basename(canonicalPath),
20687
+ rootPath: canonicalPath
20688
+ });
20533
20689
  } catch (error) {
20534
20690
  const message = error instanceof Error ? error.message : String(error);
20535
20691
  console.warn(`[project-manager] skipped historical project root: ${message}`);
20536
20692
  }
20537
20693
  }
20538
20694
  };
20539
- upsertProject = async (input) => {
20695
+ upsertProject = async (input, options = {}) => {
20540
20696
  const projects = await this.store.list();
20541
20697
  const existing = projects.find((project) => project.rootPath === input.rootPath);
20542
20698
  if (existing) return existing;
20543
20699
  const now = (/* @__PURE__ */ new Date()).toISOString();
20700
+ if (options.restoreRemoved) {
20701
+ const restored = await this.store.restoreByRootPath(input.rootPath, now);
20702
+ if (restored) {
20703
+ await this.options.onProjectRegistered?.(structuredClone(restored));
20704
+ return restored;
20705
+ }
20706
+ }
20544
20707
  const project = {
20545
20708
  id: createProjectId(),
20546
20709
  name: input.name,
@@ -20854,6 +21017,95 @@ function toArtifactLink(row) {
20854
21017
  };
20855
21018
  }
20856
21019
  //#endregion
21020
+ //#region src/features/projects/stores/project-work-query.store.ts
21021
+ var ProjectWorkQueryStore = class {
21022
+ constructor(db, toItem) {
21023
+ this.db = db;
21024
+ this.toItem = toItem;
21025
+ }
21026
+ listItems = (params) => {
21027
+ const { cursor, includeDeleted, limit, projectId, stateId } = params;
21028
+ const conditions = ["items.project_id = ?"];
21029
+ const values = [projectId];
21030
+ if (!includeDeleted) conditions.push("items.deleted_at IS NULL");
21031
+ if (stateId) {
21032
+ conditions.push("items.state_id = ?");
21033
+ values.push(stateId);
21034
+ }
21035
+ const total = Number(this.db().prepare(`SELECT COUNT(*) AS count FROM project_work_items AS items
21036
+ WHERE ${conditions.join(" AND ")}`).get(...values).count);
21037
+ if (cursor) {
21038
+ conditions.push("(items.updated_at < ? OR (items.updated_at = ? AND items.id < ?))");
21039
+ values.push(cursor.updatedAt, cursor.updatedAt, cursor.id);
21040
+ }
21041
+ const rows = this.db().prepare(`SELECT items.*,
21042
+ (SELECT COUNT(*) FROM project_work_artifact_links AS links
21043
+ WHERE links.project_id = items.project_id AND links.work_item_id = items.id) AS artifact_count
21044
+ FROM project_work_items AS items
21045
+ WHERE ${conditions.join(" AND ")}
21046
+ ORDER BY items.updated_at DESC, items.id DESC
21047
+ LIMIT ?`).all(...values, limit + 1);
21048
+ return {
21049
+ items: rows.slice(0, limit).map((row) => ({
21050
+ ...this.toItem(row),
21051
+ artifactCount: Number(row.artifact_count)
21052
+ })),
21053
+ hasMore: rows.length > limit,
21054
+ total
21055
+ };
21056
+ };
21057
+ summarizeItems = (projectId) => {
21058
+ const row = this.db().prepare(`SELECT
21059
+ COUNT(*) AS total,
21060
+ SUM(CASE WHEN states.category NOT IN ('completed', 'canceled') THEN 1 ELSE 0 END) AS active,
21061
+ SUM(CASE WHEN states.category = 'completed' THEN 1 ELSE 0 END) AS completed,
21062
+ SUM(CASE WHEN items.attention != 'none' THEN 1 ELSE 0 END) AS attention,
21063
+ MAX(items.updated_at) AS updated_at
21064
+ FROM project_work_items AS items
21065
+ JOIN project_work_states AS states
21066
+ ON states.project_id = items.project_id AND states.id = items.state_id
21067
+ WHERE items.project_id = ? AND items.deleted_at IS NULL`).get(projectId);
21068
+ return {
21069
+ total: Number(row.total),
21070
+ active: Number(row.active ?? 0),
21071
+ completed: Number(row.completed ?? 0),
21072
+ attention: Number(row.attention ?? 0),
21073
+ updatedAt: row.updated_at
21074
+ };
21075
+ };
21076
+ listRecentArtifacts = (params) => {
21077
+ const { cursor, limit, projectId } = params;
21078
+ const total = Number(this.db().prepare("SELECT COUNT(DISTINCT path) AS count FROM project_work_artifact_links WHERE project_id = ?").get(projectId).count);
21079
+ const cursorCondition = cursor ? "AND (created_at < ? OR (created_at = ? AND id < ?))" : "";
21080
+ const rows = this.db().prepare(`WITH ranked AS (
21081
+ SELECT links.id, links.path, links.label, links.work_item_id,
21082
+ items.title AS work_item_title, links.created_at,
21083
+ ROW_NUMBER() OVER (
21084
+ PARTITION BY links.path
21085
+ ORDER BY links.created_at DESC, links.id DESC
21086
+ ) AS path_rank
21087
+ FROM project_work_artifact_links AS links
21088
+ JOIN project_work_items AS items
21089
+ ON items.project_id = links.project_id AND items.id = links.work_item_id
21090
+ WHERE links.project_id = ?
21091
+ )
21092
+ SELECT id, path, label, work_item_id, work_item_title, created_at
21093
+ FROM ranked
21094
+ WHERE path_rank = 1 ${cursorCondition}
21095
+ ORDER BY created_at DESC, id DESC
21096
+ LIMIT ?`).all(projectId, ...cursor ? [
21097
+ cursor.createdAt,
21098
+ cursor.createdAt,
21099
+ cursor.id
21100
+ ] : [], limit + 1);
21101
+ return {
21102
+ artifacts: rows.slice(0, limit),
21103
+ hasMore: rows.length > limit,
21104
+ total
21105
+ };
21106
+ };
21107
+ };
21108
+ //#endregion
20857
21109
  //#region src/features/projects/stores/project-work-state.store.ts
20858
21110
  const DEFAULT_PROJECT_WORK_STATES = [
20859
21111
  {
@@ -21007,6 +21259,7 @@ var ProjectWorkStore = class {
21007
21259
  database = null;
21008
21260
  readyPromise = null;
21009
21261
  activities = new ProjectWorkActivityStore(() => this.db());
21262
+ queries = new ProjectWorkQueryStore(() => this.db(), toItem);
21010
21263
  states = new ProjectWorkStateStore(() => this.db(), this.activities.insertActivity);
21011
21264
  constructor(databasePath) {
21012
21265
  this.databasePath = databasePath;
@@ -21041,12 +21294,6 @@ var ProjectWorkStore = class {
21041
21294
  await this.ensureReady();
21042
21295
  return await this.states.deleteState(projectId, stateId, migrateToStateId, actor);
21043
21296
  };
21044
- listItems = async (projectId, includeDeleted = false) => {
21045
- await this.ensureReady();
21046
- return this.db().prepare(`SELECT * FROM project_work_items
21047
- WHERE project_id = ? ${includeDeleted ? "" : "AND deleted_at IS NULL"}
21048
- ORDER BY updated_at DESC, created_at DESC`).all(projectId).map(toItem);
21049
- };
21050
21297
  getItem = async (projectId, workItemId) => {
21051
21298
  await this.ensureReady();
21052
21299
  const row = this.db().prepare("SELECT * FROM project_work_items WHERE project_id = ? AND id = ? LIMIT 1").get(projectId, workItemId);
@@ -21193,8 +21440,12 @@ var ProjectWorkStore = class {
21193
21440
  );
21194
21441
  CREATE INDEX IF NOT EXISTS project_work_items_list_idx
21195
21442
  ON project_work_items(project_id, deleted_at, updated_at);
21443
+ CREATE INDEX IF NOT EXISTS project_work_items_state_list_idx
21444
+ ON project_work_items(project_id, state_id, deleted_at, updated_at, id);
21196
21445
  CREATE INDEX IF NOT EXISTS project_work_activity_timeline_idx
21197
21446
  ON project_work_activities(project_id, work_item_id, created_at);
21447
+ CREATE INDEX IF NOT EXISTS project_work_artifacts_recent_idx
21448
+ ON project_work_artifact_links(project_id, created_at, id);
21198
21449
  `);
21199
21450
  };
21200
21451
  db = () => {
@@ -21242,6 +21493,112 @@ function isProjectWorkError(error) {
21242
21493
  return error instanceof ProjectWorkError;
21243
21494
  }
21244
21495
  //#endregion
21496
+ //#region src/features/projects/services/project-work-query.service.ts
21497
+ var ProjectWorkQueryService = class {
21498
+ constructor(store, projectManager) {
21499
+ this.store = store;
21500
+ this.projectManager = projectManager;
21501
+ }
21502
+ list = async (projectId, input = {}) => {
21503
+ await this.ensureProject(projectId);
21504
+ const limit = this.requirePageLimit(input.limit);
21505
+ const cursor = input.cursor ? this.decodeCursor(input.cursor, "work item", "updatedAt") : void 0;
21506
+ const states = await this.store.listStates(projectId);
21507
+ if (input.stateId && !states.some((state) => state.id === input.stateId)) throw new ProjectWorkError("PROJECT_WORK_STATE_NOT_FOUND", "work item state was not found");
21508
+ const page = this.store.queries.listItems({
21509
+ projectId,
21510
+ includeDeleted: input.includeDeleted ?? false,
21511
+ limit,
21512
+ ...input.stateId ? { stateId: input.stateId } : {},
21513
+ ...cursor ? { cursor } : {}
21514
+ });
21515
+ const stateById = new Map(states.map((state) => [state.id, state]));
21516
+ const items = page.items.map((item) => ({
21517
+ ...item,
21518
+ state: this.requireMappedState(stateById, item.stateId)
21519
+ }));
21520
+ const last = items.at(-1);
21521
+ return {
21522
+ items,
21523
+ total: page.total,
21524
+ nextCursor: page.hasMore && last ? this.encodeCursor({
21525
+ updatedAt: last.updatedAt,
21526
+ id: last.id
21527
+ }) : null
21528
+ };
21529
+ };
21530
+ summary = async (projectId) => {
21531
+ await this.ensureProject(projectId);
21532
+ return this.store.queries.summarizeItems(projectId);
21533
+ };
21534
+ listRecentArtifacts = async (projectId, input = {}) => {
21535
+ const project = await this.requireProject(projectId);
21536
+ await this.store.ensureProject(projectId);
21537
+ const limit = this.requirePageLimit(input.limit);
21538
+ const cursor = input.cursor ? this.decodeCursor(input.cursor, "project artifact", "createdAt") : void 0;
21539
+ const page = this.store.queries.listRecentArtifacts({
21540
+ projectId,
21541
+ limit,
21542
+ ...cursor ? { cursor } : {}
21543
+ });
21544
+ const artifacts = await Promise.all(page.artifacts.map(async (artifact) => ({
21545
+ id: artifact.id,
21546
+ path: artifact.path,
21547
+ label: artifact.label,
21548
+ workItemId: artifact.work_item_id,
21549
+ workItemTitle: artifact.work_item_title,
21550
+ createdAt: artifact.created_at,
21551
+ exists: await this.isArtifactAvailable(project.rootPath, artifact.path)
21552
+ })));
21553
+ const last = artifacts.at(-1);
21554
+ return {
21555
+ artifacts,
21556
+ total: page.total,
21557
+ nextCursor: page.hasMore && last ? this.encodeCursor({
21558
+ createdAt: last.createdAt,
21559
+ id: last.id
21560
+ }) : null
21561
+ };
21562
+ };
21563
+ ensureProject = async (projectId) => {
21564
+ await this.requireProject(projectId);
21565
+ await this.store.ensureProject(projectId);
21566
+ };
21567
+ requireProject = async (projectId) => {
21568
+ const normalized = projectId.trim();
21569
+ const project = normalized ? await this.projectManager.getProjectById(normalized) : null;
21570
+ if (!project) throw new ProjectWorkError("PROJECT_NOT_FOUND", "project was not found");
21571
+ return project;
21572
+ };
21573
+ requireMappedState = (states, stateId) => {
21574
+ const state = states.get(stateId);
21575
+ if (!state) throw new ProjectWorkError("PROJECT_WORK_STATE_NOT_FOUND", "work item state was not found");
21576
+ return state;
21577
+ };
21578
+ requirePageLimit = (value) => {
21579
+ const limit = value ?? 20;
21580
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new ProjectWorkError("PROJECT_WORK_VALIDATION_FAILED", "page limit must be an integer between 1 and 100");
21581
+ return limit;
21582
+ };
21583
+ encodeCursor = (value) => Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
21584
+ decodeCursor = (value, label, timestampKey) => {
21585
+ try {
21586
+ const decoded = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
21587
+ if (!decoded || typeof decoded !== "object" || typeof decoded.id !== "string" || typeof decoded[timestampKey] !== "string" || Object.keys(decoded).some((key) => key !== "id" && key !== timestampKey) || Object.values(decoded).some((entry) => typeof entry !== "string" || !entry)) throw new Error("invalid cursor shape");
21588
+ return decoded;
21589
+ } catch {
21590
+ throw new ProjectWorkError("PROJECT_WORK_VALIDATION_FAILED", `${label} cursor is invalid`);
21591
+ }
21592
+ };
21593
+ isArtifactAvailable = async (projectRoot, projectRelativePath) => {
21594
+ try {
21595
+ return (await stat(resolve(projectRoot, projectRelativePath))).isFile();
21596
+ } catch {
21597
+ return false;
21598
+ }
21599
+ };
21600
+ };
21601
+ //#endregion
21245
21602
  //#region src/features/projects/types/project-work.types.ts
21246
21603
  const PROJECT_WORK_STATE_CATEGORIES = [
21247
21604
  "backlog",
@@ -21259,9 +21616,11 @@ const PROJECT_WORK_ATTENTION_VALUES = [
21259
21616
  //#region src/features/projects/managers/project-work.manager.ts
21260
21617
  var ProjectWorkManager = class {
21261
21618
  store;
21619
+ queries;
21262
21620
  constructor(options) {
21263
21621
  this.options = options;
21264
21622
  this.store = new ProjectWorkStore(options.databasePath);
21623
+ this.queries = new ProjectWorkQueryService(this.store, options.projectManager);
21265
21624
  }
21266
21625
  initialize = async () => {
21267
21626
  await this.store.initialize();
@@ -21272,31 +21631,9 @@ var ProjectWorkManager = class {
21272
21631
  await this.requireProject(projectId);
21273
21632
  await this.store.ensureProject(projectId);
21274
21633
  };
21275
- list = async (projectId, includeDeleted = false) => {
21276
- await this.ensureProject(projectId);
21277
- const [states, items] = await Promise.all([this.store.listStates(projectId), this.store.listItems(projectId, includeDeleted)]);
21278
- const stateById = new Map(states.map((state) => [state.id, state]));
21279
- const detailed = await Promise.all(items.map(async (item) => ({
21280
- ...item,
21281
- state: this.requireMappedState(stateById, item.stateId),
21282
- artifacts: await this.store.listArtifacts(projectId, item.id)
21283
- })));
21284
- return {
21285
- items: detailed,
21286
- states,
21287
- total: detailed.length
21288
- };
21289
- };
21290
- summary = async (projectId) => {
21291
- const { items } = await this.list(projectId);
21292
- return {
21293
- total: items.length,
21294
- active: items.filter((item) => item.state.category !== "completed" && item.state.category !== "canceled").length,
21295
- completed: items.filter((item) => item.state.category === "completed").length,
21296
- attention: items.filter((item) => item.attention !== "none").length,
21297
- updatedAt: items[0]?.updatedAt ?? null
21298
- };
21299
- };
21634
+ list = async (projectId, input = {}) => await this.queries.list(projectId, input);
21635
+ summary = async (projectId) => await this.queries.summary(projectId);
21636
+ listRecentArtifacts = async (projectId, input = {}) => await this.queries.listRecentArtifacts(projectId, input);
21300
21637
  get = async (projectId, workItemId) => {
21301
21638
  await this.ensureProject(projectId);
21302
21639
  const item = await this.store.getItem(projectId, workItemId);
@@ -21465,11 +21802,6 @@ var ProjectWorkManager = class {
21465
21802
  if (!project) throw new ProjectWorkError("PROJECT_NOT_FOUND", "project was not found");
21466
21803
  return project;
21467
21804
  };
21468
- requireMappedState = (states, stateId) => {
21469
- const state = states.get(stateId);
21470
- if (!state) throw new ProjectWorkError("PROJECT_WORK_STATE_NOT_FOUND", "work item state was not found");
21471
- return state;
21472
- };
21473
21805
  requireName = (value, label) => {
21474
21806
  const normalized = typeof value === "string" ? value.trim() : "";
21475
21807
  if (!normalized) throw new ProjectWorkError("PROJECT_WORK_VALIDATION_FAILED", `${label} is required`);
@@ -26702,13 +27034,29 @@ function agentActor(context) {
26702
27034
  }
26703
27035
  var ProjectWorkListTool = class {
26704
27036
  name = "project_work_list";
26705
- description = "List persistent work items and custom states for the current project.";
27037
+ description = "List one bounded page of persistent work items for the current project.";
26706
27038
  parameters = {
26707
27039
  type: "object",
26708
- properties: { include_deleted: {
26709
- type: "boolean",
26710
- description: "Include deleted work items."
26711
- } },
27040
+ properties: {
27041
+ include_deleted: {
27042
+ type: "boolean",
27043
+ description: "Include deleted work items."
27044
+ },
27045
+ state_id: {
27046
+ type: "string",
27047
+ description: "Only return work items in this custom state."
27048
+ },
27049
+ cursor: {
27050
+ type: "string",
27051
+ description: "Opaque next_cursor from an earlier list response."
27052
+ },
27053
+ limit: {
27054
+ type: "integer",
27055
+ minimum: 1,
27056
+ maximum: 100,
27057
+ description: "Page size. Defaults to 20."
27058
+ }
27059
+ },
26712
27060
  additionalProperties: false
26713
27061
  };
26714
27062
  constructor(work, context) {
@@ -26717,7 +27065,12 @@ var ProjectWorkListTool = class {
26717
27065
  }
26718
27066
  execute = async (args) => {
26719
27067
  const params = normalizeToolParams(args);
26720
- return JSON.stringify(await this.work.list(this.context.projectId, optionalBoolean(params.include_deleted) ?? false), null, 2);
27068
+ return JSON.stringify(await this.work.list(this.context.projectId, {
27069
+ includeDeleted: optionalBoolean(params.include_deleted) ?? false,
27070
+ ...optionalString$1(params.state_id) ? { stateId: optionalString$1(params.state_id) } : {},
27071
+ ...optionalString$1(params.cursor) ? { cursor: optionalString$1(params.cursor) } : {},
27072
+ ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
27073
+ }), null, 2);
26721
27074
  };
26722
27075
  };
26723
27076
  var ProjectWorkGetTool = class {