@notis_ai/cli 0.2.5 → 0.2.7
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 +33 -9
- package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +7 -0
- package/dist/scaffolds/notis-affiliate-prospects/README.md +20 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +42 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +248 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +134 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +108 -0
- package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +35 -0
- package/dist/scaffolds/notis-affiliate-prospects/components.json +20 -0
- package/dist/scaffolds/notis-affiliate-prospects/index.html +12 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +218 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +52 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +100 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +25 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +187 -0
- package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +65 -0
- package/dist/scaffolds/notis-affiliate-prospects/package.json +32 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/package.json +32 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectActionBar.tsx +273 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +90 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useMultiSelect.ts +503 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/index.ts +54 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/runtime.ts +161 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +38 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +72 -0
- package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +242 -0
- package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +51 -0
- package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +23 -0
- package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +11 -0
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/notis.config.ts +0 -1
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
- package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
- package/dist/scaffolds/notis-journal/app/globals.css +37 -0
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
- package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-journal/components.json +20 -0
- package/dist/scaffolds/notis-journal/index.html +12 -0
- package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
- package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
- package/dist/scaffolds/notis-journal/package.json +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
- package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
- package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
- package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +17 -373
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
- package/dist/scaffolds.json +22 -0
- package/package.json +3 -3
- package/skills/notis-apps/SKILL.md +43 -5
- package/skills/notis-apps/cli.md +22 -6
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/cli.js +8 -1
- package/src/command-specs/apps.js +307 -36
- package/src/command-specs/diagnostics.js +674 -0
- package/src/command-specs/helpers.js +4 -1
- package/src/command-specs/index.js +7 -1
- package/src/command-specs/meta.js +8 -2
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +237 -44
- package/src/runtime/agent-browser.js +204 -21
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +21 -4
- package/src/runtime/app-dev-sessions.js +99 -1
- package/src/runtime/app-platform.js +197 -18
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/output.js +12 -1
- package/src/runtime/profiles.js +72 -16
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +24 -82
- package/template/.harness/index.html.tmpl +128 -6
- package/template/CHANGELOG.md +5 -0
- package/template/app/page.tsx +11 -41
- package/template/notis.config.ts +0 -1
- package/template/package-lock.json +4137 -0
- package/template/package.json +1 -0
- package/template/packages/sdk/package.json +4 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
- package/template/packages/sdk/src/config.ts +74 -0
- package/template/packages/sdk/src/documents.ts +229 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
- package/template/packages/sdk/src/index.ts +30 -1
- package/template/packages/sdk/src/runtime.ts +76 -17
- package/template/packages/sdk/src/styles.css +148 -0
- /package/{template → dist/scaffolds/notis-affiliate-prospects}/packages/sdk/src/hooks/useDatabase.ts +0 -0
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
validateArguments,
|
|
18
18
|
} from './helpers.js';
|
|
19
19
|
|
|
20
|
-
async function resolveJsonInput(value, label) {
|
|
20
|
+
export async function resolveJsonInput(value, label) {
|
|
21
21
|
if (!value) return undefined;
|
|
22
22
|
|
|
23
23
|
if (value === '-') {
|
|
@@ -39,6 +39,159 @@ async function resolveJsonInput(value, label) {
|
|
|
39
39
|
return parseJson(value, label);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const READ_ACTIONS = new Set([
|
|
43
|
+
'CHECK',
|
|
44
|
+
'DESCRIBE',
|
|
45
|
+
'DOWNLOAD',
|
|
46
|
+
'FETCH',
|
|
47
|
+
'FIND',
|
|
48
|
+
'GET',
|
|
49
|
+
'INSPECT',
|
|
50
|
+
'LIST',
|
|
51
|
+
'LOOKUP',
|
|
52
|
+
'PREVIEW',
|
|
53
|
+
'QUERY',
|
|
54
|
+
'READ',
|
|
55
|
+
'SEARCH',
|
|
56
|
+
'STATUS',
|
|
57
|
+
'VERIFY',
|
|
58
|
+
]);
|
|
59
|
+
const WRITE_ACTIONS = new Set([
|
|
60
|
+
'AUTHENTIFY',
|
|
61
|
+
'CANCEL',
|
|
62
|
+
'CONNECT',
|
|
63
|
+
'COPY',
|
|
64
|
+
'CREATE',
|
|
65
|
+
'DELETE',
|
|
66
|
+
'DEPLOY',
|
|
67
|
+
'DISCARD',
|
|
68
|
+
'EDIT',
|
|
69
|
+
'GRANT',
|
|
70
|
+
'INCREMENT',
|
|
71
|
+
'INSERT',
|
|
72
|
+
'LINK',
|
|
73
|
+
'MOVE',
|
|
74
|
+
'POST',
|
|
75
|
+
'PUBLISH',
|
|
76
|
+
'REFUND',
|
|
77
|
+
'REMOVE',
|
|
78
|
+
'RENAME',
|
|
79
|
+
'SAVE',
|
|
80
|
+
'SEND',
|
|
81
|
+
'SUBMIT',
|
|
82
|
+
'UPDATE',
|
|
83
|
+
'UPLOAD',
|
|
84
|
+
'UPSERT',
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
function splitSqlStatements(query) {
|
|
88
|
+
const statements = [];
|
|
89
|
+
let current = '';
|
|
90
|
+
let quote = null;
|
|
91
|
+
let dollarTag = null;
|
|
92
|
+
for (let index = 0; index < query.length; index += 1) {
|
|
93
|
+
const char = query[index];
|
|
94
|
+
if (dollarTag) {
|
|
95
|
+
if (query.startsWith(dollarTag, index)) {
|
|
96
|
+
current += dollarTag;
|
|
97
|
+
index += dollarTag.length - 1;
|
|
98
|
+
dollarTag = null;
|
|
99
|
+
} else {
|
|
100
|
+
current += char;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (quote) {
|
|
105
|
+
current += char;
|
|
106
|
+
if (char === quote) {
|
|
107
|
+
if (query[index + 1] === quote) {
|
|
108
|
+
current += query[index + 1];
|
|
109
|
+
index += 1;
|
|
110
|
+
} else {
|
|
111
|
+
quote = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (char === "'" || char === '"') {
|
|
117
|
+
quote = char;
|
|
118
|
+
current += char;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (char === '$') {
|
|
122
|
+
const match = query.slice(index).match(/^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/);
|
|
123
|
+
if (match) {
|
|
124
|
+
dollarTag = match[0];
|
|
125
|
+
current += dollarTag;
|
|
126
|
+
index += dollarTag.length - 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (char === ';') {
|
|
131
|
+
if (current.trim()) statements.push(current.trim());
|
|
132
|
+
current = '';
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
current += char;
|
|
136
|
+
}
|
|
137
|
+
if (current.trim()) statements.push(current.trim());
|
|
138
|
+
return statements;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function classifySqlMutation(query) {
|
|
142
|
+
const statements = splitSqlStatements(query);
|
|
143
|
+
if (statements.length > 1) {
|
|
144
|
+
const classifications = statements.map(classifySqlMutation);
|
|
145
|
+
if (classifications.includes(true)) return true;
|
|
146
|
+
if (classifications.every((classification) => classification === false)) return false;
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const statement = statements[0] || '';
|
|
150
|
+
if (statement) {
|
|
151
|
+
const normalizedStatement = statement.toUpperCase();
|
|
152
|
+
if (/^EXPLAIN\b/.test(normalizedStatement)) {
|
|
153
|
+
const executes = /^EXPLAIN\s+(?:\([^)]*\bANALYZE\b[^)]*\)\s*|ANALYZE\s+)/.test(normalizedStatement);
|
|
154
|
+
if (!executes) return false;
|
|
155
|
+
return /\b(ALTER|CREATE|DELETE|DROP|GRANT|INSERT|MERGE|REVOKE|TRUNCATE|UPDATE)\b/.test(normalizedStatement);
|
|
156
|
+
}
|
|
157
|
+
if (/^SHOW\b/.test(normalizedStatement)) return false;
|
|
158
|
+
// PostgreSQL SELECT can call side-effecting functions. Without catalog
|
|
159
|
+
// knowledge it is not provably read-only, so preserve idempotency and report
|
|
160
|
+
// an unknown classification instead of claiming mutating=false.
|
|
161
|
+
if (/^SELECT\b/.test(normalizedStatement)) return null;
|
|
162
|
+
if (/^WITH\b/.test(normalizedStatement)) {
|
|
163
|
+
return /\b(ALTER|CREATE|DELETE|DROP|GRANT|INSERT|MERGE|REVOKE|TRUNCATE|UPDATE)\b/.test(normalizedStatement)
|
|
164
|
+
? true
|
|
165
|
+
: null;
|
|
166
|
+
}
|
|
167
|
+
if (/^(ALTER|CREATE|DELETE|DROP|GRANT|INSERT|MERGE|REVOKE|TRUNCATE|UPDATE)\b/.test(normalizedStatement)) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function classifyToolMutation(toolName, args = {}) {
|
|
175
|
+
const normalized = localNotisToolSlug(toolName).toUpperCase();
|
|
176
|
+
if (normalized.includes('EXECUTE_SQL') && typeof args.query === 'string') {
|
|
177
|
+
const query = args.query
|
|
178
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
179
|
+
.replace(/--[^\n]*/g, ' ')
|
|
180
|
+
.trim();
|
|
181
|
+
const sqlClassification = classifySqlMutation(query);
|
|
182
|
+
if (sqlClassification !== null) return sqlClassification;
|
|
183
|
+
}
|
|
184
|
+
if (/(?:^|_)(?:FIND|GET|LOOKUP|SEARCH)_OR_(?:CREATE|INSERT|SAVE|UPDATE|UPSERT)(?:_|$)/.test(normalized)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
const tokens = normalized.split(/[^A-Z0-9]+/).filter(Boolean);
|
|
188
|
+
for (const token of tokens) {
|
|
189
|
+
if (WRITE_ACTIONS.has(token)) return true;
|
|
190
|
+
if (READ_ACTIONS.has(token)) return false;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
42
195
|
const LONG_RUNNING_TOOL_TIMEOUT_MS = 600000;
|
|
43
196
|
const LONG_RUNNING_TOOL_NAMES = new Set([
|
|
44
197
|
'LOCAL_NOTIS_GENERATE_IMAGE_OPENAI',
|
|
@@ -67,7 +220,7 @@ function guessContentType(filePath) {
|
|
|
67
220
|
return EXTENSION_CONTENT_TYPES.get(lower.slice(dotIndex)) || 'application/octet-stream';
|
|
68
221
|
}
|
|
69
222
|
|
|
70
|
-
async function hashFileSha256(localPath) {
|
|
223
|
+
export async function hashFileSha256(localPath) {
|
|
71
224
|
const hash = createHash('sha256');
|
|
72
225
|
for await (const chunk of createReadStream(localPath)) {
|
|
73
226
|
hash.update(chunk);
|
|
@@ -123,7 +276,7 @@ async function parseFileBindingSpec(spec, index) {
|
|
|
123
276
|
};
|
|
124
277
|
}
|
|
125
278
|
|
|
126
|
-
async function parseFileBindings(rawFileOptions) {
|
|
279
|
+
export async function parseFileBindings(rawFileOptions) {
|
|
127
280
|
const values = Array.isArray(rawFileOptions)
|
|
128
281
|
? rawFileOptions
|
|
129
282
|
: (rawFileOptions ? [rawFileOptions] : []);
|
|
@@ -227,7 +380,12 @@ async function toolsDescribeHandler(ctx) {
|
|
|
227
380
|
|
|
228
381
|
async function toolsExecHandler(ctx) {
|
|
229
382
|
const requestedToolName = localNotisToolSlug(ctx.args.toolName);
|
|
383
|
+
let targetMutating = classifyToolMutation(requestedToolName);
|
|
230
384
|
ensureLongRunningToolTimeout(ctx, requestedToolName);
|
|
385
|
+
ctx.output.emitProgress({
|
|
386
|
+
phase: 'prepare',
|
|
387
|
+
message: `Resolving ${requestedToolName}`,
|
|
388
|
+
});
|
|
231
389
|
const fileBindings = await parseFileBindings(ctx.options.file);
|
|
232
390
|
|
|
233
391
|
if (ctx.options.getSchema) {
|
|
@@ -243,10 +401,16 @@ async function toolsExecHandler(ctx) {
|
|
|
243
401
|
});
|
|
244
402
|
}
|
|
245
403
|
|
|
246
|
-
|
|
404
|
+
if (ctx.options.argumentsFile && ctx.options.arguments) {
|
|
405
|
+
throw usageError('Use either --arguments or --arguments-file, not both.');
|
|
406
|
+
}
|
|
407
|
+
const rawArguments = ctx.options.argumentsFile
|
|
408
|
+
? `@${ctx.options.argumentsFile}`
|
|
409
|
+
: (ctx.options.arguments || '{}');
|
|
247
410
|
const args = rawArguments === '-' || rawArguments.startsWith('@')
|
|
248
411
|
? await resolveJsonInput(rawArguments, 'arguments') || {}
|
|
249
412
|
: parseMaybeJson(rawArguments, 'arguments') || {};
|
|
413
|
+
targetMutating = classifyToolMutation(requestedToolName, args);
|
|
250
414
|
|
|
251
415
|
if (ctx.options.dryRun) {
|
|
252
416
|
if (fileBindings.length) {
|
|
@@ -278,6 +442,10 @@ async function toolsExecHandler(ctx) {
|
|
|
278
442
|
}
|
|
279
443
|
|
|
280
444
|
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
445
|
+
ctx.output.emitProgress({
|
|
446
|
+
phase: 'execute',
|
|
447
|
+
message: `Calling ${requestedToolName}`,
|
|
448
|
+
});
|
|
281
449
|
|
|
282
450
|
const result = await runToolCommand({
|
|
283
451
|
runtime: ctx.runtime,
|
|
@@ -285,16 +453,26 @@ async function toolsExecHandler(ctx) {
|
|
|
285
453
|
arguments_: {
|
|
286
454
|
tools: [{ tool_slug: requestedToolName, arguments: args }],
|
|
287
455
|
},
|
|
288
|
-
mutating:
|
|
289
|
-
idempotencyKey,
|
|
456
|
+
mutating: targetMutating !== false,
|
|
457
|
+
idempotencyKey: targetMutating === false ? null : idempotencyKey,
|
|
290
458
|
fileBindings,
|
|
291
459
|
});
|
|
460
|
+
ctx.output.emitProgress({
|
|
461
|
+
phase: 'complete',
|
|
462
|
+
message: `Finished ${requestedToolName}`,
|
|
463
|
+
requestId: result.requestId,
|
|
464
|
+
});
|
|
292
465
|
|
|
293
466
|
return ctx.output.emitSuccess({
|
|
294
467
|
command: ctx.spec.command_path.join(' '),
|
|
295
468
|
data: result.payload,
|
|
296
469
|
humanSummary: `Executed tool ${requestedToolName}`,
|
|
297
|
-
|
|
470
|
+
requestId: result.requestId,
|
|
471
|
+
meta: {
|
|
472
|
+
mutating: targetMutating,
|
|
473
|
+
mutation_classification: targetMutating === null ? 'unknown' : 'inferred',
|
|
474
|
+
idempotency_key: targetMutating === false ? null : idempotencyKey,
|
|
475
|
+
},
|
|
298
476
|
renderHuman: () => JSON.stringify(result.payload, null, 2),
|
|
299
477
|
});
|
|
300
478
|
}
|
|
@@ -342,43 +520,48 @@ async function toolsExecParallelHandler(ctx) {
|
|
|
342
520
|
}
|
|
343
521
|
|
|
344
522
|
async function toolsLinkHandler(ctx) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
523
|
+
const credentials = ctx.options.credentials
|
|
524
|
+
? await resolveJsonInput(ctx.options.credentials, 'credentials')
|
|
525
|
+
: undefined;
|
|
526
|
+
if (credentials !== undefined && (typeof credentials !== 'object' || credentials === null || Array.isArray(credentials))) {
|
|
527
|
+
throw usageError('credentials must be a JSON object');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const arguments_ = {
|
|
531
|
+
toolkit: ctx.args.toolkit,
|
|
532
|
+
...(ctx.options.reconnect ? { reconnect: true } : {}),
|
|
533
|
+
...(ctx.options.connectionId ? { reconnect_connection_id: ctx.options.connectionId } : {}),
|
|
534
|
+
...(ctx.options.label ? { label: ctx.options.label } : {}),
|
|
535
|
+
...(credentials ? { credentials } : {}),
|
|
536
|
+
};
|
|
537
|
+
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
538
|
+
const result = await runToolCommand({
|
|
539
|
+
runtime: ctx.runtime,
|
|
540
|
+
toolName: 'LOCAL_NOTIS_AUTHENTIFY',
|
|
541
|
+
arguments_,
|
|
542
|
+
mutating: true,
|
|
543
|
+
idempotencyKey,
|
|
544
|
+
});
|
|
545
|
+
const payload = result.payload || {};
|
|
546
|
+
if (payload.status === 'error') {
|
|
547
|
+
throw usageError(payload.message || `Failed to connect ${ctx.args.toolkit}`, payload);
|
|
361
548
|
}
|
|
362
|
-
const
|
|
549
|
+
const authUrl = payload.redirect_url || payload.integrations_url || payload.url || null;
|
|
363
550
|
|
|
364
551
|
return ctx.output.emitSuccess({
|
|
365
552
|
command: ctx.spec.command_path.join(' '),
|
|
366
553
|
data: {
|
|
367
|
-
|
|
368
|
-
auth_url:
|
|
554
|
+
...payload,
|
|
555
|
+
auth_url: authUrl,
|
|
369
556
|
},
|
|
370
|
-
humanSummary:
|
|
557
|
+
humanSummary: ctx.options.reconnect
|
|
558
|
+
? `Reconnected ${ctx.args.toolkit}`
|
|
559
|
+
: `Started connection for ${ctx.args.toolkit}`,
|
|
371
560
|
hints: [
|
|
372
561
|
{ command: 'notis tools toolkits', reason: 'Verify the toolkit is connected after setup' },
|
|
373
562
|
],
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
`Connect ${ctx.args.toolkit}:`,
|
|
377
|
-
` ${portalUrl}`,
|
|
378
|
-
'',
|
|
379
|
-
'After connecting, verify with:',
|
|
380
|
-
' notis tools toolkits',
|
|
381
|
-
].join('\n'),
|
|
563
|
+
meta: { mutating: true, idempotency_key: idempotencyKey },
|
|
564
|
+
renderHuman: () => JSON.stringify({ ...payload, auth_url: authUrl }, null, 2),
|
|
382
565
|
});
|
|
383
566
|
}
|
|
384
567
|
|
|
@@ -436,6 +619,7 @@ export const toolsCommandSpecs = [
|
|
|
436
619
|
arguments: [{ token: '<tool-name>', description: 'Tool name returned by `notis tools search`.' }],
|
|
437
620
|
options: [
|
|
438
621
|
{ flags: '--arguments <json>', description: 'JSON object, @file path, or - for stdin.' },
|
|
622
|
+
{ flags: '--arguments-file <path>', description: 'Read the JSON arguments object from a file.' },
|
|
439
623
|
{ flags: '--file <argument-path=local-path>', description: 'Upload a local file into a file-uploadable tool argument. Repeatable.', collect: true },
|
|
440
624
|
{ flags: '--get-schema', description: 'Display the tool parameter schema without executing.' },
|
|
441
625
|
{ flags: '--dry-run', description: 'Validate arguments against the tool schema without executing.' },
|
|
@@ -447,6 +631,7 @@ export const toolsCommandSpecs = [
|
|
|
447
631
|
'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --get-schema',
|
|
448
632
|
'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
|
|
449
633
|
'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments @query.json',
|
|
634
|
+
'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments-file query.json',
|
|
450
635
|
'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments - < query.json',
|
|
451
636
|
'notis tools exec composio-dropbox-upload_file --arguments \'{"path":"/target/in/dropbox.pdf"}\' --file content=./Invoice.pdf',
|
|
452
637
|
],
|
|
@@ -481,19 +666,27 @@ export const toolsCommandSpecs = [
|
|
|
481
666
|
},
|
|
482
667
|
{
|
|
483
668
|
command_path: ['tools', 'link'],
|
|
484
|
-
summary: '
|
|
485
|
-
when_to_use: 'Use this when a tool requires authentication
|
|
669
|
+
summary: 'Connect or reconnect an integration toolkit.',
|
|
670
|
+
when_to_use: 'Use this when a tool requires authentication or an active connection must be replaced.',
|
|
486
671
|
args_schema: {
|
|
487
672
|
arguments: [{ token: '<toolkit>', description: 'Toolkit name to connect (e.g. github, gmail, slack).' }],
|
|
488
|
-
options: [
|
|
673
|
+
options: [
|
|
674
|
+
{ flags: '--reconnect', description: 'Replace the existing account instead of adding another connection.' },
|
|
675
|
+
{ flags: '--connection-id <id>', key: 'connectionId', description: 'Exact connection id to replace when multiple accounts exist.' },
|
|
676
|
+
{ flags: '--label <label>', description: 'Account label for a new or replacement connection.' },
|
|
677
|
+
{ flags: '--credentials <json>', description: 'Credential JSON object, @file path, or - for stdin. Prefer stdin so secrets do not enter shell history.' },
|
|
678
|
+
],
|
|
489
679
|
},
|
|
490
|
-
examples: [
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
680
|
+
examples: [
|
|
681
|
+
'notis tools link github',
|
|
682
|
+
'notis tools link dataforseo --reconnect --credentials - < credentials.json',
|
|
683
|
+
],
|
|
684
|
+
output_schema: 'Returns the connection result and an authentication URL when provider authorization is still required.',
|
|
685
|
+
mutates: true,
|
|
686
|
+
idempotent: false,
|
|
687
|
+
require_auth: true,
|
|
495
688
|
related_commands: ['notis tools toolkits', 'notis tools search <query>'],
|
|
496
|
-
backend_call: { type: '
|
|
689
|
+
backend_call: { type: 'tool', name: 'LOCAL_NOTIS_AUTHENTIFY' },
|
|
497
690
|
handler: toolsLinkHandler,
|
|
498
691
|
},
|
|
499
692
|
];
|
|
@@ -1,7 +1,118 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
|
|
5
|
+
export const HIDE_HARNESS_STATUS_SCRIPT =
|
|
6
|
+
"(() => { const el = document.getElementById('harness-status'); if (el) el.style.opacity = '0'; return true; })()";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prepare the mounted harness page for a listing capture: remove the debug
|
|
10
|
+
* banner and the layout the harness adds around the app (top padding for the
|
|
11
|
+
* banner, 100vh min-height) so content height can be measured and framed
|
|
12
|
+
* without artificial whitespace.
|
|
13
|
+
*/
|
|
14
|
+
const PREPARE_CAPTURE_SCRIPT =
|
|
15
|
+
"(() => { const s = document.getElementById('harness-status'); if (s) s.style.display = 'none';" +
|
|
16
|
+
" const r = document.getElementById('root'); if (r) { r.style.paddingTop = '0'; r.style.minHeight = '0'; }" +
|
|
17
|
+
" document.body.style.minHeight = '0'; document.documentElement.style.minHeight = '0'; return true; })()";
|
|
18
|
+
|
|
19
|
+
const FONTS_STATUS_SCRIPT =
|
|
20
|
+
"(() => (document.fonts ? document.fonts.status : 'loaded'))()";
|
|
21
|
+
|
|
22
|
+
const CONTENT_HEIGHT_SCRIPT =
|
|
23
|
+
'(() => { const r = document.getElementById(\'root\');' +
|
|
24
|
+
' if (!r) return Math.ceil(document.documentElement.scrollHeight);' +
|
|
25
|
+
' return Math.ceil(Math.max(r.scrollHeight, r.getBoundingClientRect().height)); })()';
|
|
26
|
+
|
|
27
|
+
// CSS viewport bounds for content-aware framing. Content shorter than the
|
|
28
|
+
// minimum gets a little breathing room; content taller than the maximum is
|
|
29
|
+
// cropped at the frame rather than shrunk into unreadably small text.
|
|
30
|
+
const MIN_CSS_VIEWPORT_HEIGHT = 720;
|
|
31
|
+
const MAX_CSS_VIEWPORT_HEIGHT = 1500;
|
|
32
|
+
|
|
33
|
+
function pngDimensions(path) {
|
|
34
|
+
try {
|
|
35
|
+
const buffer = readFileSync(path);
|
|
36
|
+
if (buffer.length < 24 || buffer.readUInt32BE(12) !== 0x49484452) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pick a CSS viewport that frames `contentHeight` at the output aspect ratio,
|
|
47
|
+
* with a deviceScaleFactor that maps it back to exactly outputWidth x
|
|
48
|
+
* outputHeight physical pixels. Width snaps to a multiple of 8 so the height
|
|
49
|
+
* stays integral for the common 16:10 target.
|
|
50
|
+
*/
|
|
51
|
+
function fitCssViewport(contentHeight, outputWidth, outputHeight) {
|
|
52
|
+
const aspect = outputWidth / outputHeight;
|
|
53
|
+
const cssH = Math.min(
|
|
54
|
+
MAX_CSS_VIEWPORT_HEIGHT,
|
|
55
|
+
Math.max(MIN_CSS_VIEWPORT_HEIGHT, Math.round(contentHeight)),
|
|
56
|
+
);
|
|
57
|
+
const cssW = Math.round((cssH * aspect) / 8) * 8;
|
|
58
|
+
return {
|
|
59
|
+
width: cssW,
|
|
60
|
+
height: Math.round(cssW / aspect),
|
|
61
|
+
scale: outputWidth / cssW,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function evalNumber(sessionName, script, fallback = null) {
|
|
66
|
+
const result = await runAgentBrowser(
|
|
67
|
+
['--session', sessionName, '--json', 'eval', script],
|
|
68
|
+
{ timeoutMs: 5000 },
|
|
69
|
+
);
|
|
70
|
+
if (result.exitCode !== 0) {
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const payload = parseAgentBrowserJson(result.stdout);
|
|
75
|
+
const raw = payload?.data?.result ?? payload?.result ?? payload?.data ?? null;
|
|
76
|
+
const value = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
77
|
+
return Number.isFinite(value) ? value : fallback;
|
|
78
|
+
} catch {
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function waitForFonts(sessionName, timeoutMs = 3000) {
|
|
84
|
+
const deadline = Date.now() + timeoutMs;
|
|
85
|
+
while (Date.now() < deadline) {
|
|
86
|
+
const result = await runAgentBrowser(
|
|
87
|
+
['--session', sessionName, '--json', 'eval', FONTS_STATUS_SCRIPT],
|
|
88
|
+
{ timeoutMs: 5000 },
|
|
89
|
+
);
|
|
90
|
+
if (result.exitCode !== 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const payload = parseAgentBrowserJson(result.stdout);
|
|
95
|
+
const raw = payload?.data?.result ?? payload?.result ?? payload?.data ?? null;
|
|
96
|
+
const status = typeof raw === 'string' && raw.startsWith('"') ? JSON.parse(raw) : raw;
|
|
97
|
+
if (status !== 'loading') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await delay(150);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function setViewport(sessionName, width, height, scale = null) {
|
|
108
|
+
const args = ['--session', sessionName, 'set', 'viewport', String(width), String(height)];
|
|
109
|
+
if (scale != null) {
|
|
110
|
+
args.push(String(scale));
|
|
111
|
+
}
|
|
112
|
+
const result = await runAgentBrowser(args, { timeoutMs: 5000 });
|
|
113
|
+
return result.exitCode === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
5
116
|
function delay(ms) {
|
|
6
117
|
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
7
118
|
}
|
|
@@ -186,13 +297,18 @@ export async function runHarnessRoute({
|
|
|
186
297
|
|
|
187
298
|
/**
|
|
188
299
|
* Open a harness route, wait for it to mount, then capture a PNG screenshot of
|
|
189
|
-
* the rendered app
|
|
190
|
-
* listing validator expects)
|
|
300
|
+
* the rendered app. Output is always exactly `width` x `height` physical
|
|
301
|
+
* pixels (default 2000x1250, the 16:10 the listing validator expects), but the
|
|
302
|
+
* CSS viewport is fitted to the rendered content height: short pages render
|
|
303
|
+
* larger (no dead whitespace below the app), tall pages get more room before
|
|
304
|
+
* cropping. The deviceScaleFactor maps the fitted CSS viewport back onto the
|
|
305
|
+
* fixed output frame. Used by `notis apps screenshot`.
|
|
191
306
|
*/
|
|
192
307
|
export async function captureHarnessScreenshot({
|
|
193
308
|
url,
|
|
194
309
|
sessionName,
|
|
195
310
|
screenshotPath,
|
|
311
|
+
focusSelector = null,
|
|
196
312
|
width = 2000,
|
|
197
313
|
height = 1250,
|
|
198
314
|
timeoutMs = 15_000,
|
|
@@ -204,12 +320,20 @@ export async function captureHarnessScreenshot({
|
|
|
204
320
|
return { ok: false, mounted: false, screenshotPath: null, tool_error: commandError('open', opened) };
|
|
205
321
|
}
|
|
206
322
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
323
|
+
// Initial fixed frame so mount/layout happen at a deterministic size; the
|
|
324
|
+
// scale keeps physical output at width x height from the very first paint.
|
|
325
|
+
const initial = fitCssViewport(900, width, height);
|
|
326
|
+
let scaledViewport = await setViewport(
|
|
327
|
+
sessionName,
|
|
328
|
+
initial.width,
|
|
329
|
+
initial.height,
|
|
330
|
+
initial.scale,
|
|
212
331
|
);
|
|
332
|
+
if (!scaledViewport) {
|
|
333
|
+
// Older agent-browser without deviceScaleFactor support: fall back to a
|
|
334
|
+
// plain fixed viewport at output size.
|
|
335
|
+
await setViewport(sessionName, width, height);
|
|
336
|
+
}
|
|
213
337
|
|
|
214
338
|
const deadline = Date.now() + timeoutMs;
|
|
215
339
|
let lastErrors = [];
|
|
@@ -247,30 +371,89 @@ export async function captureHarnessScreenshot({
|
|
|
247
371
|
};
|
|
248
372
|
}
|
|
249
373
|
|
|
250
|
-
//
|
|
374
|
+
// Strip harness chrome (debug banner, banner padding, 100vh min-height) so
|
|
375
|
+
// the frame contains only the app, then wait for webfonts before measuring.
|
|
251
376
|
await runAgentBrowser(
|
|
252
|
-
[
|
|
253
|
-
'--session',
|
|
254
|
-
sessionName,
|
|
255
|
-
'eval',
|
|
256
|
-
"(() => { const el = document.getElementById('harness-status'); if (el) el.style.display = 'none'; return true; })()",
|
|
257
|
-
],
|
|
377
|
+
['--session', sessionName, 'eval', PREPARE_CAPTURE_SCRIPT],
|
|
258
378
|
{ timeoutMs: 5000 },
|
|
259
379
|
);
|
|
380
|
+
await waitForFonts(sessionName);
|
|
381
|
+
|
|
382
|
+
// Fit the CSS viewport to the rendered content. Content height depends on
|
|
383
|
+
// viewport width, so refine once after the first resize reflows the page.
|
|
384
|
+
let framing = null;
|
|
385
|
+
if (scaledViewport) {
|
|
386
|
+
let contentHeight = await evalNumber(sessionName, CONTENT_HEIGHT_SCRIPT);
|
|
387
|
+
for (let pass = 0; contentHeight != null && pass < 2; pass += 1) {
|
|
388
|
+
const fitted = fitCssViewport(contentHeight, width, height);
|
|
389
|
+
if (framing && fitted.width === framing.width) {
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
scaledViewport = await setViewport(sessionName, fitted.width, fitted.height, fitted.scale);
|
|
393
|
+
if (!scaledViewport) {
|
|
394
|
+
framing = null;
|
|
395
|
+
await setViewport(sessionName, width, height);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
framing = { ...fitted, content_height: contentHeight };
|
|
399
|
+
await delay(150);
|
|
400
|
+
const remeasured = await evalNumber(sessionName, CONTENT_HEIGHT_SCRIPT);
|
|
401
|
+
if (remeasured == null || Math.abs(remeasured - contentHeight) <= 32) {
|
|
402
|
+
framing.content_height = remeasured ?? contentHeight;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
contentHeight = remeasured;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
260
408
|
|
|
261
|
-
// Let the route settle (
|
|
409
|
+
// Let the route settle (async data, resize transitions) before the capture.
|
|
262
410
|
await delay(500);
|
|
263
411
|
|
|
264
412
|
mkdirSync(dirname(screenshotPath), { recursive: true });
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
413
|
+
const screenshotArgs = ['--session', sessionName, 'screenshot'];
|
|
414
|
+
if (focusSelector) {
|
|
415
|
+
screenshotArgs.push(focusSelector);
|
|
416
|
+
}
|
|
417
|
+
screenshotArgs.push(screenshotPath, '--screenshot-format', 'png');
|
|
418
|
+
const shot = await runAgentBrowser(screenshotArgs, { timeoutMs: 15_000 });
|
|
269
419
|
if (shot.exitCode !== 0) {
|
|
270
420
|
return { ok: false, mounted: true, screenshotPath: null, errors: lastErrors, timed_out: false, tool_error: commandError('screenshot', shot) };
|
|
271
421
|
}
|
|
272
422
|
|
|
273
|
-
|
|
423
|
+
// The listing validator requires exact output dimensions. If the fitted
|
|
424
|
+
// capture came out wrong (e.g. the browser ignored the scale factor),
|
|
425
|
+
// recapture once at a plain fixed viewport.
|
|
426
|
+
const dimensions = pngDimensions(screenshotPath);
|
|
427
|
+
if (!focusSelector && framing && (!dimensions || dimensions.width !== width || dimensions.height !== height)) {
|
|
428
|
+
await setViewport(sessionName, width, height);
|
|
429
|
+
await delay(300);
|
|
430
|
+
const retry = await runAgentBrowser(
|
|
431
|
+
['--session', sessionName, 'screenshot', screenshotPath, '--screenshot-format', 'png'],
|
|
432
|
+
{ timeoutMs: 15_000 },
|
|
433
|
+
);
|
|
434
|
+
if (retry.exitCode !== 0) {
|
|
435
|
+
return { ok: false, mounted: true, screenshotPath: null, errors: lastErrors, timed_out: false, tool_error: commandError('screenshot', retry) };
|
|
436
|
+
}
|
|
437
|
+
framing = null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (focusSelector) {
|
|
441
|
+
framing = {
|
|
442
|
+
...(framing || {}),
|
|
443
|
+
focus_selector: focusSelector,
|
|
444
|
+
source_dimensions: dimensions,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
mounted: true,
|
|
451
|
+
screenshotPath,
|
|
452
|
+
errors: lastErrors,
|
|
453
|
+
timed_out: false,
|
|
454
|
+
tool_error: null,
|
|
455
|
+
framing,
|
|
456
|
+
};
|
|
274
457
|
}
|
|
275
458
|
|
|
276
459
|
export async function closeAgentBrowserSession(sessionName) {
|