@notis_ai/cli 0.2.0-beta.177.1 → 0.2.0-beta.179.1

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.
@@ -4797,7 +4797,7 @@ var init_skill_sync = __esm({
4797
4797
 
4798
4798
  // src/cli.js
4799
4799
  import { readFileSync as readFileSync18 } from "node:fs";
4800
- import { dirname as dirname16, join as join16 } from "node:path";
4800
+ import { dirname as dirname16, join as join17 } from "node:path";
4801
4801
  import { fileURLToPath as fileURLToPath11 } from "node:url";
4802
4802
 
4803
4803
  // node_modules/commander/lib/error.js
@@ -8169,8 +8169,8 @@ function useColor() {
8169
8169
  var program = new Command();
8170
8170
 
8171
8171
  // src/command-specs/reports.js
8172
- import { readFileSync as readFileSync9 } from "node:fs";
8173
- import { resolve as resolve6 } from "node:path";
8172
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, rmSync as rmSync6 } from "node:fs";
8173
+ import { resolve as resolve6, join as join9 } from "node:path";
8174
8174
 
8175
8175
  // src/command-specs/apps.js
8176
8176
  init_errors();
@@ -9223,10 +9223,10 @@ function detectProjectWarnings(projectDir, appConfig = null) {
9223
9223
  warnings.push("Missing Tailwind config.");
9224
9224
  }
9225
9225
  const readsWorkspaceDatabases = appConfig?.capabilities?.workspaceDatabases === "read";
9226
- if (appConfig && !readsWorkspaceDatabases && (!Array.isArray(appConfig.databases) || appConfig.databases.length === 0)) {
9226
+ if (appConfig && appConfig.kind !== "report" && !readsWorkspaceDatabases && (!Array.isArray(appConfig.databases) || appConfig.databases.length === 0)) {
9227
9227
  warnings.push("No database references declared in notis.config.ts.");
9228
9228
  }
9229
- if (appConfig) {
9229
+ if (appConfig && appConfig.kind !== "report") {
9230
9230
  try {
9231
9231
  const listing = inspectListingReadiness(projectDir, appConfig);
9232
9232
  warnings.push(...listing.errors, ...listing.warnings);
@@ -9657,6 +9657,33 @@ function generateManifest(appConfig, projectDir) {
9657
9657
  }
9658
9658
  return entry;
9659
9659
  });
9660
+ if (appConfig.kind === "report") {
9661
+ if (routes.length !== 1 || routes[0].collection || routes[0].resourceDeepLinks) {
9662
+ throw usageError("Reports require one standalone route, without app collections or resource deep links.");
9663
+ }
9664
+ if (["databases", "skills", "automations", "onboarding", "tagline", "categories", "author", "screenshots"].some((key) => {
9665
+ const value = appConfig[key];
9666
+ return Array.isArray(value) ? value.length > 0 : Boolean(value);
9667
+ })) throw usageError("Reports cannot own databases, skills, automations or Store metadata.");
9668
+ if (Object.values(appConfig.capabilities || {}).some((value) => value !== "read")) {
9669
+ throw usageError("Reports support read capabilities only; shell grants require an installed app.");
9670
+ }
9671
+ const databaseAccess = appConfig.databaseAccess || [];
9672
+ if (!Array.isArray(databaseAccess) || databaseAccess.some(
9673
+ (entry) => !entry || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(entry.id || "") || !["read", "write"].includes(entry.access)
9674
+ )) throw usageError("databaseAccess entries require an existing database UUID and read/write access.");
9675
+ return {
9676
+ kind: "report",
9677
+ version: 1,
9678
+ spec_version: 4,
9679
+ routes: routes.map((route) => ({ ...route, default: true, tool_access: { base: { tools: appConfig.tools || [] } } })),
9680
+ bundle: { js: "bundle/app.js", css: "bundle/app.css" },
9681
+ tools: appConfig.tools || [],
9682
+ tool_bindings: normalizeAppToolBindings(appConfig.toolBindings),
9683
+ capabilities: normalizeAppCapabilities(appConfig.capabilities),
9684
+ database_access: databaseAccess
9685
+ };
9686
+ }
9660
9687
  const databases = (appConfig.databases || []).map((entry) => {
9661
9688
  if (typeof entry === "string") return entry;
9662
9689
  if (!entry || typeof entry !== "object" || !entry.slug) return entry;
@@ -11600,6 +11627,7 @@ function buildHarnessDescriptor({ state, manifest, appConfig, route, scenario =
11600
11627
  Array.isArray(appConfig?.tools) && appConfig.tools.length ? appConfig.tools : manifest.tools
11601
11628
  );
11602
11629
  return {
11630
+ ...state.resource ? { resource: state.resource } : {},
11603
11631
  app: {
11604
11632
  id: state.appId || "harness-app",
11605
11633
  slug: state.slug,
@@ -11720,6 +11748,78 @@ async function startAppTestServer({ apps, port, harness = {} }) {
11720
11748
  import { spawn as spawn2, spawnSync } from "node:child_process";
11721
11749
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
11722
11750
  import { dirname as dirname5 } from "node:path";
11751
+
11752
+ // src/runtime/screenshot-framing.js
11753
+ function measureScreenshotContent() {
11754
+ const root = document.getElementById("root");
11755
+ if (!root) return null;
11756
+ const viewportWidth = document.documentElement.clientWidth;
11757
+ const visible = (element) => {
11758
+ const rect2 = element.getBoundingClientRect();
11759
+ const style = getComputedStyle(element);
11760
+ return rect2.width > 0 && rect2.height > 0 && style.display !== "none" && style.visibility !== "hidden";
11761
+ };
11762
+ let node = root;
11763
+ let content = null;
11764
+ while (node) {
11765
+ const hasOwnText = Array.from(node.childNodes).some(
11766
+ (child) => child.nodeType === 3 && child.textContent.trim()
11767
+ );
11768
+ const children = Array.from(node.children).filter(visible);
11769
+ if (hasOwnText || children.length !== 1) break;
11770
+ node = children[0];
11771
+ const rect2 = node.getBoundingClientRect();
11772
+ if (["DIV", "MAIN", "SECTION", "ARTICLE", "FORM"].includes(node.tagName) && rect2.width >= 320 && rect2.height >= 160 && rect2.width < viewportWidth * 0.9) {
11773
+ content = node;
11774
+ }
11775
+ }
11776
+ if (!content) return null;
11777
+ content.setAttribute("data-notis-screenshot-content", "");
11778
+ const rect = content.getBoundingClientRect();
11779
+ return {
11780
+ selector: "[data-notis-screenshot-content]",
11781
+ width: rect.width,
11782
+ height: rect.height,
11783
+ viewport_width: viewportWidth
11784
+ };
11785
+ }
11786
+ var MEASURE_SCREENSHOT_CONTENT_SCRIPT = `(${measureScreenshotContent.toString()})()`;
11787
+ function contentCapturePlan(bounds, viewport, outputWidth, outputHeight) {
11788
+ const aspect = outputWidth / outputHeight;
11789
+ if (bounds.width / bounds.height < aspect * 0.75) {
11790
+ const width = Math.max(768, Math.ceil((bounds.width + 32) / 8) * 8);
11791
+ return { focus: false, width, height: Math.round(width / aspect), scale: outputWidth / width };
11792
+ }
11793
+ return {
11794
+ focus: true,
11795
+ width: viewport.width,
11796
+ height: viewport.height,
11797
+ scale: Math.min(4, outputWidth / bounds.width)
11798
+ };
11799
+ }
11800
+ function safeViewportHeight(rectangles, height) {
11801
+ const crossing = rectangles.filter((rect) => rect.top < height && rect.bottom > height && rect.top >= height * 0.85);
11802
+ return crossing.length ? Math.max(Math.ceil(height * 0.85), Math.floor(Math.min(...crossing.map((rect) => rect.top)) - 8)) : height;
11803
+ }
11804
+ var SAFE_VIEWPORT_HEIGHT_SCRIPT = `(() => {
11805
+ const root = document.getElementById('root');
11806
+ if (!root) return innerHeight;
11807
+ const rects = [];
11808
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
11809
+ let node;
11810
+ while ((node = walker.nextNode())) {
11811
+ if (!node.textContent.trim() || getComputedStyle(node.parentElement).visibility !== 'visible') continue;
11812
+ const range = document.createRange(); range.selectNodeContents(node);
11813
+ for (const rect of range.getClientRects()) rects.push({ top: rect.top, bottom: rect.bottom });
11814
+ }
11815
+ for (const control of root.querySelectorAll('button,input,select,textarea')) {
11816
+ const rect = control.getBoundingClientRect();
11817
+ rects.push({ top: rect.top, bottom: rect.bottom });
11818
+ }
11819
+ return (${safeViewportHeight.toString()})(rects, innerHeight);
11820
+ })()`;
11821
+
11822
+ // src/runtime/agent-browser.js
11723
11823
  var PREPARE_CAPTURE_SCRIPT = "(() => { const s = document.getElementById('harness-status'); if (s) s.style.display = 'none'; const r = document.getElementById('root'); if (r) { r.style.paddingTop = '0'; r.style.minHeight = '0'; } document.body.style.minHeight = '0'; document.documentElement.style.minHeight = '0'; return true; })()";
11724
11824
  var FONTS_STATUS_SCRIPT = "(() => (document.fonts ? document.fonts.status : 'loaded'))()";
11725
11825
  var CONTENT_HEIGHT_SCRIPT = "(() => { const r = document.getElementById('root'); if (!r) return Math.ceil(document.documentElement.scrollHeight); return Math.ceil(Math.max(r.scrollHeight, r.getBoundingClientRect().height)); })()";
@@ -11902,7 +12002,8 @@ async function runHarnessRoute({
11902
12002
  sessionName,
11903
12003
  timeoutMs = 1e4,
11904
12004
  snapshotPath = null,
11905
- designViewports = DEFAULT_DESIGN_VIEWPORTS
12005
+ designViewports = DEFAULT_DESIGN_VIEWPORTS,
12006
+ waitForRuntime = false
11906
12007
  }) {
11907
12008
  const opened = await runAgentBrowser(["--session", sessionName, "open", url], {
11908
12009
  timeoutMs: Math.min(Math.max(timeoutMs, 5e3), 3e4)
@@ -11918,17 +12019,21 @@ async function runHarnessRoute({
11918
12019
  };
11919
12020
  }
11920
12021
  const deadline = Date.now() + timeoutMs;
12022
+ let readySince = null;
11921
12023
  while (Date.now() < deadline) {
11922
12024
  const lastRead = await readHarness(sessionName, 5e3);
11923
12025
  if (!lastRead.ok) {
12026
+ readySince = null;
11924
12027
  await delay(250);
11925
12028
  continue;
11926
12029
  }
11927
12030
  const harness2 = lastRead.harness;
11928
- if (harness2?.mounted || Array.isArray(harness2?.errors) && harness2.errors.length > 0) {
11929
- await delay(250);
11930
- break;
11931
- }
12031
+ if (Array.isArray(harness2?.errors) && harness2.errors.length > 0) break;
12032
+ const pending = (harness2?.runtimeCalls || []).some((call) => call?.ok == null);
12033
+ if (harness2?.mounted && (!waitForRuntime || !pending)) {
12034
+ readySince ??= Date.now();
12035
+ if (Date.now() - readySince >= 250) break;
12036
+ } else readySince = null;
11932
12037
  await delay(250);
11933
12038
  }
11934
12039
  const finalRead = await readHarness(sessionName, 5e3);
@@ -11954,8 +12059,8 @@ async function runHarnessRoute({
11954
12059
  savedSnapshotPath = snapshotPath;
11955
12060
  }
11956
12061
  }
11957
- const timedOut = !harness.mounted && Date.now() >= deadline;
11958
12062
  const runtimeCalls = Array.isArray(harness.runtimeCalls) ? harness.runtimeCalls : [];
12063
+ const timedOut = (!harness.mounted || waitForRuntime && runtimeCalls.some((call) => call?.ok == null)) && Date.now() >= deadline;
11959
12064
  let design = [];
11960
12065
  let designToolError = null;
11961
12066
  if (harness.mounted && Array.isArray(designViewports) && designViewports.length > 0) {
@@ -11986,6 +12091,7 @@ async function captureHarnessScreenshot({
11986
12091
  sessionName,
11987
12092
  screenshotPath,
11988
12093
  focusSelector = null,
12094
+ frameContent = false,
11989
12095
  width = 2e3,
11990
12096
  height = 1250,
11991
12097
  timeoutMs = 15e3
@@ -12070,12 +12176,49 @@ async function captureHarnessScreenshot({
12070
12176
  }
12071
12177
  }
12072
12178
  await delay(500);
12179
+ let contentBounds = null;
12180
+ if (frameContent && !focusSelector) {
12181
+ const measured = await runAgentBrowser(
12182
+ ["--session", sessionName, "--json", "eval", MEASURE_SCREENSHOT_CONTENT_SCRIPT],
12183
+ { timeoutMs: 5e3 }
12184
+ );
12185
+ if (measured.exitCode === 0) {
12186
+ try {
12187
+ const bounds = parseHarnessFromEval(measured.stdout);
12188
+ if (bounds?.selector === "[data-notis-screenshot-content]" && Number.isFinite(bounds.width) && bounds.width > 0 && Number.isFinite(bounds.height) && bounds.height > 0) {
12189
+ contentBounds = bounds;
12190
+ }
12191
+ } catch {
12192
+ }
12193
+ }
12194
+ }
12195
+ let captureFocus = focusSelector || contentBounds?.selector || null;
12196
+ if (contentBounds && scaledViewport) {
12197
+ const plan = contentCapturePlan(contentBounds, framing || initial, width, height);
12198
+ if (await setViewport(sessionName, plan.width, plan.height, plan.scale)) {
12199
+ await delay(300);
12200
+ if (!plan.focus) {
12201
+ const safeHeight = await evalNumber(sessionName, SAFE_VIEWPORT_HEIGHT_SCRIPT, plan.height);
12202
+ if (safeHeight < plan.height && await setViewport(sessionName, plan.width, safeHeight, plan.scale)) {
12203
+ plan.height = safeHeight;
12204
+ }
12205
+ }
12206
+ captureFocus = plan.focus ? contentBounds.selector : null;
12207
+ framing = {
12208
+ ...framing || {},
12209
+ ...plan,
12210
+ content_bounds: contentBounds,
12211
+ capture_mode: plan.focus ? "content" : "width-fit-viewport"
12212
+ };
12213
+ await delay(300);
12214
+ }
12215
+ }
12073
12216
  const designResult = await evalDesignAssertions(sessionName, { viewport: "capture" });
12074
12217
  const design = Array.isArray(designResult) ? designResult : [];
12075
12218
  mkdirSync3(dirname5(screenshotPath), { recursive: true });
12076
12219
  const screenshotArgs = ["--session", sessionName, "screenshot"];
12077
- if (focusSelector) {
12078
- screenshotArgs.push(focusSelector);
12220
+ if (captureFocus) {
12221
+ screenshotArgs.push(captureFocus);
12079
12222
  }
12080
12223
  screenshotArgs.push(screenshotPath, "--screenshot-format", "png");
12081
12224
  const shot = await runAgentBrowser(screenshotArgs, { timeoutMs: 15e3 });
@@ -12083,7 +12226,9 @@ async function captureHarnessScreenshot({
12083
12226
  return { ok: false, mounted: true, screenshotPath: null, errors: lastErrors, timed_out: false, tool_error: commandError("screenshot", shot) };
12084
12227
  }
12085
12228
  const dimensions = pngDimensions2(screenshotPath);
12086
- if (!focusSelector && framing && (!dimensions || dimensions.width !== width || dimensions.height !== height)) {
12229
+ const expectedDimensions = framing?.capture_mode === "width-fit-viewport" ? { width: Math.round(framing.width * framing.scale), height: Math.round(framing.height * framing.scale) } : { width, height };
12230
+ const tolerance = frameContent ? 1 : 0;
12231
+ if (!captureFocus && framing && (!dimensions || Math.abs(dimensions.width - expectedDimensions.width) > tolerance || Math.abs(dimensions.height - expectedDimensions.height) > tolerance)) {
12087
12232
  await setViewport(sessionName, width, height);
12088
12233
  await delay(300);
12089
12234
  const retry = await runAgentBrowser(
@@ -12095,10 +12240,11 @@ async function captureHarnessScreenshot({
12095
12240
  }
12096
12241
  framing = null;
12097
12242
  }
12098
- if (focusSelector) {
12243
+ if (captureFocus) {
12099
12244
  framing = {
12100
12245
  ...framing || {},
12101
- focus_selector: focusSelector,
12246
+ focus_selector: captureFocus,
12247
+ content_bounds: contentBounds,
12102
12248
  source_dimensions: dimensions
12103
12249
  };
12104
12250
  }
@@ -12289,15 +12435,16 @@ function resolveStoreScreenshotAccent(accent, seed = "notis-app") {
12289
12435
  }
12290
12436
  return ACCENT_NAMES[stableHash(seed) % ACCENT_NAMES.length];
12291
12437
  }
12292
- function storeScreenshotLayout(width = 2e3, height = 1250) {
12438
+ function storeScreenshotLayout(width = 2e3, height = 1250, contentAspect = STORE_SCREENSHOT_ASPECT) {
12439
+ const aspect = Number.isFinite(contentAspect) && contentAspect > 0 ? contentAspect : STORE_SCREENSHOT_ASPECT;
12293
12440
  const horizontalInset = Math.max(48, Math.round(width * 0.06));
12294
12441
  const verticalInset = Math.max(40, Math.round(height * 0.06));
12295
12442
  let cardWidth = width - horizontalInset * 2;
12296
- let cardHeight = Math.round(cardWidth / STORE_SCREENSHOT_ASPECT);
12443
+ let cardHeight = Math.round(cardWidth / aspect);
12297
12444
  const maximumCardHeight = height - verticalInset * 2;
12298
12445
  if (cardHeight > maximumCardHeight) {
12299
12446
  cardHeight = maximumCardHeight;
12300
- cardWidth = Math.round(cardHeight * STORE_SCREENSHOT_ASPECT);
12447
+ cardWidth = Math.round(cardHeight * aspect);
12301
12448
  }
12302
12449
  const cardLeft = Math.round((width - cardWidth) / 2);
12303
12450
  const cardTop = Math.round((height - cardHeight) / 2) - Math.round(height * 8e-3);
@@ -12333,11 +12480,12 @@ async function composeStoreScreenshot({
12333
12480
  }) {
12334
12481
  const sharp = await loadSharp();
12335
12482
  const resolvedTheme = theme === "dark" ? "dark" : "light";
12336
- const layout = storeScreenshotLayout(width, height);
12483
+ const source = await sharp(inputPath).metadata();
12484
+ const layout = storeScreenshotLayout(width, height, source.width / source.height);
12337
12485
  const resolvedAccent = resolveStoreScreenshotAccent(accent, seed);
12338
12486
  const roundedCapture = await sharp(inputPath).resize(layout.cardWidth, layout.cardHeight, {
12339
- fit: "cover",
12340
- position: focused ? "top" : "centre"
12487
+ fit: "contain",
12488
+ background: resolvedTheme === "dark" ? "#09090b" : "#ffffff"
12341
12489
  }).composite([{
12342
12490
  input: roundedRectSvg(layout.cardWidth, layout.cardHeight, layout.radius),
12343
12491
  blend: "dest-in"
@@ -13176,6 +13324,9 @@ function assertHarnessResult(result, route, databaseSlugs, mode = "stub", capabi
13176
13324
  });
13177
13325
  }
13178
13326
  if (mode === "live") {
13327
+ if (runtimeCalls.some((call) => call?.ok == null)) {
13328
+ assertions.push({ ok: false, code: "runtime_calls_pending", message: "Live verification ended before every started runtime call settled." });
13329
+ }
13179
13330
  if (runtimeCalls.length > 0 && runtimeCalls.every((call) => call?.ok === false)) {
13180
13331
  assertions.push({
13181
13332
  ok: false,
@@ -13414,6 +13565,8 @@ async function appsVerifyHandler(ctx) {
13414
13565
  throw usageError(`Project has problems:
13415
13566
  ${problems.map((p) => ` - ${p}`).join("\n")}`);
13416
13567
  }
13568
+ const appConfig = await loadAppConfig(projectDir);
13569
+ const isReport = appConfig.kind === "report";
13417
13570
  const mode = ctx.options.mode || "stub";
13418
13571
  if (!["stub", "live"].includes(mode)) {
13419
13572
  throw usageError('--mode must be either "stub" or "live".');
@@ -13426,8 +13579,10 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
13426
13579
  if (!ctx.runtime.jwt) {
13427
13580
  throw usageError("Live verify mode requires CLI auth. Run notis login and retry.");
13428
13581
  }
13429
- linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
13430
- if (!linkedState?.app_id) {
13582
+ if (isReport) {
13583
+ if (!ctx.options.documentId || !/^\d+$/.test(String(ctx.options.expectedRevision ?? ""))) throw usageError("Live report verification requires --document-id and --expected-revision of the saved report.");
13584
+ } else linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
13585
+ if (!isReport && !linkedState?.app_id) {
13431
13586
  throw usageError("Live verify mode requires a linked app. Run `notis apps link <app-id> .` first.");
13432
13587
  }
13433
13588
  }
@@ -13437,15 +13592,14 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
13437
13592
  });
13438
13593
  }
13439
13594
  const manifest = readManifest(projectDir);
13440
- const appConfig = await loadAppConfig(projectDir);
13441
- const listing = inspectListingReadiness(projectDir, appConfig);
13595
+ const listing = isReport ? { ready: true, errors: [], warnings: [] } : inspectListingReadiness(projectDir, appConfig);
13442
13596
  if (listing.errors.length && ctx.options.listing === true) {
13443
13597
  throw usageError(`Listing metadata has problems:
13444
13598
  ${listing.errors.map((error) => ` - ${error}`).join("\n")}`);
13445
13599
  }
13446
13600
  const listingWarnings = [
13447
13601
  ...[...listing.errors, ...listing.warnings].map((message) => `Store readiness: ${message}`),
13448
- ...findUnknownScreenshotScenarios(projectDir, resolveListingScreenshots(projectDir, appConfig))
13602
+ ...isReport ? [] : findUnknownScreenshotScenarios(projectDir, resolveListingScreenshots(projectDir, appConfig))
13449
13603
  ];
13450
13604
  const routes = routeSelection(manifest, parseRouteSlugs(ctx.options.routes));
13451
13605
  const port = parsePort(ctx.options.port) || await getAvailablePort();
@@ -13467,7 +13621,8 @@ ${listing.errors.map((error) => ` - ${error}`).join("\n")}`);
13467
13621
  apps: [{
13468
13622
  slug: appSlug,
13469
13623
  projectDir,
13470
- appId: linkedState?.app_id || "harness-app"
13624
+ appId: linkedState?.app_id || "harness-app",
13625
+ resource: isReport ? { kind: "report", id: ctx.options.documentId || "preview", revision: Number(ctx.options.expectedRevision || 0) } : void 0
13471
13626
  }],
13472
13627
  port,
13473
13628
  harness: {
@@ -13541,7 +13696,8 @@ ${listing.errors.map((error) => ` - ${error}`).join("\n")}`);
13541
13696
  const result = await runHarnessRoute({
13542
13697
  url,
13543
13698
  sessionName: browserSessionName,
13544
- timeoutMs: Number.parseInt(ctx.globalOptions.timeoutMs || "", 10) || 1e4,
13699
+ timeoutMs: Number.parseInt(ctx.globalOptions.timeoutMs || "", 10) || (mode === "live" ? 9e4 : 1e4),
13700
+ waitForRuntime: mode === "live",
13545
13701
  snapshotPath
13546
13702
  });
13547
13703
  const assertions = assertHarnessResult(
@@ -13596,11 +13752,7 @@ ${listing.errors.map((error) => ` - ${error}`).join("\n")}`);
13596
13752
  },
13597
13753
  summary,
13598
13754
  results,
13599
- listing: {
13600
- ready: listing.ready,
13601
- gated: ctx.options.listing === true,
13602
- problems: listing.errors
13603
- }
13755
+ ...!isReport ? { listing: { ready: listing.ready, gated: ctx.options.listing === true, problems: listing.errors } } : {}
13604
13756
  };
13605
13757
  if (!keepOpen) await cleanup();
13606
13758
  ctx.output.emitSuccess({
@@ -13739,6 +13891,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
13739
13891
  sessionName: captureSessionName,
13740
13892
  screenshotPath: browserScreenshotPath,
13741
13893
  focusSelector: ctx.options.raw ? null : focus,
13894
+ frameContent: !ctx.options.raw,
13742
13895
  width,
13743
13896
  height,
13744
13897
  timeoutMs: Number.parseInt(ctx.globalOptions.timeoutMs || "", 10) || 15e3
@@ -13753,7 +13906,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
13753
13906
  height,
13754
13907
  accent: appConfig.accent,
13755
13908
  seed: appConfig.name || manifest.app?.name || appSlug,
13756
- focused: Boolean(focus),
13909
+ focused: Boolean(result.framing?.focus_selector),
13757
13910
  theme: theme || "light"
13758
13911
  });
13759
13912
  } catch (error) {
@@ -14513,76 +14666,86 @@ var appsCommandSpecs = [
14513
14666
 
14514
14667
  // src/command-specs/reports.js
14515
14668
  init_errors();
14516
- var reuse = (command, name = command) => {
14517
- const spec = appsCommandSpecs.find((item) => item.command_path.join(" ") === `apps ${command}`);
14669
+ async function assertReportProject(ctx) {
14670
+ const dir = resolveProjectDir(ctx.args.dir || ".");
14671
+ if ((await loadAppConfig(dir)).kind !== "report") throw usageError('Set kind: "report" in notis.config.ts. Reports have no app installation or owned resources.');
14672
+ return dir;
14673
+ }
14674
+ async function initReport(ctx) {
14675
+ const dir = resolveProjectDir(ctx.args.dir || `./${ctx.args.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`);
14676
+ const result = await appsInitHandler({ ...ctx, args: { ...ctx.args, dir }, options: {}, output: { emitSuccess: (value) => value } });
14677
+ writeFileSync6(join9(dir, "notis.config.ts"), `import { defineNotisApp } from '@notis/sdk/config';
14678
+ export default defineNotisApp({
14679
+ kind: 'report', name: ${JSON.stringify(ctx.args.name)},
14680
+ routes: [{ path: '/', slug: 'home', name: 'Report', default: true }],
14681
+ tools: [],
14682
+ });
14683
+ `);
14684
+ writeFileSync6(join9(dir, "app/page.tsx"), `'use client';
14685
+ import { useNotis, NotisCommentBoundary } from '@notis/sdk';
14686
+ export default function Report() {
14687
+ const { app } = useNotis();
14688
+ return <NotisCommentBoundary><main className="notis-app-shell space-y-6">
14689
+ <h1 className="text-2xl font-semibold">{app?.name || 'Report'}</h1>
14690
+ <p className="text-muted-foreground">Your report is ready to build.</p>
14691
+ </main></NotisCommentBoundary>;
14692
+ }
14693
+ `);
14694
+ rmSync6(join9(dir, "CHANGELOG.md"), { force: true });
14695
+ return ctx.output.emitSuccess({ ...result, command: "reports init", hints: [
14696
+ { command: `cd ${dir} && npm install`, reason: "Install dependencies" },
14697
+ { command: `notis reports build ${dir}`, reason: "Build the standalone report" }
14698
+ ] });
14699
+ }
14700
+ var localReportCommand = (command, name = command) => {
14701
+ const app = appsCommandSpecs.find((item) => item.command_path.join(" ") === `apps ${command}`);
14518
14702
  return {
14519
- ...spec,
14520
- handler: async (ctx) => {
14521
- const output = new Proxy(ctx.output, {
14522
- get(target, key) {
14523
- if (key === "emitSuccess") return (result) => {
14524
- const value2 = { ...result, warnings: (result.warnings || []).filter((warning) => !warning.startsWith("Store readiness:")) };
14525
- if (value2.data?.listing) {
14526
- value2.data = { ...value2.data };
14527
- delete value2.data.listing;
14528
- }
14529
- return target.emitSuccess(value2);
14530
- };
14531
- const value = Reflect.get(target, key);
14532
- return typeof value === "function" ? value.bind(target) : value;
14533
- }
14534
- });
14535
- return spec.handler({ ...ctx, options: { ...ctx.options, listing: false, ...name === "preview" ? { keepOpen: true } : {} }, output });
14536
- },
14703
+ ...app,
14537
14704
  command_path: ["reports", name],
14538
- summary: `${name[0].toUpperCase() + name.slice(1)} a record-owned SDK report locally.${name === "preview" ? " Keeps the preview server and browser session open." : ""}`,
14539
- args_schema: {
14540
- ...spec.args_schema,
14541
- options: (spec.args_schema?.options || []).map((option) => option.flags === "--listing" ? { ...option, description: "Ignored for reports; saving a report does not publish a Store listing." } : option)
14542
- },
14543
- examples: (spec.examples || []).filter((example) => !example.includes("--listing")).map((example) => example.replace(`apps ${command}`, `reports ${name}`)),
14544
- when_to_use: "Author an independent report without deploying its owning app."
14705
+ summary: `${name[0].toUpperCase() + name.slice(1)} a standalone live SDK report.`,
14706
+ when_to_use: "Author a private standalone document using the shared view runtime; no app or database is required.",
14707
+ args_schema: { ...app.args_schema, options: [...(app.args_schema.options || []).filter((option) => !["--listing", "--from <slug>"].includes(option.flags)), ...command === "verify" ? [{ flags: "--document-id <id>", description: "Saved report for live verification." }, { flags: "--expected-revision <revision>", description: "Saved report revision for live verification." }] : []] },
14708
+ examples: [`notis reports ${name} ${command === "init" ? '"Weekly report" ./weekly-report' : "./weekly-report"}`],
14709
+ handler: async (ctx) => {
14710
+ if (command === "init") return initReport(ctx);
14711
+ await assertReportProject(ctx);
14712
+ return (command === "build" ? appsBuildHandler : appsVerifyHandler)({ ...ctx, options: { ...ctx.options, ...name === "preview" ? { keepOpen: true } : {} } });
14713
+ }
14545
14714
  };
14546
14715
  };
14547
14716
  var reportsCommandSpecs = [
14548
- reuse("init"),
14549
- reuse("build"),
14550
- reuse("verify"),
14551
- reuse("verify", "preview"),
14717
+ localReportCommand("init"),
14718
+ localReportCommand("build"),
14719
+ localReportCommand("verify"),
14720
+ localReportCommand("verify", "preview"),
14552
14721
  {
14553
14722
  command_path: ["reports", "save"],
14554
- summary: "Build, verify and save a report into an app-owned database record.",
14555
- when_to_use: "Persist an independently authored report, not an app release.",
14723
+ summary: "Build, verify and save a standalone live report document.",
14724
+ when_to_use: "Persist an independently owned report, not an app release.",
14556
14725
  args_schema: {
14557
14726
  arguments: [{ token: "[dir]", key: "dir", description: "Report source directory." }],
14558
14727
  options: [
14559
- { flags: "--database-id <id>", description: "Required. Owning app database." },
14560
- { flags: "--document-id <id>", description: "Existing record to update or attach to." },
14561
- { flags: "--attach", description: "Attach to an existing non-view record." },
14562
- { flags: "--expected-revision <revision>", description: "Fresh view revision (0 for a record without a view)." },
14563
- { flags: "--title <title>", description: "Required, including updates. Record title." },
14564
- { flags: "--context-file <file>", description: "Required. UTF-8 readable report content and structure." },
14565
- { flags: "--properties-file <file>", description: "JSON database property values keyed by name." }
14728
+ { flags: "--document-id <id>", description: "Existing report document to update." },
14729
+ { flags: "--expected-revision <revision>", description: "Current saved report revision; required for updates." },
14730
+ { flags: "--title <title>", description: "Required, including updates. Document title." },
14731
+ { flags: "--context-file <file>", description: "Required. UTF-8 readable report content and structure." }
14566
14732
  ]
14567
14733
  },
14568
- examples: ['notis reports save ./weekly-report --database-id <id> --title "Weekly review" --context-file ./context.md'],
14734
+ examples: ['notis reports save ./weekly-report --title "Weekly review" --context-file ./context.md'],
14569
14735
  mutates: true,
14570
14736
  idempotent: true,
14571
14737
  backend_call: { type: "tool", name: "LOCAL_NOTIS_SAVE_REPORT" },
14572
14738
  async handler(ctx) {
14573
- const dir = resolveProjectDir(ctx.args.dir || ".");
14574
- if (!ctx.options.databaseId || !ctx.options.title || !ctx.options.contextFile) throw usageError("--database-id, --title and --context-file are required.");
14575
- if (ctx.options.documentId && (!/^\d+$/.test(String(ctx.options.expectedRevision ?? "")) || !Number.isSafeInteger(Number(ctx.options.expectedRevision)))) throw usageError("--expected-revision is required for update/attach.");
14576
- if (ctx.options.attach && !ctx.options.documentId) throw usageError("--attach requires --document-id.");
14739
+ const dir = await assertReportProject(ctx);
14740
+ if (!ctx.options.title || !ctx.options.contextFile) throw usageError("--title and --context-file are required.");
14741
+ if (ctx.options.documentId && (!/^\d+$/.test(String(ctx.options.expectedRevision ?? "")) || !Number.isSafeInteger(Number(ctx.options.expectedRevision)))) throw usageError("--expected-revision is required for update.");
14577
14742
  const context = readFileSync9(resolve6(ctx.options.contextFile), "utf8");
14578
- const properties = ctx.options.propertiesFile ? JSON.parse(readFileSync9(resolve6(ctx.options.propertiesFile), "utf8")) : {};
14579
14743
  await buildArtifact(dir, { stdio: ctx.output.isMachineMode() ? "pipe" : "inherit" });
14580
14744
  const release = prepareAppRelease(dir);
14581
14745
  try {
14582
14746
  if (release.manifest.routes?.length !== 1) throw usageError("Reports require exactly one SDK route.");
14583
14747
  let verification;
14584
- const verify = appsCommandSpecs.find((item) => item.command_path.join(" ") === "apps verify");
14585
- const exit = await verify.handler({ ...ctx, args: { dir: release.projectDir }, options: { skipBuild: true, mode: "stub" }, output: { ...ctx.output, isMachineMode: () => true, emitSuccess: (value) => {
14748
+ const exit = await appsVerifyHandler({ ...ctx, args: { dir: release.projectDir }, options: { skipBuild: true, mode: "stub" }, output: { ...ctx.output, isMachineMode: () => true, emitSuccess: (value) => {
14586
14749
  verification = value;
14587
14750
  } } });
14588
14751
  if (exit !== EXIT_CODES.ok || verification?.data?.status !== "passed") throw usageError("Report verification failed; nothing saved.");
@@ -14593,12 +14756,10 @@ var reportsCommandSpecs = [
14593
14756
  mutating: true,
14594
14757
  idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
14595
14758
  arguments_: {
14596
- operation: ctx.options.attach ? "attach" : ctx.options.documentId ? "update" : "create",
14597
- database_id: ctx.options.databaseId,
14759
+ operation: ctx.options.documentId ? "update" : "create",
14598
14760
  title: ctx.options.title,
14599
- properties,
14600
14761
  ...ctx.options.documentId ? { document_id: ctx.options.documentId, expected_revision: Number(ctx.options.expectedRevision) } : {},
14601
- report: { schema: "notis-report/v2", context, artifact: { manifest: release.manifest, files, encoding: "base64" } }
14762
+ report: { schema: "notis-report/v3", context, artifact: { manifest: release.manifest, files, encoding: "base64" } }
14602
14763
  }
14603
14764
  });
14604
14765
  if (!result?.payload?.document?.id || !result.payload.document.view_revision) throw usageError("Save returned no record identity. Read back before retrying; the outcome may be unknown.");
@@ -15518,7 +15679,7 @@ init_auth_recovery();
15518
15679
  init_profiles();
15519
15680
  init_oauth();
15520
15681
  import { readFileSync as readFileSync13 } from "node:fs";
15521
- import { dirname as dirname12, join as join11 } from "node:path";
15682
+ import { dirname as dirname12, join as join12 } from "node:path";
15522
15683
  import { fileURLToPath as fileURLToPath8 } from "node:url";
15523
15684
 
15524
15685
  // src/command-specs/auth.js
@@ -15536,15 +15697,15 @@ import {
15536
15697
  readFileSync as readFileSync11,
15537
15698
  readdirSync as readdirSync6,
15538
15699
  renameSync as renameSync4,
15539
- writeFileSync as writeFileSync6
15700
+ writeFileSync as writeFileSync7
15540
15701
  } from "node:fs";
15541
15702
  import { createHash as createHash4 } from "node:crypto";
15542
15703
  import { homedir as homedir4 } from "node:os";
15543
- import { dirname as dirname10, join as join9 } from "node:path";
15704
+ import { dirname as dirname10, join as join10 } from "node:path";
15544
15705
  import { fileURLToPath as fileURLToPath7 } from "node:url";
15545
15706
  var HERE = dirname10(fileURLToPath7(import.meta.url));
15546
- var INSTRUCTIONS_PATH = join9(HERE, "..", "..", "skills", "notis-cli", "AGENT_INSTRUCTIONS.md");
15547
- var HOOK_BUNDLE_PATH = join9(HERE, "..", "..", "dist", "agent-hooks", "notis-agent-hook.mjs");
15707
+ var INSTRUCTIONS_PATH = join10(HERE, "..", "..", "skills", "notis-cli", "AGENT_INSTRUCTIONS.md");
15708
+ var HOOK_BUNDLE_PATH = join10(HERE, "..", "..", "dist", "agent-hooks", "notis-agent-hook.mjs");
15548
15709
  var START_MARKER = "<!-- notis-cli:instructions:start -->";
15549
15710
  var END_MARKER = "<!-- notis-cli:instructions:end -->";
15550
15711
  var LEGACY_SENTINEL = "Use the Notis CLI (`npx --package @notis_ai/cli@latest -- notis ...`)";
@@ -15554,18 +15715,18 @@ var AGENT_IDS = Object.freeze(["codex", "claude-code"]);
15554
15715
  function atomicWrite(filePath, contents, mode = 384) {
15555
15716
  mkdirSync7(dirname10(filePath), { recursive: true, mode: 448 });
15556
15717
  const temporaryPath = `${filePath}.notis-${process.pid}-${Date.now()}.tmp`;
15557
- writeFileSync6(temporaryPath, contents, { mode });
15718
+ writeFileSync7(temporaryPath, contents, { mode });
15558
15719
  renameSync4(temporaryPath, filePath);
15559
15720
  }
15560
15721
  function readText(filePath) {
15561
15722
  return existsSync9(filePath) ? readFileSync11(filePath, "utf-8") : "";
15562
15723
  }
15563
15724
  function activeCodexInstructionsPath(home) {
15564
- const overridePath = join9(home, ".codex", "AGENTS.override.md");
15725
+ const overridePath = join10(home, ".codex", "AGENTS.override.md");
15565
15726
  if (existsSync9(overridePath) && readText(overridePath).trim()) {
15566
15727
  return overridePath;
15567
15728
  }
15568
- return join9(home, ".codex", "AGENTS.md");
15729
+ return join10(home, ".codex", "AGENTS.md");
15569
15730
  }
15570
15731
  function upsertInstructionBlock(filePath, block) {
15571
15732
  const existing = readText(filePath);
@@ -15655,7 +15816,7 @@ function installManagedHookRuntime({
15655
15816
  } = {}) {
15656
15817
  const bundle = readFileSync11(bundlePath2);
15657
15818
  const digest = createHash4("sha256").update(bundle).digest("hex");
15658
- const runtimePath = join9(
15819
+ const runtimePath = join10(
15659
15820
  home,
15660
15821
  ".notis",
15661
15822
  "agent-hooks",
@@ -15666,7 +15827,7 @@ function installManagedHookRuntime({
15666
15827
  const existingDigest = existsSync9(runtimePath) ? createHash4("sha256").update(readFileSync11(runtimePath)).digest("hex") : null;
15667
15828
  if (existingDigest !== digest) atomicWrite(runtimePath, bundle, 320);
15668
15829
  chmodSync(runtimePath, 320);
15669
- const launcherPath = join9(
15830
+ const launcherPath = join10(
15670
15831
  home,
15671
15832
  ".notis",
15672
15833
  "agent-hooks",
@@ -15750,16 +15911,16 @@ function upsertPromptHooks(filePath, agentId, profileName, enabled, launcherPath
15750
15911
  function agentPaths(agentId, home) {
15751
15912
  if (agentId === "codex") {
15752
15913
  return {
15753
- root: join9(home, ".codex"),
15914
+ root: join10(home, ".codex"),
15754
15915
  instructions: activeCodexInstructionsPath(home),
15755
- hooks: join9(home, ".codex", "hooks.json")
15916
+ hooks: join10(home, ".codex", "hooks.json")
15756
15917
  };
15757
15918
  }
15758
15919
  if (agentId === "claude-code") {
15759
15920
  return {
15760
- root: join9(home, ".claude"),
15761
- instructions: join9(home, ".claude", "CLAUDE.md"),
15762
- hooks: join9(home, ".claude", "settings.json")
15921
+ root: join10(home, ".claude"),
15922
+ instructions: join10(home, ".claude", "CLAUDE.md"),
15923
+ hooks: join10(home, ".claude", "settings.json")
15763
15924
  };
15764
15925
  }
15765
15926
  throw new Error(`Unsupported agent: ${agentId}`);
@@ -15830,10 +15991,10 @@ import {
15830
15991
  mkdirSync as mkdirSync8,
15831
15992
  readFileSync as readFileSync12,
15832
15993
  renameSync as renameSync5,
15833
- writeFileSync as writeFileSync7
15994
+ writeFileSync as writeFileSync8
15834
15995
  } from "node:fs";
15835
15996
  import { homedir as homedir5 } from "node:os";
15836
- import { dirname as dirname11, join as join10 } from "node:path";
15997
+ import { dirname as dirname11, join as join11 } from "node:path";
15837
15998
  var MAX_SEEN_HASHES = 500;
15838
15999
  var MAX_PROMPT_CHARS = 12e3;
15839
16000
  function hash(value) {
@@ -15841,7 +16002,7 @@ function hash(value) {
15841
16002
  }
15842
16003
  function statePath(sessionId, home = homedir5()) {
15843
16004
  if (!sessionId) return null;
15844
- return join10(home, ".notis", "agent-memory-hooks", `${hash(sessionId)}.json`);
16005
+ return join11(home, ".notis", "agent-memory-hooks", `${hash(sessionId)}.json`);
15845
16006
  }
15846
16007
  function readState(sessionId, home) {
15847
16008
  const filePath = statePath(sessionId, home);
@@ -15879,7 +16040,7 @@ function writeState(sessionId, state, home) {
15879
16040
  if (!filePath) return;
15880
16041
  mkdirSync8(dirname11(filePath), { recursive: true, mode: 448 });
15881
16042
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
15882
- writeFileSync7(temporaryPath, `${JSON.stringify(state)}
16043
+ writeFileSync8(temporaryPath, `${JSON.stringify(state)}
15883
16044
  `, { mode: 384 });
15884
16045
  renameSync5(temporaryPath, filePath);
15885
16046
  }
@@ -16383,7 +16544,7 @@ var authCommandSpecs = [
16383
16544
 
16384
16545
  // src/command-specs/onboarding.js
16385
16546
  var HERE2 = dirname12(fileURLToPath8(import.meta.url));
16386
- var BUNDLED_BRIEF_PATH = join11(HERE2, "..", "..", "skills", "notis-onboarding", "BRIEF.md");
16547
+ var BUNDLED_BRIEF_PATH = join12(HERE2, "..", "..", "skills", "notis-onboarding", "BRIEF.md");
16387
16548
  async function fetchOnboardingState(runtime) {
16388
16549
  try {
16389
16550
  const { payload } = await runToolCommand({
@@ -17239,11 +17400,11 @@ import { createHash as createHash8, randomUUID as randomUUID6 } from "node:crypt
17239
17400
  import {
17240
17401
  mkdtempSync as mkdtempSync4,
17241
17402
  readFileSync as readFileSync15,
17242
- rmSync as rmSync6,
17243
- writeFileSync as writeFileSync8
17403
+ rmSync as rmSync7,
17404
+ writeFileSync as writeFileSync9
17244
17405
  } from "node:fs";
17245
17406
  import { tmpdir as tmpdir3 } from "node:os";
17246
- import { basename as basename6, join as join12 } from "node:path";
17407
+ import { basename as basename6, join as join13 } from "node:path";
17247
17408
  import { setTimeout as delay2 } from "node:timers/promises";
17248
17409
  function discoveryNames(payload) {
17249
17410
  return [...new Set(
@@ -17324,12 +17485,12 @@ function remotePath(folder, name) {
17324
17485
  return `${normalizedFolder}/${name}`;
17325
17486
  }
17326
17487
  async function smokeFileUploadHandler(ctx) {
17327
- const tempDir = mkdtempSync4(join12(tmpdir3(), "notis-smoke-file-upload-"));
17488
+ const tempDir = mkdtempSync4(join13(tmpdir3(), "notis-smoke-file-upload-"));
17328
17489
  const marker = `${(/* @__PURE__ */ new Date()).toISOString().replaceAll(/[-:.]/g, "")}-${randomUUID6().slice(0, 8)}`;
17329
17490
  const generatedFixture = !ctx.options.sourceFile;
17330
- const fixturePath = ctx.options.sourceFile || join12(tempDir, `notis-file-upload-${marker}.txt`);
17491
+ const fixturePath = ctx.options.sourceFile || join13(tempDir, `notis-file-upload-${marker}.txt`);
17331
17492
  if (generatedFixture) {
17332
- writeFileSync8(
17493
+ writeFileSync9(
17333
17494
  fixturePath,
17334
17495
  `Notis Dropbox file-upload smoke
17335
17496
  marker=${marker}
@@ -17552,7 +17713,7 @@ unicode=\xE9t\xE9 Z\xFCrich \u6771\u4EAC
17552
17713
  result.cleanup.deleted = false;
17553
17714
  }
17554
17715
  }
17555
- rmSync6(tempDir, { recursive: true, force: true });
17716
+ rmSync7(tempDir, { recursive: true, force: true });
17556
17717
  }
17557
17718
  }
17558
17719
  var smokeCommandSpecs = [
@@ -18318,7 +18479,7 @@ init_profiles();
18318
18479
  import { createHash as createHash9, randomUUID as randomUUID7 } from "node:crypto";
18319
18480
  import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
18320
18481
  import { homedir as homedir7 } from "node:os";
18321
- import { dirname as dirname14, join as join14 } from "node:path";
18482
+ import { dirname as dirname14, join as join15 } from "node:path";
18322
18483
 
18323
18484
  // src/runtime/base-skills.js
18324
18485
  import {
@@ -18328,11 +18489,11 @@ import {
18328
18489
  mkdirSync as mkdirSync9,
18329
18490
  readlinkSync,
18330
18491
  renameSync as renameSync6,
18331
- rmSync as rmSync7,
18492
+ rmSync as rmSync8,
18332
18493
  symlinkSync
18333
18494
  } from "node:fs";
18334
18495
  import { homedir as homedir6 } from "node:os";
18335
- import { dirname as dirname13, join as join13, relative as relative5, resolve as resolve8 } from "node:path";
18496
+ import { dirname as dirname13, join as join14, relative as relative5, resolve as resolve8 } from "node:path";
18336
18497
  import { fileURLToPath as fileURLToPath9 } from "node:url";
18337
18498
  var BASE_SKILL_NAMES = Object.freeze([
18338
18499
  "notis-apps",
@@ -18343,29 +18504,29 @@ var HERE3 = dirname13(fileURLToPath9(import.meta.url));
18343
18504
  function resolveBundledBaseSkillsRoot({ resourcesPath = process.resourcesPath } = {}) {
18344
18505
  const sourceCheckout = resolve8(HERE3, "../../../../server/skills");
18345
18506
  const packaged = resolve8(HERE3, "../../dist/base-skills");
18346
- const desktopResource = resourcesPath ? join13(resourcesPath, "base-skills") : null;
18507
+ const desktopResource = resourcesPath ? join14(resourcesPath, "base-skills") : null;
18347
18508
  const desktopPackagerRoot = resourcesPath || null;
18348
18509
  for (const candidate of [sourceCheckout, packaged, desktopResource, desktopPackagerRoot]) {
18349
- if (candidate && BASE_SKILL_NAMES.every((name) => existsSync12(join13(candidate, name, "SKILL.md")))) {
18510
+ if (candidate && BASE_SKILL_NAMES.every((name) => existsSync12(join14(candidate, name, "SKILL.md")))) {
18350
18511
  return candidate;
18351
18512
  }
18352
18513
  }
18353
18514
  return packaged;
18354
18515
  }
18355
18516
  function getBaseSkillPaths({ home = homedir6(), userId = null } = {}) {
18356
- const notisRoot = join13(home, ".notis", "skills");
18517
+ const notisRoot = join14(home, ".notis", "skills");
18357
18518
  const targetRoots = [
18358
- join13(home, ".agents", "skills"),
18359
- join13(home, ".codex", "skills"),
18360
- join13(home, ".cursor", "skills"),
18361
- join13(home, ".claude", "skills")
18519
+ join14(home, ".agents", "skills"),
18520
+ join14(home, ".codex", "skills"),
18521
+ join14(home, ".cursor", "skills"),
18522
+ join14(home, ".claude", "skills")
18362
18523
  ];
18363
18524
  if (userId) {
18364
- targetRoots.push(join13(notisRoot, "users", userId, "skills"));
18525
+ targetRoots.push(join14(notisRoot, "users", userId, "skills"));
18365
18526
  }
18366
18527
  return {
18367
- baseRoot: join13(notisRoot, "base"),
18368
- backupRoot: join13(notisRoot, "backups"),
18528
+ baseRoot: join14(notisRoot, "base"),
18529
+ backupRoot: join14(notisRoot, "backups"),
18369
18530
  targetRoots
18370
18531
  };
18371
18532
  }
@@ -18378,7 +18539,7 @@ function sameLinkTarget(linkPath, expectedTarget) {
18378
18539
  }
18379
18540
  }
18380
18541
  function backupConflict(linkPath, backupRoot, targetLabel, skillName, now) {
18381
- const backupPath = join13(backupRoot, String(now), targetLabel, skillName);
18542
+ const backupPath = join14(backupRoot, String(now), targetLabel, skillName);
18382
18543
  mkdirSync9(dirname13(backupPath), { recursive: true, mode: 448 });
18383
18544
  renameSync6(linkPath, backupPath);
18384
18545
  return backupPath;
@@ -18387,7 +18548,7 @@ function replaceDirectory(source, destination) {
18387
18548
  mkdirSync9(dirname13(destination), { recursive: true, mode: 448 });
18388
18549
  const temporary = `${destination}.tmp-${process.pid}-${Date.now()}`;
18389
18550
  const previous = `${destination}.previous-${process.pid}-${Date.now()}`;
18390
- rmSync7(temporary, { recursive: true, force: true });
18551
+ rmSync8(temporary, { recursive: true, force: true });
18391
18552
  cpSync2(source, temporary, { recursive: true });
18392
18553
  let movedPrevious = false;
18393
18554
  try {
@@ -18396,9 +18557,9 @@ function replaceDirectory(source, destination) {
18396
18557
  movedPrevious = true;
18397
18558
  }
18398
18559
  renameSync6(temporary, destination);
18399
- if (movedPrevious) rmSync7(previous, { recursive: true, force: true });
18560
+ if (movedPrevious) rmSync8(previous, { recursive: true, force: true });
18400
18561
  } catch (error) {
18401
- rmSync7(temporary, { recursive: true, force: true });
18562
+ rmSync8(temporary, { recursive: true, force: true });
18402
18563
  if (movedPrevious && !existsSync12(destination) && existsSync12(previous)) {
18403
18564
  renameSync6(previous, destination);
18404
18565
  }
@@ -18420,19 +18581,19 @@ function reconcileBaseSkills({
18420
18581
  skills: [...BASE_SKILL_NAMES]
18421
18582
  };
18422
18583
  for (const name of BASE_SKILL_NAMES) {
18423
- const source = join13(sourceRoot, name);
18424
- if (!existsSync12(join13(source, "SKILL.md"))) {
18584
+ const source = join14(sourceRoot, name);
18585
+ if (!existsSync12(join14(source, "SKILL.md"))) {
18425
18586
  throw new Error(`Bundled base skill is missing: ${name}`);
18426
18587
  }
18427
- replaceDirectory(source, join13(paths.baseRoot, name));
18588
+ replaceDirectory(source, join14(paths.baseRoot, name));
18428
18589
  result.installed += 1;
18429
18590
  }
18430
18591
  for (const targetRoot of paths.targetRoots) {
18431
18592
  mkdirSync9(targetRoot, { recursive: true, mode: 448 });
18432
18593
  const targetLabel = relative5(home, targetRoot).replaceAll("/", "_") || "home";
18433
18594
  for (const name of BASE_SKILL_NAMES) {
18434
- const skillTarget = join13(paths.baseRoot, name);
18435
- const linkPath = join13(targetRoot, name);
18595
+ const skillTarget = join14(paths.baseRoot, name);
18596
+ const linkPath = join14(targetRoot, name);
18436
18597
  if (sameLinkTarget(linkPath, skillTarget)) {
18437
18598
  result.unchanged += 1;
18438
18599
  continue;
@@ -18466,7 +18627,7 @@ var DEFAULT_LOCK_POLL_MS = 50;
18466
18627
  async function lockSnapshot(lockDirectory) {
18467
18628
  try {
18468
18629
  const [raw, metadata] = await Promise.all([
18469
- readFile(join14(lockDirectory, "owner"), "utf8").catch(() => ""),
18630
+ readFile(join15(lockDirectory, "owner"), "utf8").catch(() => ""),
18470
18631
  stat(lockDirectory)
18471
18632
  ]);
18472
18633
  let owner = {};
@@ -18502,11 +18663,11 @@ function delay3(milliseconds) {
18502
18663
  return new Promise((resolve9) => setTimeout(resolve9, milliseconds));
18503
18664
  }
18504
18665
  async function quarantineStaleLock(lockDirectory, snapshot) {
18505
- const quarantineRoot = join14(dirname14(lockDirectory), ".stale-operation-locks");
18666
+ const quarantineRoot = join15(dirname14(lockDirectory), ".stale-operation-locks");
18506
18667
  const snapshotToken = createHash9("sha256").update(JSON.stringify(snapshot)).digest("hex");
18507
18668
  await mkdir(quarantineRoot, { recursive: true, mode: 448 });
18508
18669
  try {
18509
- await rename(lockDirectory, join14(quarantineRoot, snapshotToken));
18670
+ await rename(lockDirectory, join15(quarantineRoot, snapshotToken));
18510
18671
  return true;
18511
18672
  } catch (error) {
18512
18673
  if (["EEXIST", "ENOTEMPTY", "ENOENT"].includes(error?.code)) return false;
@@ -18514,16 +18675,16 @@ async function quarantineStaleLock(lockDirectory, snapshot) {
18514
18675
  }
18515
18676
  }
18516
18677
  async function writeLockOwnerAtomically(lockDirectory, owner) {
18517
- const temporaryOwnerPath = join14(lockDirectory, `.owner.${owner.id}.tmp`);
18518
- const ownerPath = join14(lockDirectory, "owner");
18678
+ const temporaryOwnerPath = join15(lockDirectory, `.owner.${owner.id}.tmp`);
18679
+ const ownerPath = join15(lockDirectory, "owner");
18519
18680
  await writeFile(temporaryOwnerPath, JSON.stringify(owner), { mode: 384 });
18520
18681
  await rename(temporaryOwnerPath, ownerPath);
18521
18682
  }
18522
18683
  async function releaseOwnedLock(lockDirectory, ownerId) {
18523
18684
  const owner = await lockSnapshot(lockDirectory);
18524
18685
  if (owner?.id !== ownerId) return;
18525
- const quarantineRoot = join14(dirname14(lockDirectory), ".stale-operation-locks");
18526
- const releasedDirectory = join14(quarantineRoot, `released.${ownerId}`);
18686
+ const quarantineRoot = join15(dirname14(lockDirectory), ".stale-operation-locks");
18687
+ const releasedDirectory = join15(quarantineRoot, `released.${ownerId}`);
18527
18688
  await mkdir(quarantineRoot, { recursive: true, mode: 448 });
18528
18689
  try {
18529
18690
  await rename(lockDirectory, releasedDirectory);
@@ -18540,7 +18701,7 @@ async function withSkillSyncLock(callback, {
18540
18701
  pollMs = DEFAULT_LOCK_POLL_MS,
18541
18702
  now = () => Date.now()
18542
18703
  } = {}) {
18543
- const lockDirectory = join14(home, ".notis", "skills", ".operation-lock");
18704
+ const lockDirectory = join15(home, ".notis", "skills", ".operation-lock");
18544
18705
  const ownerId = `${process.pid}.${randomUUID7()}`;
18545
18706
  const deadline = now() + timeoutMs;
18546
18707
  await mkdir(dirname14(lockDirectory), { recursive: true, mode: 448 });
@@ -18579,7 +18740,7 @@ async function withSkillSyncLock(callback, {
18579
18740
  let heartbeatInFlight = Promise.resolve();
18580
18741
  const refreshHeartbeat = async () => {
18581
18742
  if (heartbeatStopped) return;
18582
- const temporaryOwnerPath = join14(lockDirectory, `.owner.${ownerId}.tmp`);
18743
+ const temporaryOwnerPath = join15(lockDirectory, `.owner.${ownerId}.tmp`);
18583
18744
  await writeFile(
18584
18745
  temporaryOwnerPath,
18585
18746
  JSON.stringify({ id: ownerId, pid: process.pid, at: now() }),
@@ -18590,7 +18751,7 @@ async function withSkillSyncLock(callback, {
18590
18751
  await rm(temporaryOwnerPath, { force: true });
18591
18752
  return;
18592
18753
  }
18593
- await rename(temporaryOwnerPath, join14(lockDirectory, "owner"));
18754
+ await rename(temporaryOwnerPath, join15(lockDirectory, "owner"));
18594
18755
  };
18595
18756
  const heartbeat = setInterval(() => {
18596
18757
  heartbeatInFlight = heartbeatInFlight.then(refreshHeartbeat).catch(() => void 0);
@@ -18640,16 +18801,16 @@ init_oauth();
18640
18801
  init_profiles();
18641
18802
  import { spawnSync as spawnSync3 } from "node:child_process";
18642
18803
  import { createHash as createHash11 } from "node:crypto";
18643
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync17, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "node:fs";
18804
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync17, renameSync as renameSync7, writeFileSync as writeFileSync10 } from "node:fs";
18644
18805
  import { homedir as homedir8 } from "node:os";
18645
- import { dirname as dirname15, join as join15 } from "node:path";
18806
+ import { dirname as dirname15, join as join16 } from "node:path";
18646
18807
  import { fileURLToPath as fileURLToPath10 } from "node:url";
18647
18808
  var SKILL_SYNC_SERVICE_LABEL = "ai.notis.skills-sync";
18648
18809
  var bundlePath = fileURLToPath10(new URL("../../dist/skill-sync-worker.mjs", import.meta.url));
18649
18810
  function atomicWrite2(target, value, mode = 384) {
18650
18811
  mkdirSync10(dirname15(target), { recursive: true, mode: 448 });
18651
18812
  const temporary = `${target}.${process.pid}.tmp`;
18652
- writeFileSync9(temporary, value, { mode });
18813
+ writeFileSync10(temporary, value, { mode });
18653
18814
  renameSync7(temporary, target);
18654
18815
  }
18655
18816
  function xml(value) {
@@ -18662,7 +18823,7 @@ async function maybeInstallSkillSyncService(runtime, {
18662
18823
  refresh
18663
18824
  } = {}) {
18664
18825
  if (process.platform !== "darwin" || runtime.credentialKind !== "oauth" || config.current_profile !== runtime.profileName || !["https://api.notis.ai", "https://api-beta.notis.ai"].includes(runtime.apiBase)) return;
18665
- const plist = join15(homedir8(), "Library", "LaunchAgents", `${SKILL_SYNC_SERVICE_LABEL}.plist`);
18826
+ const plist = join16(homedir8(), "Library", "LaunchAgents", `${SKILL_SYNC_SERVICE_LABEL}.plist`);
18666
18827
  if (existsSync13(plist)) {
18667
18828
  const saved = readFileSync17(plist, "utf8");
18668
18829
  if (runtime.oauthUserId && [runtime.profileName, runtime.apiBase, runtime.oauthUserId].every((value) => saved.includes(`<string>${xml(value)}</string>`))) return install(runtime);
@@ -18696,10 +18857,10 @@ async function installSkillSyncServiceLocked(runtime, {
18696
18857
  if (runtime.credentialKind !== "oauth" || runtime.envCredentialOverride || !isValidProfileName(runtime.profileName) || !["https://api.notis.ai", "https://api-beta.notis.ai"].includes(runtime.apiBase)) {
18697
18858
  return { status: "skipped_non_personal_profile" };
18698
18859
  }
18699
- const root = join15(home, ".notis", "skills", "service");
18860
+ const root = join16(home, ".notis", "skills", "service");
18700
18861
  const bundle = readFileSync17(source);
18701
18862
  const digest = createHash11("sha256").update(bundle).digest("hex");
18702
- const installedBundle = join15(root, "runtime", digest, "worker.mjs");
18863
+ const installedBundle = join16(root, "runtime", digest, "worker.mjs");
18703
18864
  if (!existsSync13(installedBundle) || !readFileSync17(installedBundle).equals(bundle)) atomicWrite2(installedBundle, bundle, 320);
18704
18865
  const args = [nodePath, installedBundle, runtime.profileName, runtime.apiBase, runtime.oauthUserId];
18705
18866
  if (!runtime.oauthUserId) return { status: "skipped_missing_account" };
@@ -18713,7 +18874,7 @@ async function installSkillSyncServiceLocked(runtime, {
18713
18874
  <key>ProcessType</key><string>Background</string>
18714
18875
  </dict></plist>
18715
18876
  `;
18716
- const plistPath = join15(home, "Library", "LaunchAgents", `${SKILL_SYNC_SERVICE_LABEL}.plist`);
18877
+ const plistPath = join16(home, "Library", "LaunchAgents", `${SKILL_SYNC_SERVICE_LABEL}.plist`);
18717
18878
  const domain = `gui/${uid}`;
18718
18879
  const target = `${domain}/${SKILL_SYNC_SERVICE_LABEL}`;
18719
18880
  const unchanged = existsSync13(plistPath) && readFileSync17(plistPath, "utf8") === plist;
@@ -18916,7 +19077,7 @@ init_channel();
18916
19077
  init_profiles();
18917
19078
  function readCliVersion() {
18918
19079
  try {
18919
- const manifestPath = join16(dirname16(fileURLToPath11(import.meta.url)), "..", "package.json");
19080
+ const manifestPath = join17(dirname16(fileURLToPath11(import.meta.url)), "..", "package.json");
18920
19081
  const version = JSON.parse(readFileSync18(manifestPath, "utf-8")).version;
18921
19082
  if (typeof version === "string" && version.trim()) {
18922
19083
  return version.trim();