@nextclaw/kernel 0.15.0 → 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,
@@ -20646,9 +20809,18 @@ async function openPortableDatabase(databasePath) {
20646
20809
  }
20647
20810
  async function createPortableDatabase(databasePath) {
20648
20811
  const initialize = (await import("sql.js")).default;
20649
- const sqlite = await initialize({ locateFile: (file) => require.resolve(`sql.js/dist/${file}`) });
20812
+ const sqlite = await initialize({ locateFile: resolveSqlJsAsset });
20650
20813
  return new PortableSqliteDatabase(databasePath, existsSync(databasePath) ? new sqlite.Database(readFileSync(databasePath)) : new sqlite.Database());
20651
20814
  }
20815
+ function resolveSqlJsAsset(file) {
20816
+ try {
20817
+ return require.resolve(`sql.js/dist/${file}`);
20818
+ } catch (error) {
20819
+ const bundledAssetPath = fileURLToPath(new URL(file, import.meta.url));
20820
+ if (file === "sql-wasm.wasm" && existsSync(bundledAssetPath)) return bundledAssetPath;
20821
+ throw error;
20822
+ }
20823
+ }
20652
20824
  var PortableSqliteDatabase = class {
20653
20825
  inTransaction = false;
20654
20826
  dirty = false;
@@ -20845,6 +21017,95 @@ function toArtifactLink(row) {
20845
21017
  };
20846
21018
  }
20847
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
20848
21109
  //#region src/features/projects/stores/project-work-state.store.ts
20849
21110
  const DEFAULT_PROJECT_WORK_STATES = [
20850
21111
  {
@@ -20998,6 +21259,7 @@ var ProjectWorkStore = class {
20998
21259
  database = null;
20999
21260
  readyPromise = null;
21000
21261
  activities = new ProjectWorkActivityStore(() => this.db());
21262
+ queries = new ProjectWorkQueryStore(() => this.db(), toItem);
21001
21263
  states = new ProjectWorkStateStore(() => this.db(), this.activities.insertActivity);
21002
21264
  constructor(databasePath) {
21003
21265
  this.databasePath = databasePath;
@@ -21032,12 +21294,6 @@ var ProjectWorkStore = class {
21032
21294
  await this.ensureReady();
21033
21295
  return await this.states.deleteState(projectId, stateId, migrateToStateId, actor);
21034
21296
  };
21035
- listItems = async (projectId, includeDeleted = false) => {
21036
- await this.ensureReady();
21037
- return this.db().prepare(`SELECT * FROM project_work_items
21038
- WHERE project_id = ? ${includeDeleted ? "" : "AND deleted_at IS NULL"}
21039
- ORDER BY updated_at DESC, created_at DESC`).all(projectId).map(toItem);
21040
- };
21041
21297
  getItem = async (projectId, workItemId) => {
21042
21298
  await this.ensureReady();
21043
21299
  const row = this.db().prepare("SELECT * FROM project_work_items WHERE project_id = ? AND id = ? LIMIT 1").get(projectId, workItemId);
@@ -21184,8 +21440,12 @@ var ProjectWorkStore = class {
21184
21440
  );
21185
21441
  CREATE INDEX IF NOT EXISTS project_work_items_list_idx
21186
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);
21187
21445
  CREATE INDEX IF NOT EXISTS project_work_activity_timeline_idx
21188
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);
21189
21449
  `);
21190
21450
  };
21191
21451
  db = () => {
@@ -21233,6 +21493,112 @@ function isProjectWorkError(error) {
21233
21493
  return error instanceof ProjectWorkError;
21234
21494
  }
21235
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
21236
21602
  //#region src/features/projects/types/project-work.types.ts
21237
21603
  const PROJECT_WORK_STATE_CATEGORIES = [
21238
21604
  "backlog",
@@ -21250,9 +21616,11 @@ const PROJECT_WORK_ATTENTION_VALUES = [
21250
21616
  //#region src/features/projects/managers/project-work.manager.ts
21251
21617
  var ProjectWorkManager = class {
21252
21618
  store;
21619
+ queries;
21253
21620
  constructor(options) {
21254
21621
  this.options = options;
21255
21622
  this.store = new ProjectWorkStore(options.databasePath);
21623
+ this.queries = new ProjectWorkQueryService(this.store, options.projectManager);
21256
21624
  }
21257
21625
  initialize = async () => {
21258
21626
  await this.store.initialize();
@@ -21263,31 +21631,9 @@ var ProjectWorkManager = class {
21263
21631
  await this.requireProject(projectId);
21264
21632
  await this.store.ensureProject(projectId);
21265
21633
  };
21266
- list = async (projectId, includeDeleted = false) => {
21267
- await this.ensureProject(projectId);
21268
- const [states, items] = await Promise.all([this.store.listStates(projectId), this.store.listItems(projectId, includeDeleted)]);
21269
- const stateById = new Map(states.map((state) => [state.id, state]));
21270
- const detailed = await Promise.all(items.map(async (item) => ({
21271
- ...item,
21272
- state: this.requireMappedState(stateById, item.stateId),
21273
- artifacts: await this.store.listArtifacts(projectId, item.id)
21274
- })));
21275
- return {
21276
- items: detailed,
21277
- states,
21278
- total: detailed.length
21279
- };
21280
- };
21281
- summary = async (projectId) => {
21282
- const { items } = await this.list(projectId);
21283
- return {
21284
- total: items.length,
21285
- active: items.filter((item) => item.state.category !== "completed" && item.state.category !== "canceled").length,
21286
- completed: items.filter((item) => item.state.category === "completed").length,
21287
- attention: items.filter((item) => item.attention !== "none").length,
21288
- updatedAt: items[0]?.updatedAt ?? null
21289
- };
21290
- };
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);
21291
21637
  get = async (projectId, workItemId) => {
21292
21638
  await this.ensureProject(projectId);
21293
21639
  const item = await this.store.getItem(projectId, workItemId);
@@ -21456,11 +21802,6 @@ var ProjectWorkManager = class {
21456
21802
  if (!project) throw new ProjectWorkError("PROJECT_NOT_FOUND", "project was not found");
21457
21803
  return project;
21458
21804
  };
21459
- requireMappedState = (states, stateId) => {
21460
- const state = states.get(stateId);
21461
- if (!state) throw new ProjectWorkError("PROJECT_WORK_STATE_NOT_FOUND", "work item state was not found");
21462
- return state;
21463
- };
21464
21805
  requireName = (value, label) => {
21465
21806
  const normalized = typeof value === "string" ? value.trim() : "";
21466
21807
  if (!normalized) throw new ProjectWorkError("PROJECT_WORK_VALIDATION_FAILED", `${label} is required`);
@@ -23800,7 +24141,6 @@ var NcpAgentSessionSummaryIndexStore = class {
23800
24141
  CREATE INDEX IF NOT EXISTS sessions_activity_idx
23801
24142
  ON sessions (deleted_at, last_message_at, created_at, updated_at);
23802
24143
  `);
23803
- if (this.database.prepare("SELECT value FROM storage_meta WHERE key = ? LIMIT 1").get(MIGRATION_STATUS_KEY)?.value === MIGRATION_COMPLETE) return;
23804
24144
  const scan = await scanNcpAgentSessionCatalogJournals({
23805
24145
  journalDir: this.journalDir,
23806
24146
  loadSession: (sessionId) => this.loadSession(sessionId),
@@ -23846,7 +24186,16 @@ var NcpAgentSessionSummaryIndexStore = class {
23846
24186
  deleted_at = CASE
23847
24187
  WHEN @restore_deleted = 1 THEN NULL
23848
24188
  ELSE sessions.deleted_at
23849
- END`).run({
24189
+ END
24190
+ WHERE sessions.peer_id IS NOT excluded.peer_id
24191
+ OR sessions.agent_id IS NOT excluded.agent_id
24192
+ OR sessions.created_at IS NOT excluded.created_at
24193
+ OR sessions.updated_at IS NOT excluded.updated_at
24194
+ OR sessions.last_message_at IS NOT excluded.last_message_at
24195
+ OR sessions.message_count IS NOT excluded.message_count
24196
+ OR sessions.status IS NOT excluded.status
24197
+ OR sessions.metadata_json IS NOT excluded.metadata_json
24198
+ OR (@restore_deleted = 1 AND sessions.deleted_at IS NOT NULL)`).run({
23850
24199
  ...row,
23851
24200
  restore_deleted: restoreDeleted ? 1 : 0
23852
24201
  });
@@ -23974,6 +24323,7 @@ var NcpAgentSessionJournalStore = class {
23974
24323
  this.unfinishedRunStore = new NcpAgentUnfinishedRunStore(journalDir, async () => (await this.summaryIndexStore.list()).map(({ sessionId }) => sessionId));
23975
24324
  }
23976
24325
  initialize = async () => await this.summaryIndexStore.initialize();
24326
+ close = () => this.summaryIndexStore.close();
23977
24327
  appendSessionEvent = async (params) => {
23978
24328
  const sessionId = normalizeNcpSessionId(params.sessionId);
23979
24329
  if (!sessionId) return;
@@ -26684,13 +27034,29 @@ function agentActor(context) {
26684
27034
  }
26685
27035
  var ProjectWorkListTool = class {
26686
27036
  name = "project_work_list";
26687
- 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.";
26688
27038
  parameters = {
26689
27039
  type: "object",
26690
- properties: { include_deleted: {
26691
- type: "boolean",
26692
- description: "Include deleted work items."
26693
- } },
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
+ },
26694
27060
  additionalProperties: false
26695
27061
  };
26696
27062
  constructor(work, context) {
@@ -26699,7 +27065,12 @@ var ProjectWorkListTool = class {
26699
27065
  }
26700
27066
  execute = async (args) => {
26701
27067
  const params = normalizeToolParams(args);
26702
- 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);
26703
27074
  };
26704
27075
  };
26705
27076
  var ProjectWorkGetTool = class {