@notis_ai/cli 0.2.0-beta.191.1 → 0.2.0-beta.197.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.
Files changed (107) hide show
  1. package/README.md +485 -86
  2. package/config/portal-route-roots.json +47 -0
  3. package/dist/agent-hooks/notis-agent-hook.mjs +32663 -6357
  4. package/dist/base-skills/notis-apps/SKILL.md +88 -67
  5. package/dist/base-skills/notis-apps/references/architecture.md +114 -167
  6. package/dist/base-skills/notis-apps/references/design.md +26 -107
  7. package/dist/base-skills/notis-apps/references/memory.md +58 -0
  8. package/dist/base-skills/notis-apps/references/reading.md +52 -74
  9. package/dist/base-skills/notis-apps/references/release.md +104 -142
  10. package/dist/base-skills/notis-apps/references/sdk.md +113 -60
  11. package/dist/base-skills/notis-apps/references/troubleshooting.md +22 -21
  12. package/dist/base-skills/notis-apps/references/views.md +142 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +4 -4
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +12 -11
  15. package/dist/base-skills/notis-cli/references/native-databases.md +6 -6
  16. package/dist/base-skills/notis-cli/references/tool-examples.md +4 -4
  17. package/dist/base-skills/notis-query/SKILL.md +34 -60
  18. package/dist/base-skills/notis-query/references/database-discovery.md +59 -69
  19. package/dist/base-skills/notis-query/references/documents.md +63 -50
  20. package/dist/base-skills/notis-query/references/query.md +57 -524
  21. package/dist/sdk/src/bundleModule.ts +79 -0
  22. package/dist/sdk/src/components/DocumentEditor.tsx +6 -3
  23. package/dist/sdk/src/components/DocumentPage.tsx +141 -0
  24. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  25. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  26. package/dist/sdk/src/config.ts +4 -26
  27. package/dist/sdk/src/documents.ts +2 -0
  28. package/dist/sdk/src/hooks/useAction.ts +28 -0
  29. package/dist/sdk/src/hooks/useCloudComputer.ts +35 -4
  30. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  31. package/dist/sdk/src/hooks/useHandover.ts +5 -4
  32. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  33. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  34. package/dist/sdk/src/hooks/useNotis.ts +3 -0
  35. package/dist/sdk/src/hooks/useNotisNavigation.ts +26 -11
  36. package/dist/sdk/src/hooks/useQuery.ts +7 -4
  37. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  38. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  39. package/dist/sdk/src/index.ts +36 -0
  40. package/dist/sdk/src/interactions/shortcuts.tsx +17 -3
  41. package/dist/sdk/src/nativeData.ts +39 -0
  42. package/dist/sdk/src/presentation.tsx +82 -0
  43. package/dist/sdk/src/provider.tsx +9 -4
  44. package/dist/sdk/src/queryCache.ts +5 -3
  45. package/dist/sdk/src/queryErrors.ts +51 -0
  46. package/dist/sdk/src/runtime.ts +146 -8
  47. package/dist/sdk/src/space.ts +425 -0
  48. package/dist/sdk/src/styles.css +96 -72
  49. package/dist/sdk/src/vite.ts +11 -1
  50. package/dist/skill-sync/index.js +458 -68
  51. package/dist/skill-sync/index.js.map +4 -4
  52. package/dist/skill-sync-worker.mjs +451 -77
  53. package/dist/space-harness/host.js +66 -0
  54. package/dist/view-renderer/dist/host.css +1 -0
  55. package/dist/view-renderer/dist/host.js +1042 -0
  56. package/dist/view-renderer/dist/theme.css +73 -0
  57. package/dist/view-renderer/src/dom.js +180 -0
  58. package/dist/view-renderer/src/errors.js +8 -0
  59. package/dist/view-renderer/src/host.tsx +173 -0
  60. package/dist/view-renderer/src/index.js +4 -0
  61. package/dist/view-renderer/src/pool.js +76 -0
  62. package/dist/view-renderer/src/remote-broker.js +59 -0
  63. package/dist/view-renderer/src/renderer.js +201 -0
  64. package/package.json +11 -5
  65. package/skills/notis-apps/cli.md +270 -163
  66. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +3 -3
  67. package/skills/notis-onboarding/BRIEF.md +7 -3
  68. package/src/command-specs/apps.js +1 -8
  69. package/src/command-specs/files.js +29 -0
  70. package/src/command-specs/handover.js +45 -16
  71. package/src/command-specs/helpers.js +1 -0
  72. package/src/command-specs/index.js +10 -4
  73. package/src/command-specs/skills.js +229 -0
  74. package/src/command-specs/spaces.js +572 -0
  75. package/src/command-specs/views.js +69 -0
  76. package/src/runtime/agent-browser.js +30 -7
  77. package/src/runtime/app-boundary-validator.js +7 -4
  78. package/src/runtime/app-data-literals.js +82 -0
  79. package/src/runtime/app-platform.js +321 -253
  80. package/src/runtime/confined-space-build.js +74 -0
  81. package/src/runtime/confined-space-verify.js +49 -0
  82. package/src/runtime/native-record-files.js +47 -0
  83. package/src/runtime/retired-sdk-files.js +20 -0
  84. package/src/runtime/skill-sync/cloud-client.ts +26 -4
  85. package/src/runtime/skill-sync/index.ts +154 -35
  86. package/src/runtime/skill-sync/local-scanner.ts +125 -17
  87. package/src/runtime/skill-sync/native-identity.ts +102 -0
  88. package/src/runtime/skill-sync/native-plan.ts +59 -0
  89. package/src/runtime/skill-sync/sync-plan.ts +16 -9
  90. package/src/runtime/skill-sync/types.ts +32 -6
  91. package/src/runtime/source-root.js +32 -0
  92. package/src/runtime/space-collection-scaffold.js +66 -0
  93. package/src/runtime/space-database-structures.js +229 -0
  94. package/src/runtime/space-fixture-broker.js +50 -0
  95. package/src/runtime/space-lock.js +337 -0
  96. package/src/runtime/space-platform.js +105 -0
  97. package/src/runtime/space-source.js +281 -0
  98. package/src/runtime/space-test-server.js +327 -0
  99. package/src/runtime/space-verification.js +89 -0
  100. package/src/runtime/space-verify-browser-worker.js +51 -0
  101. package/src/runtime/space-view-manifest.js +254 -0
  102. package/src/runtime/sync-skills.js +1 -1
  103. package/src/runtime/transport.js +8 -1
  104. package/src/runtime/view-render.js +49 -0
  105. package/src/space-harness-host.ts +103 -0
  106. package/template/app/globals.css +1 -0
  107. package/src/command-specs/reports.js +0 -97
@@ -0,0 +1,69 @@
1
+ import { httpRequest } from '../runtime/transport.js';
2
+ import { usageError } from '../runtime/errors.js';
3
+ import { renderViewWithCliRuntime } from '../runtime/view-render.js';
4
+ import { resolve } from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+
7
+ export function viewRenderCommandSpec({ screenshotOnly = false } = {}) {
8
+ const command_path = screenshotOnly ? ['spaces', 'screenshot'] : ['views', 'render'];
9
+ return {
10
+ command_path, summary: screenshotOnly ? 'Capture a full-height PNG of a live view with your current access.' : 'Render a live view as Markdown and a full-height PNG with your current access.',
11
+ when_to_use: 'Check the actual deployed view; writes are blocked and current access is checked again before any output is saved.',
12
+ args_schema: { arguments: [{ token: '<url>', key: 'url', description: 'Current view-qualified or record link.' }], options: [
13
+ ...(!screenshotOnly ? [{ flags: '--outputs <kinds>', description: 'Comma-separated markdown,screenshot (default both).' }] : []),
14
+ { flags: '--width <pixels>', description: '1440 (default) or 390.' },
15
+ { flags: '--output-dir <path>', description: 'New output directory; defaults to a unique .notis/renders directory. Existing files are never overwritten.' },
16
+ ] }, examples: [screenshotOnly ? 'notis spaces screenshot <view-link> --width 390' : 'notis views render <view-link> --outputs markdown,screenshot'],
17
+ mutates: false, idempotent: true, require_auth: true,
18
+ backend_call: { type: 'http', path: '/portal_view_render/prepare' },
19
+ async handler(ctx) {
20
+ const outputs = screenshotOnly ? ['screenshot'] : (ctx.options.outputs || 'markdown,screenshot').split(',');
21
+ if (!outputs.length || new Set(outputs).size !== outputs.length || outputs.some(value => !['markdown', 'screenshot'].includes(value))) {
22
+ throw usageError('Choose markdown, screenshot, or markdown,screenshot.');
23
+ }
24
+ const width = ctx.options.width === undefined ? 1440 : Number(ctx.options.width);
25
+ const directory = resolve(ctx.options.outputDir || `.notis/renders/${randomUUID()}`);
26
+ const controller = new AbortController(), cancel = () => controller.abort();
27
+ process.once('SIGINT', cancel); process.once('SIGTERM', cancel);
28
+ try {
29
+ const { result, artifacts } = await renderViewWithCliRuntime({ runtime: ctx.runtime,
30
+ url: ctx.args.url, outputs, width, directory, signal: controller.signal });
31
+ const data = { ...result, artifacts };
32
+ if (data.screenshot) {
33
+ const { data_base64: _bytes, ...metadata } = data.screenshot;
34
+ data.screenshot = { ...metadata, path: artifacts.screenshot };
35
+ }
36
+ return ctx.output.emitSuccess({ command: command_path.join(' '), data,
37
+ humanSummary: `Rendered ${result.url}. Saved ${Object.values(artifacts).join(' and ')}.`, meta: { mutating: false } });
38
+ } finally { process.removeListener('SIGINT', cancel); process.removeListener('SIGTERM', cancel); }
39
+ },
40
+ };
41
+ }
42
+
43
+ export const viewsCommandSpecs = [{
44
+ command_path: ['views', 'find'],
45
+ summary: 'Find the views you can open for a record, database, view link, Space or search.',
46
+ when_to_use: 'Get fresh view-qualified links and declared URL parameters before citing or opening a view.',
47
+ args_schema: { options: [
48
+ { flags: '--record-key <id>', description: 'Find views that show this exact native record.' },
49
+ { flags: '--database-id <id>', description: 'Find views over this database.' },
50
+ { flags: '--url <url>', description: 'Resolve this view link and its current parameters.' },
51
+ { flags: '--space-id <id>', description: 'Describe this Space view.' },
52
+ { flags: '--query <text>', description: 'Search accessible view names, descriptions and readable context.' },
53
+ ] },
54
+ examples: ['notis views find --record-key <record-key>', 'notis views find --url <view-link>', 'notis views find --query inbox'],
55
+ mutates: false, idempotent: true, require_auth: true,
56
+ backend_call: { type: 'http', path: '/portal_spaces/find-views' },
57
+ async handler(ctx) {
58
+ const options = { record_key: ctx.options.recordKey, database_id: ctx.options.databaseId,
59
+ url: ctx.options.url, space_id: ctx.options.spaceId, query: ctx.options.query };
60
+ const selected = Object.entries(options).filter(([, value]) => value !== undefined);
61
+ if (selected.length !== 1 || typeof selected[0][1] !== 'string' || !selected[0][1].trim()) {
62
+ throw usageError('Choose exactly one of --record-key, --database-id, --url, --space-id or --query.');
63
+ }
64
+ const result = await httpRequest({ runtime: ctx.runtime, method: 'POST', path: '/portal_spaces/find-views',
65
+ body: Object.fromEntries(selected), spacesProtocol: 1 });
66
+ return ctx.output.emitSuccess({ command: 'views find', data: result.payload,
67
+ humanSummary: JSON.stringify(result.payload, null, 2), meta: { mutating: false, request_id: result.requestId } });
68
+ },
69
+ }, viewRenderCommandSpec()];
@@ -131,9 +131,11 @@ function runAgentBrowser(args, { timeoutMs = 30_000 } = {}) {
131
131
  let stdout = '';
132
132
  let stderr = '';
133
133
  let timedOut = false;
134
+ let hardStop;
134
135
  const timer = setTimeout(() => {
135
136
  timedOut = true;
136
137
  child.kill('SIGTERM');
138
+ hardStop = setTimeout(() => child.kill('SIGKILL'), 2000);
137
139
  }, timeoutMs);
138
140
 
139
141
  child.stdout.on('data', (chunk) => {
@@ -143,7 +145,7 @@ function runAgentBrowser(args, { timeoutMs = 30_000 } = {}) {
143
145
  stderr += chunk.toString();
144
146
  });
145
147
  child.on('error', (error) => {
146
- clearTimeout(timer);
148
+ clearTimeout(timer); clearTimeout(hardStop);
147
149
  resolvePromise({
148
150
  exitCode: 1,
149
151
  stdout,
@@ -152,7 +154,7 @@ function runAgentBrowser(args, { timeoutMs = 30_000 } = {}) {
152
154
  });
153
155
  });
154
156
  child.on('close', (code) => {
155
- clearTimeout(timer);
157
+ clearTimeout(timer); clearTimeout(hardStop);
156
158
  resolvePromise({
157
159
  exitCode: timedOut ? 124 : (code ?? 1),
158
160
  stdout,
@@ -225,10 +227,17 @@ export async function runHarnessRoute({
225
227
  snapshotPath = null,
226
228
  designViewports = DEFAULT_DESIGN_VIEWPORTS,
227
229
  waitForRuntime = false,
230
+ signal,
228
231
  }) {
232
+ const cancelled = () => ({ mounted: false, renderStarted: false, runtimeCalls: [],
233
+ errors: [{ phase: 'browser', message: 'Verification cancelled.' }], snapshotPath: null, tool_error: null });
234
+ if (signal?.aborted) return cancelled();
229
235
  const opened = await runAgentBrowser(['--session', sessionName, 'open', url], {
230
236
  timeoutMs: Math.min(Math.max(timeoutMs, 5000), 30_000),
231
237
  });
238
+ // Let an in-flight open settle before the owner closes its browser. Closing
239
+ // concurrently with open can leave a newly launched browser behind.
240
+ if (signal?.aborted) return cancelled();
232
241
  if (opened.exitCode !== 0) {
233
242
  return {
234
243
  mounted: false,
@@ -242,7 +251,7 @@ export async function runHarnessRoute({
242
251
 
243
252
  const deadline = Date.now() + timeoutMs;
244
253
  let readySince = null;
245
- while (Date.now() < deadline) {
254
+ while (Date.now() < deadline && !signal?.aborted) {
246
255
  const lastRead = await readHarness(sessionName, 5000);
247
256
  if (!lastRead.ok) {
248
257
  readySince = null;
@@ -259,6 +268,8 @@ export async function runHarnessRoute({
259
268
  await delay(250);
260
269
  }
261
270
 
271
+ if (signal?.aborted) return cancelled();
272
+
262
273
  const finalRead = await readHarness(sessionName, 5000);
263
274
  if (!finalRead.ok) {
264
275
  return {
@@ -525,10 +536,22 @@ export async function captureHarnessScreenshot({
525
536
  }
526
537
 
527
538
  export async function closeAgentBrowserSession(sessionName) {
528
- const result = await runAgentBrowser(['--session', sessionName, 'close'], {
529
- timeoutMs: 5000,
530
- });
531
- return result.exitCode === 0;
539
+ return (await closeAgentBrowserSessionResult(sessionName)).ok;
540
+ }
541
+
542
+ export async function closeAgentBrowserSessionResult(sessionName) {
543
+ const started = Date.now();
544
+ // agent-browser waits up to 30s for CDP Browser.close and then up to 5s
545
+ // for its owned Chrome process. Do not kill the client at the start of the
546
+ // provider's shutdown grace period and lose its actual close acknowledgement.
547
+ const result = await runAgentBrowser(['--session', sessionName, '--json', 'close'], { timeoutMs: 40_000 });
548
+ let acknowledged = false;
549
+ try {
550
+ const reply = parseAgentBrowserJson(result.stdout);
551
+ acknowledged = reply?.success === true && reply?.data?.closed === true;
552
+ } catch { /* Missing acknowledgement is a failed cleanup, never a pass. */ }
553
+ return { ok: result.exitCode === 0 && acknowledged, exit_code: result.exitCode,
554
+ timed_out: Boolean(result.timedOut), duration_ms: Date.now() - started };
532
555
  }
533
556
 
534
557
  // ---------------------------------------------------------------------------
@@ -3,6 +3,7 @@ import { dirname, extname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import { usageError } from './errors.js';
6
+ import { maskSerializedJsonData } from './app-data-literals.js';
6
7
 
7
8
  const moduleDir = dirname(fileURLToPath(import.meta.url));
8
9
 
@@ -136,7 +137,7 @@ function validateTextFile(relPath, content) {
136
137
  return applyRules(content, rules.css, relPath);
137
138
  }
138
139
  if (extension === '.js' || extension === '.jsx' || extension === '.ts' || extension === '.tsx') {
139
- return applyRules(content, rules.javascript, relPath);
140
+ return applyRules(maskSerializedJsonData(content, extension), rules.javascript, relPath);
140
141
  }
141
142
  return [];
142
143
  }
@@ -311,7 +312,9 @@ export function collectDesignViolationsForFile(relPath, content, config = getCom
311
312
  if (!matchesAny(normalized, config.include)) return [];
312
313
  if (matchesAny(normalized, config.exclude)) return [];
313
314
 
314
- const lines = content.split('\n');
315
+ const checked = ['.js', '.jsx', '.ts', '.tsx'].includes(extname(normalized))
316
+ ? maskSerializedJsonData(content, extname(normalized)) : content;
317
+ const lines = checked.split('\n');
315
318
  const violations = [];
316
319
  for (const rule of config.rules) {
317
320
  if (rule.include && !matchesAny(normalized, rule.include)) continue;
@@ -319,12 +322,12 @@ export function collectDesignViolationsForFile(relPath, content, config = getCom
319
322
 
320
323
  const matches = [];
321
324
  if (rule.scope === 'file') {
322
- const match = rule.regex.exec(content);
325
+ const match = rule.regex.exec(checked);
323
326
  if (match) matches.push(match.index);
324
327
  } else {
325
328
  rule.regex.lastIndex = 0;
326
329
  let match;
327
- while ((match = rule.regex.exec(content)) !== null) {
330
+ while ((match = rule.regex.exec(checked)) !== null) {
328
331
  matches.push(match.index);
329
332
  if (match[0].length === 0) rule.regex.lastIndex += 1;
330
333
  }
@@ -0,0 +1,82 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ let babelParse;
5
+
6
+ function isJsonParse(node) {
7
+ const callee = node.callee;
8
+ return node.type === 'CallExpression' && callee?.type === 'MemberExpression' && !callee.computed
9
+ && callee.object?.type === 'Identifier' && callee.object.name === 'JSON'
10
+ && callee.property?.type === 'Identifier' && callee.property.name === 'parse';
11
+ }
12
+
13
+ function dataContext(ancestors) {
14
+ if (ancestors.some(node => node.type === 'JSXAttribute')) return false;
15
+ for (let index = ancestors.length - 1; index >= 0; index -= 1) {
16
+ const node = ancestors[index];
17
+ if (node.type === 'JSXAttribute' || node.type === 'ImportDeclaration'
18
+ || node.type === 'ImportExpression' || node.type === 'TSImportType'
19
+ || node.type === 'ExportAllDeclaration' || node.type === 'ExportNamedDeclaration'
20
+ || node.type === 'NewExpression' || node.type === 'TaggedTemplateExpression'
21
+ || node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') return false;
22
+ if (node.type === 'CallExpression' || node.type === 'OptionalCallExpression') {
23
+ if (!isJsonParse(node)) return false;
24
+ }
25
+ if (['VariableDeclarator', 'ExportDefaultDeclaration', 'ReturnStatement'].includes(node.type)) return true;
26
+ if (node.type === 'ExpressionStatement') return false;
27
+ }
28
+ return false;
29
+ }
30
+
31
+ /**
32
+ * Serialized JSON is data, not source: a review may quote forbidden host code
33
+ * or historical styles. Mask only complete object/array JSON literal values in
34
+ * data-expression positions. Keep ordinary strings, import/call/selector and
35
+ * JSX attribute contexts, and every template expression under the existing
36
+ * rules. Parsing errors (or a missing parser) retain the original checks.
37
+ * Source/archive bytes are never rewritten; offsets and line breaks survive.
38
+ */
39
+ export function maskSerializedJsonData(content, extension = '.js') {
40
+ let ast;
41
+ try {
42
+ babelParse ||= require('@babel/parser').parse;
43
+ ast = babelParse(content, {
44
+ sourceType: 'unambiguous',
45
+ allowReturnOutsideFunction: true,
46
+ plugins: [...(['.ts', '.tsx'].includes(extension) ? ['typescript'] : []),
47
+ ...(extension !== '.ts' ? ['jsx'] : [])],
48
+ });
49
+ } catch { return content; }
50
+ const ranges = [];
51
+ const pending = [[ast, []]];
52
+ while (pending.length) {
53
+ const [node, ancestors] = pending.pop();
54
+ if (!node || typeof node !== 'object') continue;
55
+ const literal = node.type === 'StringLiteral' ? node.value
56
+ : node.type === 'TemplateLiteral' && node.expressions.length === 0 ? node.quasis[0]?.value.cooked : null;
57
+ if (typeof literal === 'string' && dataContext(ancestors)) {
58
+ try {
59
+ const value = JSON.parse(literal);
60
+ if (value !== null && typeof value === 'object') {
61
+ ranges.push([node.start, node.end]);
62
+ continue;
63
+ }
64
+ } catch { /* Ordinary/non-JSON strings keep their original checks. */ }
65
+ }
66
+ const parents = [...ancestors, node];
67
+ for (const [key, value] of Object.entries(node)) {
68
+ if (['loc', 'extra', 'tokens', 'comments', 'errors'].includes(key)) continue;
69
+ if (Array.isArray(value)) {
70
+ for (const child of value) if (child?.type) pending.push([child, parents]);
71
+ } else if (value?.type) pending.push([value, parents]);
72
+ }
73
+ }
74
+ let cursor = 0;
75
+ const result = [];
76
+ for (const [start, end] of ranges.sort((a, b) => a[0] - b[0])) {
77
+ result.push(content.slice(cursor, start), content.slice(start, end).replace(/[^\r\n]/g, ' '));
78
+ cursor = end;
79
+ }
80
+ result.push(content.slice(cursor));
81
+ return result.join('');
82
+ }