@notis_ai/cli 0.2.6 → 0.2.8

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 (151) hide show
  1. package/README.md +17 -0
  2. package/dist/scaffolds/notis-database/app/page.tsx +38 -41
  3. package/dist/scaffolds/notis-database/lib/types.ts +5 -7
  4. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  5. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  6. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  7. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  8. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  9. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +1839 -0
  10. package/dist/scaffolds/notis-database/notis.config.ts +48 -1
  11. package/dist/scaffolds/notis-database/package-lock.json +3935 -0
  12. package/dist/scaffolds/notis-database/package.json +2 -1
  13. package/dist/scaffolds/notis-database/packages/sdk/package.json +36 -0
  14. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  15. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +60 -0
  16. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  17. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  18. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  19. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +196 -0
  20. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +229 -0
  21. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +41 -0
  22. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  23. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +78 -0
  24. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +121 -0
  25. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  26. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +34 -0
  27. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  28. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +64 -0
  29. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +56 -0
  30. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  31. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  32. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +83 -0
  33. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +43 -0
  34. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +220 -0
  35. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +186 -0
  36. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +15 -0
  37. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +56 -0
  38. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +15 -0
  39. package/dist/scaffolds/notis-database/src/mock-runtime.ts +4 -1
  40. package/dist/scaffolds/notis-journal/CHANGELOG.md +4 -0
  41. package/dist/scaffolds/notis-journal/app/page.tsx +1 -0
  42. package/dist/scaffolds/notis-journal/package-lock.json +4615 -0
  43. package/dist/scaffolds/notis-journal/package.json +2 -2
  44. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +52 -1
  45. package/dist/scaffolds/notis-notes/app/page.tsx +39 -46
  46. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +596 -0
  47. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +144 -0
  48. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  49. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  50. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  51. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  52. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  53. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +752 -0
  54. package/dist/scaffolds/notis-notes/notis.config.ts +48 -3
  55. package/dist/scaffolds/notis-notes/package-lock.json +4636 -0
  56. package/dist/scaffolds/notis-notes/package.json +7 -3
  57. package/dist/scaffolds/notis-notes/packages/sdk/package.json +36 -0
  58. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  59. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +60 -0
  60. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  61. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  62. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  63. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +196 -0
  64. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +229 -0
  65. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +41 -0
  66. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  67. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +78 -0
  68. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +121 -0
  69. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  70. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +34 -0
  71. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  72. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +64 -0
  73. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +56 -0
  74. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  75. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  76. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +83 -0
  77. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +43 -0
  78. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +220 -0
  79. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +186 -0
  80. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +15 -0
  81. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +56 -0
  82. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +15 -0
  83. package/dist/scaffolds/notis-random/CHANGELOG.md +12 -2
  84. package/dist/scaffolds/notis-random/app/history/page.tsx +5 -4
  85. package/dist/scaffolds/notis-random/app/page.tsx +141 -74
  86. package/dist/scaffolds/notis-random/components/ui/button.tsx +50 -0
  87. package/dist/scaffolds/notis-random/components/ui/card.tsx +16 -0
  88. package/dist/scaffolds/notis-random/components/ui/input.tsx +23 -0
  89. package/dist/scaffolds/notis-random/components.json +20 -0
  90. package/dist/scaffolds/notis-random/lib/notis-tools.ts +26 -7
  91. package/dist/scaffolds/notis-random/lib/rng.ts +179 -19
  92. package/dist/scaffolds/notis-random/lib/roll-record.ts +103 -16
  93. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  94. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  95. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  96. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  97. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  98. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +753 -0
  99. package/dist/scaffolds/notis-random/notis.config.ts +47 -3
  100. package/dist/scaffolds/notis-random/package-lock.json +4513 -0
  101. package/dist/scaffolds/notis-random/package.json +6 -2
  102. package/dist/scaffolds/notis-random/packages/sdk/package.json +36 -0
  103. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  104. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +60 -0
  105. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  106. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  107. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  108. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +196 -0
  109. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +229 -0
  110. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +41 -0
  111. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  112. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +78 -0
  113. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +121 -0
  114. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  115. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +34 -0
  116. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  117. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +64 -0
  118. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +56 -0
  119. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  120. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  121. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +83 -0
  122. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +43 -0
  123. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +220 -0
  124. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +186 -0
  125. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +15 -0
  126. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +56 -0
  127. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +15 -0
  128. package/dist/scaffolds/notis-random/tailwind.config.ts +8 -1
  129. package/dist/scaffolds.json +8 -6
  130. package/package.json +2 -1
  131. package/skills/notis-apps/SKILL.md +449 -144
  132. package/skills/notis-apps/cli.md +15 -0
  133. package/skills/notis-cli/SKILL.md +8 -4
  134. package/skills/notis-onboarding/BRIEF.md +93 -0
  135. package/src/cli.js +52 -7
  136. package/src/command-specs/apps.js +154 -21
  137. package/src/command-specs/diagnostics.js +674 -0
  138. package/src/command-specs/helpers.js +4 -1
  139. package/src/command-specs/index.js +8 -0
  140. package/src/command-specs/onboarding.js +216 -0
  141. package/src/command-specs/smoke.js +386 -0
  142. package/src/command-specs/tools.js +189 -5
  143. package/src/runtime/app-platform.js +65 -4
  144. package/src/runtime/desktop-auth.js +66 -17
  145. package/src/runtime/errors.js +1 -0
  146. package/src/runtime/output.js +21 -3
  147. package/src/runtime/profiles.js +214 -18
  148. package/src/runtime/telemetry.js +92 -0
  149. package/src/runtime/transport.js +29 -1
  150. package/template/.harness/index.html.tmpl +30 -2
  151. package/template/packages/sdk/src/config.ts +38 -1
@@ -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
- const rawArguments = ctx.options.arguments || '{}';
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,30 @@ async function toolsExecHandler(ctx) {
285
453
  arguments_: {
286
454
  tools: [{ tool_slug: requestedToolName, arguments: args }],
287
455
  },
456
+ // The server is authoritative about provider/MCP effects. A tool name that
457
+ // looks like a read is not proof of read-only behavior, so generic
458
+ // executions always carry a key and use mutation-safe transport retries.
459
+ // Proven reads are still executed in the read lane by hosted MCP.
288
460
  mutating: true,
289
461
  idempotencyKey,
290
462
  fileBindings,
291
463
  });
464
+ ctx.output.emitProgress({
465
+ phase: 'complete',
466
+ message: `Finished ${requestedToolName}`,
467
+ requestId: result.requestId,
468
+ });
292
469
 
293
470
  return ctx.output.emitSuccess({
294
471
  command: ctx.spec.command_path.join(' '),
295
472
  data: result.payload,
296
473
  humanSummary: `Executed tool ${requestedToolName}`,
297
- meta: { mutating: true, idempotency_key: idempotencyKey },
474
+ requestId: result.requestId,
475
+ meta: {
476
+ mutating: targetMutating,
477
+ mutation_classification: targetMutating === null ? 'unknown' : 'name_hint_only',
478
+ idempotency_key: idempotencyKey,
479
+ },
298
480
  renderHuman: () => JSON.stringify(result.payload, null, 2),
299
481
  });
300
482
  }
@@ -441,6 +623,7 @@ export const toolsCommandSpecs = [
441
623
  arguments: [{ token: '<tool-name>', description: 'Tool name returned by `notis tools search`.' }],
442
624
  options: [
443
625
  { flags: '--arguments <json>', description: 'JSON object, @file path, or - for stdin.' },
626
+ { flags: '--arguments-file <path>', description: 'Read the JSON arguments object from a file.' },
444
627
  { flags: '--file <argument-path=local-path>', description: 'Upload a local file into a file-uploadable tool argument. Repeatable.', collect: true },
445
628
  { flags: '--get-schema', description: 'Display the tool parameter schema without executing.' },
446
629
  { flags: '--dry-run', description: 'Validate arguments against the tool schema without executing.' },
@@ -452,6 +635,7 @@ export const toolsCommandSpecs = [
452
635
  'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --get-schema',
453
636
  'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --dry-run --arguments \'{"database_slug":"tasks","query":{}}\'',
454
637
  'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments @query.json',
638
+ 'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments-file query.json',
455
639
  'notis tools exec LOCAL_NOTIS_DATABASE_QUERY --arguments - < query.json',
456
640
  'notis tools exec composio-dropbox-upload_file --arguments \'{"path":"/target/in/dropbox.pdf"}\' --file content=./Invoice.pdf',
457
641
  ],
@@ -231,7 +231,13 @@ export function detectProjectWarnings(projectDir, appConfig = null) {
231
231
  warnings.push('Missing Tailwind config.');
232
232
  }
233
233
 
234
- if (appConfig && (!Array.isArray(appConfig.databases) || appConfig.databases.length === 0)) {
234
+ const readsWorkspaceDatabases =
235
+ appConfig?.capabilities?.workspaceDatabases === 'read';
236
+ if (
237
+ appConfig
238
+ && !readsWorkspaceDatabases
239
+ && (!Array.isArray(appConfig.databases) || appConfig.databases.length === 0)
240
+ ) {
235
241
  warnings.push('No database references declared in notis.config.ts.');
236
242
  }
237
243
 
@@ -490,13 +496,24 @@ export async function runProjectScript({ projectDir, scriptName, env = {}, stdio
490
496
  stdio,
491
497
  env: { ...process.env, ...env },
492
498
  });
499
+ let capturedOutput = '';
500
+ for (const stream of [child.stdout, child.stderr]) {
501
+ stream?.on('data', (chunk) => {
502
+ capturedOutput += chunk.toString();
503
+ });
504
+ }
493
505
  child.on('error', rejectPromise);
494
506
  child.on('exit', (code) => {
495
507
  if (code === 0) {
496
508
  resolvePromise();
497
509
  return;
498
510
  }
499
- rejectPromise(new Error(`npm run ${scriptName} failed with exit code ${code}`));
511
+ const detail = capturedOutput.trim();
512
+ rejectPromise(
513
+ new Error(
514
+ `npm run ${scriptName} failed with exit code ${code}${detail ? `:\n${detail}` : ''}`,
515
+ ),
516
+ );
500
517
  });
501
518
  });
502
519
  }
@@ -707,7 +724,14 @@ export function generateManifest(appConfig, projectDir) {
707
724
  return entry;
708
725
  });
709
726
 
710
- const databases = appConfig.databases || [];
727
+ // Database entries may be a bare slug (structure only) or an object opting
728
+ // into shipping rows to installers. Normalize to the manifest's snake_case.
729
+ const databases = (appConfig.databases || []).map((entry) => {
730
+ if (typeof entry === 'string') return entry;
731
+ if (!entry || typeof entry !== 'object' || !entry.slug) return entry;
732
+ const seedDocuments = entry.seedDocuments ?? entry.seed_documents;
733
+ return seedDocuments === true ? { slug: entry.slug, seed_documents: true } : entry.slug;
734
+ });
711
735
  const categories = normalizeCategories(appConfig.categories || []);
712
736
  const metadata = { screenshots: resolveListingScreenshots(projectDir, appConfig) };
713
737
  const appSlug = safeKebab(appConfig.name);
@@ -786,12 +810,29 @@ export function generateManifest(appConfig, projectDir) {
786
810
  css: 'bundle/app.css',
787
811
  },
788
812
  databases,
813
+ capabilities: normalizeAppCapabilities(appConfig.capabilities),
789
814
  tools: appConfig.tools || [],
790
815
  skills,
791
816
  onboarding: appConfig.onboarding || null,
792
817
  };
793
818
  }
794
819
 
820
+ /**
821
+ * Keeps only capabilities the platform actually understands, at the exact
822
+ * values it accepts. An unknown key or value is dropped rather than passed
823
+ * through, so a typo can never reach the server as a permission grant.
824
+ */
825
+ export function normalizeAppCapabilities(capabilities) {
826
+ if (!capabilities || typeof capabilities !== 'object') {
827
+ return {};
828
+ }
829
+ const normalized = {};
830
+ if (capabilities.workspaceDatabases === 'read') {
831
+ normalized.workspaceDatabases = 'read';
832
+ }
833
+ return normalized;
834
+ }
835
+
795
836
  export function resolveConfiguredAppSkills(appConfig, projectDir) {
796
837
  const configured = Array.isArray(appConfig.skills) ? appConfig.skills : [];
797
838
  const projectRoot = resolve(projectDir);
@@ -831,13 +872,14 @@ export function resolveConfiguredAppSkills(appConfig, projectDir) {
831
872
  /**
832
873
  * Build the app bundle: generate entry file, run `vite build`, package into .notis/output/.
833
874
  */
834
- export async function buildArtifact(projectDir) {
875
+ export async function buildArtifact(projectDir, { stdio = 'inherit' } = {}) {
835
876
  await prepareArtifactBuild(projectDir);
836
877
 
837
878
  // Run Vite build
838
879
  await runProjectScript({
839
880
  projectDir,
840
881
  scriptName: 'build',
882
+ stdio,
841
883
  });
842
884
 
843
885
  // Verify the canonical `.notis/output/bundle` packaging contract.
@@ -948,8 +990,13 @@ export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
948
990
  if (existsSync(pkgPath)) {
949
991
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
950
992
  pkg.name = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
993
+ // A scaffold creates a new app identity, even when its source comes from a
994
+ // versioned Store example. New apps must therefore start their own release
995
+ // history instead of inheriting the example app's registry version.
996
+ pkg.notisAppVersion = '0.1.0';
951
997
  ensureScaffoldLocalSdk(projectDir, pkg);
952
998
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
999
+ normalizeScaffoldLockfile(projectDir, pkg);
953
1000
  }
954
1001
 
955
1002
  // Update notis.config.ts with the app name
@@ -983,6 +1030,20 @@ export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
983
1030
  return { projectDir };
984
1031
  }
985
1032
 
1033
+ function normalizeScaffoldLockfile(projectDir, pkg) {
1034
+ const lockPath = join(projectDir, 'package-lock.json');
1035
+ if (!existsSync(lockPath)) {
1036
+ return;
1037
+ }
1038
+
1039
+ const lockfile = JSON.parse(readFileSync(lockPath, 'utf-8'));
1040
+ lockfile.name = pkg.name;
1041
+ if (lockfile.packages?.['']) {
1042
+ lockfile.packages[''].name = pkg.name;
1043
+ }
1044
+ writeFileSync(lockPath, JSON.stringify(lockfile, null, 2) + '\n');
1045
+ }
1046
+
986
1047
  function ensureScaffoldLocalSdk(projectDir, pkg) {
987
1048
  let shouldInstallLocalSdk = false;
988
1049
  for (const dependencyGroup of ['dependencies', 'devDependencies']) {
@@ -24,7 +24,15 @@ function defaultDesktopAppName(apiBase) {
24
24
  }
25
25
  }
26
26
 
27
- export function getDesktopAuthRecovery(runtime) {
27
+ /**
28
+ * Recovery hints for an unusable desktop-managed credential.
29
+ *
30
+ * `mode` distinguishes the two cases an agent has to act on differently:
31
+ * "expired" means a session existed and the desktop app can renew it, while
32
+ * "missing" means this machine was never signed in — telling that caller to
33
+ * renew something is misleading.
34
+ */
35
+ export function getDesktopAuthRecovery(runtime, { mode = 'expired' } = {}) {
28
36
  const desktopRunning = isPidRunning(runtime.desktopPid);
29
37
  const appName = runtime.desktopAppName || defaultDesktopAppName(runtime.apiBase);
30
38
  let command = 'Start the Notis desktop app';
@@ -32,22 +40,63 @@ export function getDesktopAuthRecovery(runtime) {
32
40
  command = `open -a ${quoteShellArgument(appName)}`;
33
41
  }
34
42
 
35
- return {
36
- desktopRunning,
37
- appName,
38
- hints: [
39
- {
40
- command,
41
- reason: desktopRunning
42
- ? `Bring ${appName} forward so it can renew CLI authentication, then retry`
43
- : `Start ${appName} to renew expired CLI authentication, then retry`,
44
- },
45
- {
46
- command: 'notis doctor',
47
- reason: 'Retry the auth and API checks after the desktop app is ready',
48
- },
49
- ],
50
- };
43
+ let reason;
44
+ if (mode === 'missing') {
45
+ reason = desktopRunning
46
+ ? `Sign in to ${appName} to authenticate the CLI on this machine, then retry`
47
+ : `Install and sign in to ${appName} to authenticate the CLI on this machine, then retry`;
48
+ } else {
49
+ reason = desktopRunning
50
+ ? `Bring ${appName} forward so it can renew CLI authentication, then retry`
51
+ : `Start ${appName} to renew expired CLI authentication, then retry`;
52
+ }
53
+
54
+ const hints = [{ command, reason }];
55
+ if (mode === 'missing') {
56
+ // Leads the list: a machine that was never signed in may not even have an
57
+ // account yet, and `notis start` covers both cases.
58
+ hints.unshift({
59
+ command: 'notis start --email <address>',
60
+ reason: 'Create or access a Notis account and sign this machine in',
61
+ });
62
+ }
63
+ hints.push({
64
+ command: 'notis doctor',
65
+ reason: 'Retry the auth and API checks after the desktop app is ready',
66
+ });
67
+
68
+ return { desktopRunning, appName, hints };
69
+ }
70
+
71
+ /**
72
+ * Block until the desktop app writes an unexpired JWT into the CLI profile.
73
+ *
74
+ * Polls the local config file only — never the server. The desktop app is what
75
+ * mints the credential, and `transport.js` re-reads the profile before every
76
+ * request, so this loop exists purely so the caller's process can wait rather
77
+ * than fail and make the user re-run the command.
78
+ */
79
+ export async function waitForDesktopAuth({
80
+ loadConfig,
81
+ getJwt,
82
+ isJwtExpired,
83
+ profileName = 'default',
84
+ timeoutMs = 300_000,
85
+ intervalMs = 2_000,
86
+ onTick,
87
+ } = {}) {
88
+ const deadline = Date.now() + timeoutMs;
89
+ for (;;) {
90
+ const jwt = getJwt(loadConfig(), profileName);
91
+ if (jwt && !isJwtExpired(jwt)) {
92
+ return jwt;
93
+ }
94
+ if (Date.now() >= deadline) {
95
+ return null;
96
+ }
97
+ if (onTick) onTick(Math.max(0, deadline - Date.now()));
98
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
99
+ }
51
100
  }
52
101
 
53
102
  export function createExpiredAuthError(runtime) {
@@ -6,6 +6,7 @@ export const EXIT_CODES = {
6
6
  conflict: 5,
7
7
  backend: 6,
8
8
  unexpected: 7,
9
+ payment: 8,
9
10
  };
10
11
 
11
12
  export class CliError extends Error {
@@ -22,6 +22,13 @@ function formatTable(rows, columns) {
22
22
  return [header, divider, ...body].join('\n');
23
23
  }
24
24
 
25
+ function formatHint(hint) {
26
+ if (typeof hint?.message === 'string' && hint.message) {
27
+ return ` ${hint.message}`;
28
+ }
29
+ return ` ${hint?.command || ''} ${hint?.reason || ''}`.trimEnd();
30
+ }
31
+
25
32
  function yamlScalar(value) {
26
33
  if (value === null || value === undefined) {
27
34
  return 'null';
@@ -94,6 +101,14 @@ export class OutputManager {
94
101
  process.stderr.write(`${message}\n`);
95
102
  }
96
103
 
104
+ emitProgress({ phase, message, requestId = null }) {
105
+ if (this.runtime.quiet) {
106
+ return;
107
+ }
108
+ const suffix = requestId ? ` (${requestId})` : '';
109
+ process.stderr.write(`[${phase}] ${message}${suffix}\n`);
110
+ }
111
+
97
112
  emitSuccess({
98
113
  ok = true,
99
114
  command,
@@ -116,7 +131,10 @@ export class OutputManager {
116
131
  meta: {
117
132
  profile: this.runtime.profileName,
118
133
  api_base: this.runtime.apiBase,
119
- mutating: Boolean(meta.mutating),
134
+ mutating:
135
+ meta.mutating === true
136
+ ? true
137
+ : (meta.mutating === false ? false : null),
120
138
  ...meta,
121
139
  },
122
140
  };
@@ -142,7 +160,7 @@ export class OutputManager {
142
160
  }
143
161
 
144
162
  if (hints.length) {
145
- const lines = hints.map((hint) => ` ${hint.command} ${hint.reason}`);
163
+ const lines = hints.map(formatHint);
146
164
  process.stdout.write(`\nNext:\n${lines.join('\n')}\n`);
147
165
  }
148
166
 
@@ -171,7 +189,7 @@ export class OutputManager {
171
189
 
172
190
  process.stderr.write(`Error: ${error.message}\n`);
173
191
  for (const hint of error.hints || []) {
174
- process.stderr.write(` ${hint.command} ${hint.reason}\n`);
192
+ process.stderr.write(`${formatHint(hint)}\n`);
175
193
  }
176
194
  return error.exitCode || EXIT_CODES.unexpected;
177
195
  }