@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.
- package/README.md +25 -39
- package/dist/agent-hooks/notis-agent-hook.mjs +315 -154
- package/dist/base-skills/notis-apps/SKILL.md +2 -0
- package/dist/base-skills/notis-apps/references/architecture.md +5 -2
- package/dist/base-skills/notis-apps/references/release.md +44 -0
- package/dist/sdk/src/config.ts +4 -0
- package/dist/sdk/src/hooks/useNotis.ts +3 -1
- package/dist/sdk/src/index.ts +1 -0
- package/dist/sdk/src/runtime.ts +13 -0
- package/package.json +1 -1
- package/skills/notis-apps/cli.md +23 -37
- package/src/command-specs/apps.js +20 -15
- package/src/command-specs/reports.js +61 -50
- package/src/runtime/agent-browser.js +66 -16
- package/src/runtime/app-platform.js +25 -1
- package/src/runtime/app-test-server.js +1 -0
- package/src/runtime/screenshot-framing.js +85 -0
- package/src/runtime/store-screenshot.js +10 -7
- package/template/.harness/index.html.tmpl +12 -4
|
@@ -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
|
|
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 (
|
|
11929
|
-
|
|
11930
|
-
|
|
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 (
|
|
12078
|
-
screenshotArgs.push(
|
|
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
|
-
|
|
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 (
|
|
12243
|
+
if (captureFocus) {
|
|
12099
12244
|
framing = {
|
|
12100
12245
|
...framing || {},
|
|
12101
|
-
focus_selector:
|
|
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 /
|
|
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 *
|
|
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
|
|
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: "
|
|
12340
|
-
|
|
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
|
-
|
|
13430
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
14517
|
-
const
|
|
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
|
-
...
|
|
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
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
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
|
-
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
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
|
|
14555
|
-
when_to_use: "Persist an independently
|
|
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: "--
|
|
14560
|
-
{ flags: "--
|
|
14561
|
-
{ flags: "--
|
|
14562
|
-
{ flags: "--
|
|
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 --
|
|
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 =
|
|
14574
|
-
if (!ctx.options.
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
15547
|
-
var HOOK_BUNDLE_PATH =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
15914
|
+
root: join10(home, ".codex"),
|
|
15754
15915
|
instructions: activeCodexInstructionsPath(home),
|
|
15755
|
-
hooks:
|
|
15916
|
+
hooks: join10(home, ".codex", "hooks.json")
|
|
15756
15917
|
};
|
|
15757
15918
|
}
|
|
15758
15919
|
if (agentId === "claude-code") {
|
|
15759
15920
|
return {
|
|
15760
|
-
root:
|
|
15761
|
-
instructions:
|
|
15762
|
-
hooks:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
17243
|
-
writeFileSync as
|
|
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
|
|
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(
|
|
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 ||
|
|
17491
|
+
const fixturePath = ctx.options.sourceFile || join13(tempDir, `notis-file-upload-${marker}.txt`);
|
|
17331
17492
|
if (generatedFixture) {
|
|
17332
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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(
|
|
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 =
|
|
18517
|
+
const notisRoot = join14(home, ".notis", "skills");
|
|
18357
18518
|
const targetRoots = [
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
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(
|
|
18525
|
+
targetRoots.push(join14(notisRoot, "users", userId, "skills"));
|
|
18365
18526
|
}
|
|
18366
18527
|
return {
|
|
18367
|
-
baseRoot:
|
|
18368
|
-
backupRoot:
|
|
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 =
|
|
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
|
-
|
|
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)
|
|
18560
|
+
if (movedPrevious) rmSync8(previous, { recursive: true, force: true });
|
|
18400
18561
|
} catch (error) {
|
|
18401
|
-
|
|
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 =
|
|
18424
|
-
if (!existsSync12(
|
|
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,
|
|
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 =
|
|
18435
|
-
const linkPath =
|
|
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(
|
|
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 =
|
|
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,
|
|
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 =
|
|
18518
|
-
const ownerPath =
|
|
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 =
|
|
18526
|
-
const releasedDirectory =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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();
|