@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.
Files changed (157) hide show
  1. package/README.md +33 -9
  2. package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +7 -0
  3. package/dist/scaffolds/notis-affiliate-prospects/README.md +20 -0
  4. package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +42 -0
  5. package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +7 -0
  6. package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +248 -0
  7. package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +134 -0
  8. package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +108 -0
  9. package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +35 -0
  10. package/dist/scaffolds/notis-affiliate-prospects/components.json +20 -0
  11. package/dist/scaffolds/notis-affiliate-prospects/index.html +12 -0
  12. package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +218 -0
  13. package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +52 -0
  14. package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +100 -0
  15. package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +25 -0
  16. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
  17. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
  18. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
  19. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +187 -0
  20. package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +65 -0
  21. package/dist/scaffolds/notis-affiliate-prospects/package.json +32 -0
  22. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/package.json +32 -0
  23. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectActionBar.tsx +273 -0
  24. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  25. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  26. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +90 -0
  27. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useBackend.ts +41 -0
  28. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useMultiSelect.ts +503 -0
  29. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotis.ts +34 -0
  30. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  31. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTool.ts +64 -0
  32. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTools.ts +56 -0
  33. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  34. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
  35. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/index.ts +54 -0
  36. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/provider.tsx +43 -0
  37. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/runtime.ts +161 -0
  38. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +38 -0
  39. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/ui.ts +15 -0
  40. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/vite.ts +56 -0
  41. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/tsconfig.json +15 -0
  42. package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +6 -0
  43. package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +72 -0
  44. package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +242 -0
  45. package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +51 -0
  46. package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +23 -0
  47. package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +11 -0
  48. package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
  49. package/dist/scaffolds/notis-database/notis.config.ts +0 -1
  50. package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
  51. package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
  52. package/dist/scaffolds/notis-journal/app/globals.css +37 -0
  53. package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
  54. package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
  55. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
  56. package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
  57. package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
  58. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
  59. package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
  60. package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
  61. package/dist/scaffolds/notis-journal/components.json +20 -0
  62. package/dist/scaffolds/notis-journal/index.html +12 -0
  63. package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
  64. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  65. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  66. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  67. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  68. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  69. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  70. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
  71. package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
  72. package/dist/scaffolds/notis-journal/package.json +34 -0
  73. package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
  74. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  75. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
  76. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  77. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  78. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  79. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
  80. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
  81. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
  82. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  83. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
  84. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
  85. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  86. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
  87. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  88. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
  89. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
  90. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  91. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  92. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
  93. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
  94. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
  95. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
  96. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
  97. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
  98. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
  99. package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
  100. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
  101. package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
  102. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
  103. package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
  104. package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
  105. package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
  106. package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
  107. package/dist/scaffolds/notis-notes/app/page.tsx +17 -373
  108. package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
  109. package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
  110. package/dist/scaffolds/notis-random/notis.config.ts +0 -1
  111. package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
  112. package/dist/scaffolds.json +22 -0
  113. package/package.json +3 -3
  114. package/skills/notis-apps/SKILL.md +43 -5
  115. package/skills/notis-apps/cli.md +22 -6
  116. package/skills/notis-cli/SKILL.md +20 -2
  117. package/skills/notis-query/cli.md +1 -1
  118. package/src/cli.js +8 -1
  119. package/src/command-specs/apps.js +307 -36
  120. package/src/command-specs/diagnostics.js +674 -0
  121. package/src/command-specs/helpers.js +4 -1
  122. package/src/command-specs/index.js +7 -1
  123. package/src/command-specs/meta.js +8 -2
  124. package/src/command-specs/smoke.js +386 -0
  125. package/src/command-specs/tools.js +237 -44
  126. package/src/runtime/agent-browser.js +204 -21
  127. package/src/runtime/app-changelog.js +79 -0
  128. package/src/runtime/app-dev-server.js +21 -4
  129. package/src/runtime/app-dev-sessions.js +99 -1
  130. package/src/runtime/app-platform.js +197 -18
  131. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  132. package/src/runtime/desktop-auth.js +93 -0
  133. package/src/runtime/output.js +12 -1
  134. package/src/runtime/profiles.js +72 -16
  135. package/src/runtime/store-screenshot.js +138 -0
  136. package/src/runtime/transport.js +24 -82
  137. package/template/.harness/index.html.tmpl +128 -6
  138. package/template/CHANGELOG.md +5 -0
  139. package/template/app/page.tsx +11 -41
  140. package/template/notis.config.ts +0 -1
  141. package/template/package-lock.json +4137 -0
  142. package/template/package.json +1 -0
  143. package/template/packages/sdk/package.json +4 -0
  144. package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  145. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  146. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
  147. package/template/packages/sdk/src/config.ts +74 -0
  148. package/template/packages/sdk/src/documents.ts +229 -0
  149. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  150. package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
  151. package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
  152. package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
  153. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
  154. package/template/packages/sdk/src/index.ts +30 -1
  155. package/template/packages/sdk/src/runtime.ts +76 -17
  156. package/template/packages/sdk/src/styles.css +148 -0
  157. /package/{template → dist/scaffolds/notis-affiliate-prospects}/packages/sdk/src/hooks/useDatabase.ts +0 -0
@@ -0,0 +1,674 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { usageError } from '../runtime/errors.js';
4
+ import {
5
+ COMPOSIO_MULTI_EXECUTE_TOOL,
6
+ fetchToolDiscovery,
7
+ healthCheck,
8
+ nextIdempotencyKey,
9
+ runToolCommand,
10
+ } from './helpers.js';
11
+
12
+ const ENTITLEMENT_FIELDS = [
13
+ 'created_at',
14
+ 'included_credit_seat_count',
15
+ 'on_demand_enabled',
16
+ 'on_demand_limit',
17
+ 'on_demand_period_used',
18
+ 'on_demand_status',
19
+ 'price_id',
20
+ 'sub_end',
21
+ 'sub_start',
22
+ 'sub_status',
23
+ 'usage_period_end',
24
+ 'usage_period_start',
25
+ ];
26
+ export const CREATE_DEBUG_ENTITLEMENT_OVERRIDE_TOOL = 'LOCAL_NOTIS_CREATE_DEBUG_ENTITLEMENT_OVERRIDE';
27
+
28
+ function sqlLiteral(value) {
29
+ return `'${String(value).replaceAll("'", "''")}'`;
30
+ }
31
+
32
+ function discoveredToolNames(payload) {
33
+ const names = [];
34
+ for (const result of payload.results || []) {
35
+ names.push(...(result.primary_tool_slugs || []), ...(result.related_tool_slugs || []));
36
+ }
37
+ return [...new Set(names.filter(Boolean))];
38
+ }
39
+
40
+ async function discoverSupabaseSqlTool(runtime) {
41
+ const payload = await fetchToolDiscovery(
42
+ runtime,
43
+ 'Execute a read-only SQL query against the connected Notis application Supabase project for debugging user context and interaction costs',
44
+ 'query',
45
+ );
46
+ const names = discoveredToolNames(payload);
47
+ const match = names.find((name) => (
48
+ name.toUpperCase().includes('SUPABASE')
49
+ && name.toUpperCase().includes('EXECUTE_SQL')
50
+ ));
51
+ if (!match) {
52
+ throw usageError('No connected Supabase execute-SQL capability was discovered. Run `notis tools link mcp_supabase_notis_app`.');
53
+ }
54
+ return match;
55
+ }
56
+
57
+ export function unwrapToolExecutionPayload(payload) {
58
+ return (
59
+ payload?.data?.results?.[0]?.response?.data
60
+ ?? payload?.results?.[0]?.response?.data
61
+ ?? payload?.data?.result
62
+ ?? payload?.result
63
+ ?? payload?.data
64
+ ?? payload
65
+ );
66
+ }
67
+
68
+ function parseJsonCandidate(value) {
69
+ if (typeof value !== 'string') return null;
70
+ const trimmed = value.trim();
71
+ try {
72
+ return JSON.parse(trimmed);
73
+ } catch {
74
+ const boundaryEnd = trimmed.lastIndexOf('</untrusted-data');
75
+ const boundaryStart = boundaryEnd >= 0
76
+ ? trimmed.lastIndexOf('<untrusted-data', boundaryEnd)
77
+ : -1;
78
+ const contentStart = boundaryStart >= 0 ? trimmed.indexOf('>', boundaryStart) + 1 : -1;
79
+ if (contentStart > 0 && boundaryEnd > contentStart) {
80
+ // Supabase MCP sometimes returns the prose envelope with literal "\\n"
81
+ // separators. Strip those only at the boundary: globally replacing them
82
+ // corrupts valid escaped newlines inside JSON string values.
83
+ const inner = trimmed
84
+ .slice(contentStart, boundaryEnd)
85
+ .trim()
86
+ .replace(/^(?:\\r?\\n)+/, '')
87
+ .replace(/(?:\\r?\\n)+$/, '')
88
+ .trim();
89
+ try {
90
+ return JSON.parse(inner);
91
+ } catch (error) {
92
+ const nested = parseJsonCandidate(inner);
93
+ if (nested !== null) return nested;
94
+ throw usageError(
95
+ `Supabase diagnostic rows were not valid JSON: ${error instanceof Error ? error.message : String(error)}; prefix=${JSON.stringify(inner.slice(0, 100))}`,
96
+ );
97
+ }
98
+ }
99
+ const arrayMatch = trimmed.match(/(\[[\s\S]*\])/);
100
+ if (arrayMatch) {
101
+ try {
102
+ return JSON.parse(arrayMatch[1]);
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ export function extractSqlRows(payload) {
112
+ let current = unwrapToolExecutionPayload(payload);
113
+ for (let depth = 0; depth < 8; depth += 1) {
114
+ if (Array.isArray(current)) return current;
115
+ if (typeof current === 'string') {
116
+ const parsed = parseJsonCandidate(current);
117
+ if (parsed === null) break;
118
+ current = parsed;
119
+ continue;
120
+ }
121
+ if (!current || typeof current !== 'object') break;
122
+ if (Array.isArray(current.rows)) return current.rows;
123
+ if ('result' in current) {
124
+ current = current.result;
125
+ continue;
126
+ }
127
+ if ('data' in current) {
128
+ current = current.data;
129
+ continue;
130
+ }
131
+ break;
132
+ }
133
+ const shape = current && typeof current === 'object'
134
+ ? `object keys: ${Object.keys(current).sort().join(', ')}`
135
+ : typeof current;
136
+ if (current && typeof current === 'object' && current.error) {
137
+ const message = typeof current.error === 'string'
138
+ ? current.error
139
+ : (current.error.message || JSON.stringify(current.error));
140
+ throw usageError(`Supabase diagnostic query failed: ${String(message).slice(0, 500)}`);
141
+ }
142
+ if (typeof current === 'string') {
143
+ const safePrefix = current.split('<untrusted-data', 1)[0].trim();
144
+ const boundaryEnd = current.lastIndexOf('</untrusted-data');
145
+ const boundaryStart = boundaryEnd >= 0
146
+ ? current.lastIndexOf('<untrusted-data', boundaryEnd)
147
+ : -1;
148
+ const contentStart = boundaryStart >= 0 ? current.indexOf('>', boundaryStart) + 1 : -1;
149
+ const inner = contentStart > 0 && boundaryEnd > contentStart
150
+ ? current.slice(contentStart, boundaryEnd).trim()
151
+ : '';
152
+ const boundaryShape = boundaryStart >= 0
153
+ ? `; boundary content length=${inner.length}, starts=${JSON.stringify(inner.slice(0, 1))}, ends=${JSON.stringify(inner.slice(-1))}`
154
+ : '';
155
+ throw usageError(
156
+ `Supabase diagnostic query returned non-row text${safePrefix ? `: ${safePrefix.slice(0, 500)}` : ''}${boundaryShape}`,
157
+ );
158
+ }
159
+ throw usageError(`The Supabase SQL capability returned an unsupported result shape (${shape}).`);
160
+ }
161
+
162
+ async function executeReadOnlySql(ctx, query, phase) {
163
+ // An exported override applies to ordinary tool calls, not to the read-only
164
+ // commands that inspect or regenerate the assertion itself. The backend
165
+ // intentionally blocks SQL under an override, so diagnostics must explicitly
166
+ // use the operator's own unmodified context.
167
+ const diagnosticRuntime = {
168
+ ...ctx.runtime,
169
+ debugEntitlementOverride: null,
170
+ };
171
+ ctx.output.emitProgress({ phase: 'discover', message: 'Resolving the connected Notis Supabase SQL capability' });
172
+ const toolName = await discoverSupabaseSqlTool(diagnosticRuntime);
173
+ ctx.output.emitProgress({ phase, message: 'Running the read-only diagnostic query' });
174
+ const result = await runToolCommand({
175
+ runtime: diagnosticRuntime,
176
+ toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
177
+ arguments_: {
178
+ tools: [{ tool_slug: toolName, arguments: { query } }],
179
+ },
180
+ mutating: false,
181
+ });
182
+ if (result.payload?.successful === false || result.payload?.error) {
183
+ throw usageError(
184
+ `Supabase diagnostic query failed: ${result.payload.error || 'unknown SQL error'}`,
185
+ );
186
+ }
187
+ return {
188
+ rows: extractSqlRows(result.payload),
189
+ requestId: result.requestId,
190
+ toolName,
191
+ };
192
+ }
193
+
194
+ function redactIdentifier(value) {
195
+ if (!value) return null;
196
+ const text = String(value);
197
+ const digest = createHash('sha256').update(text).digest('hex').slice(0, 12);
198
+ return `${text.slice(0, 3)}…#${digest}`;
199
+ }
200
+
201
+ function canonicalObjectHash(value) {
202
+ const ordered = Object.fromEntries(
203
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)),
204
+ );
205
+ return createHash('sha256').update(JSON.stringify(ordered)).digest('hex');
206
+ }
207
+
208
+ function entitlementParityHash(context) {
209
+ const canonical = { scope_type: context.billing.scope_type };
210
+ for (const field of ENTITLEMENT_FIELDS) {
211
+ let value = context.billing[field] ?? null;
212
+ if (
213
+ (field === 'created_at' || field.endsWith('_start') || field.endsWith('_end'))
214
+ && typeof value === 'string'
215
+ ) {
216
+ const timestamp = Date.parse(value);
217
+ if (Number.isFinite(timestamp)) value = timestamp;
218
+ } else if (typeof value === 'number' && !Number.isInteger(value)) {
219
+ value = Math.round(value * 1_000_000) / 1_000_000;
220
+ }
221
+ canonical[field] = value;
222
+ }
223
+ return canonicalObjectHash(canonical);
224
+ }
225
+
226
+ export function buildUserContextSql(reference) {
227
+ const literal = sqlLiteral(reference);
228
+ return `
229
+ WITH target AS (
230
+ SELECT u.*
231
+ FROM public.users AS u
232
+ LEFT JOIN public.user_primary_emails AS upe ON upe.user_id = u.user_id
233
+ WHERE u.user_id::text = ${literal}
234
+ OR lower(upe.primary_email) = lower(${literal})
235
+ ORDER BY CASE WHEN u.user_id::text = ${literal} THEN 0 ELSE 1 END
236
+ LIMIT 1
237
+ ),
238
+ effective AS (
239
+ SELECT
240
+ target.*,
241
+ team.row_json AS team_json,
242
+ CASE WHEN team.row_json IS NULL THEN 'user' ELSE 'team' END AS scope_type,
243
+ COALESCE(team.row_json, to_jsonb(target)) AS scope_json
244
+ FROM target
245
+ LEFT JOIN LATERAL (
246
+ SELECT to_jsonb(t) AS row_json
247
+ FROM public.teams AS t
248
+ WHERE t.id::text = target.team_id::text
249
+ OR t.owner_id = target.user_id
250
+ ORDER BY CASE WHEN t.id::text = target.team_id::text THEN 0 ELSE 1 END
251
+ LIMIT 1
252
+ ) AS team ON true
253
+ )
254
+ SELECT jsonb_build_object(
255
+ 'user_id', effective.user_id,
256
+ 'primary_email', (
257
+ SELECT upe.primary_email FROM public.user_primary_emails AS upe
258
+ WHERE upe.user_id = effective.user_id LIMIT 1
259
+ ),
260
+ 'test_user', COALESCE((to_jsonb(effective)->>'test_user')::boolean, false),
261
+ 'users_for_eval', COALESCE((to_jsonb(effective)->>'users_for_eval')::boolean, false),
262
+ 'channel_runtime_defaults', COALESCE(to_jsonb(effective)->'channel_runtime_defaults', '{}'::jsonb),
263
+ 'billing', jsonb_build_object(
264
+ 'created_at', to_jsonb(effective)->>'created_at',
265
+ 'scope_type', effective.scope_type,
266
+ 'scope_id', COALESCE(effective.scope_json->>'id', effective.user_id::text),
267
+ 'billing_owner_id', COALESCE(effective.team_json->>'owner_id', effective.user_id::text),
268
+ 'price_id', effective.scope_json->>'price_id',
269
+ 'sub_status', effective.scope_json->>'sub_status',
270
+ 'sub_start', effective.scope_json->>'sub_start',
271
+ 'sub_end', effective.scope_json->>'sub_end',
272
+ 'usage_period_start', effective.scope_json->>'usage_period_start',
273
+ 'usage_period_end', effective.scope_json->>'usage_period_end',
274
+ 'on_demand_enabled', COALESCE((effective.scope_json->>'on_demand_enabled')::boolean, false),
275
+ 'on_demand_limit', (effective.scope_json->>'on_demand_limit')::numeric,
276
+ 'on_demand_status', effective.scope_json->>'on_demand_status',
277
+ 'on_demand_period_used', COALESCE((effective.scope_json->>'on_demand_period_used')::numeric, 0),
278
+ 'included_credit_seat_count', COALESCE((effective.scope_json->>'included_credit_seat_count')::integer, 1)
279
+ ),
280
+ 'runtime', jsonb_build_object(
281
+ 'model', COALESCE(
282
+ to_jsonb(effective)->'settings'->>'model',
283
+ to_jsonb(effective)->'settings'->>'model_name'
284
+ ),
285
+ 'mode', to_jsonb(effective)->'settings'->>'mode',
286
+ 'reasoning_effort', to_jsonb(effective)->'settings'->>'reasoning_effort',
287
+ 'channel_defaults', COALESCE(to_jsonb(effective)->'channel_runtime_defaults', '{}'::jsonb),
288
+ 'last_channel', (
289
+ SELECT to_jsonb(ca)->>'provider'
290
+ FROM public.channel_accounts AS ca
291
+ WHERE ca.id::text = to_jsonb(effective)->>'last_channel_account_id'
292
+ LIMIT 1
293
+ )
294
+ ),
295
+ 'plan', (
296
+ SELECT jsonb_build_object(
297
+ 'price', to_jsonb(prices),
298
+ 'catalog', to_jsonb(plan)
299
+ )
300
+ FROM public.prices AS prices
301
+ LEFT JOIN public.pricing_plans AS plan
302
+ ON effective.scope_json->>'price_id' IN (
303
+ plan.test_monthly_price_id, plan.test_yearly_price_id,
304
+ plan.prod_monthly_price_id, plan.prod_yearly_price_id
305
+ )
306
+ WHERE prices.stripe_price_id = effective.scope_json->>'price_id'
307
+ LIMIT 1
308
+ ),
309
+ 'channels', (
310
+ SELECT COALESCE(jsonb_agg(jsonb_build_object(
311
+ 'provider', to_jsonb(ca)->>'provider',
312
+ 'service', to_jsonb(ca)->>'messaging_service',
313
+ 'status', to_jsonb(ca)->>'status'
314
+ )), '[]'::jsonb)
315
+ FROM public.channel_accounts AS ca WHERE ca.user_id = effective.user_id
316
+ ),
317
+ 'integrations', jsonb_build_object(
318
+ 'composio_labeled_accounts', (
319
+ SELECT count(*) FROM public.user_integration_labels AS c
320
+ WHERE c.user_id = effective.user_id
321
+ ),
322
+ 'mcp', (SELECT count(*) FROM public.user_mcps AS m WHERE m.user_id = effective.user_id)
323
+ )
324
+ ) AS context
325
+ FROM effective;`;
326
+ }
327
+
328
+ export async function loadUserContext(ctx, reference) {
329
+ const execution = await executeReadOnlySql(ctx, buildUserContextSql(reference), 'user-context');
330
+ const row = execution.rows[0];
331
+ const raw = row?.context ?? row;
332
+ if (!raw) {
333
+ throw usageError(`No Notis user matched "${reference}".`);
334
+ }
335
+ const context = typeof raw === 'string' ? JSON.parse(raw) : raw;
336
+ const parityHash = entitlementParityHash(context);
337
+ return {
338
+ context,
339
+ parityHash,
340
+ requestId: execution.requestId,
341
+ toolName: execution.toolName,
342
+ };
343
+ }
344
+
345
+ async function debugUserContextHandler(ctx) {
346
+ const loaded = await loadUserContext(ctx, ctx.args.user);
347
+ const context = loaded.context;
348
+ const redactPlanIds = (value) => {
349
+ if (Array.isArray(value)) return value.map(redactPlanIds);
350
+ if (!value || typeof value !== 'object') return value;
351
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
352
+ key,
353
+ /(?:^|_)(?:price_id|messaging_service_id)$/.test(key)
354
+ ? redactIdentifier(nested)
355
+ : redactPlanIds(nested),
356
+ ]));
357
+ };
358
+ const redacted = {
359
+ identity: {
360
+ user: redactIdentifier(context.user_id),
361
+ email: redactIdentifier(context.primary_email),
362
+ test_user: Boolean(context.test_user),
363
+ users_for_eval: Boolean(context.users_for_eval),
364
+ },
365
+ billing: {
366
+ ...context.billing,
367
+ scope_id: redactIdentifier(context.billing?.scope_id),
368
+ billing_owner_id: redactIdentifier(context.billing?.billing_owner_id),
369
+ price_id: redactIdentifier(context.billing?.price_id),
370
+ },
371
+ plan: redactPlanIds(context.plan),
372
+ usage_settings: {
373
+ on_demand_enabled: context.billing?.on_demand_enabled,
374
+ on_demand_limit: context.billing?.on_demand_limit,
375
+ usage_period_start: context.billing?.usage_period_start,
376
+ usage_period_end: context.billing?.usage_period_end,
377
+ },
378
+ feature_flags: redactPlanIds(context.plan?.price || {}),
379
+ model_and_channel_defaults: context.runtime || {
380
+ channel_defaults: context.channel_runtime_defaults || {},
381
+ },
382
+ channels: context.channels || [],
383
+ integrations: context.integrations || {},
384
+ parity_hash: loaded.parityHash,
385
+ };
386
+ ctx.output.emitProgress({ phase: 'complete', message: 'Resolved effective redacted user context', requestId: loaded.requestId });
387
+ return ctx.output.emitSuccess({
388
+ command: ctx.spec.command_path.join(' '),
389
+ data: redacted,
390
+ humanSummary: `Resolved effective user context for ${redacted.identity.email}`,
391
+ requestId: loaded.requestId,
392
+ meta: { mutating: false, sql_tool: loaded.toolName },
393
+ renderHuman: () => JSON.stringify(redacted, null, 2),
394
+ });
395
+ }
396
+
397
+ export async function mintEntitlementOverride(ctx, referenceUser, actorUser = null) {
398
+ const requestedTtl = Number.parseInt(ctx.options.ttlSeconds || '600', 10);
399
+ if (!Number.isInteger(requestedTtl) || requestedTtl < 30 || requestedTtl > 900) {
400
+ throw usageError('--ttl-seconds must be between 30 and 900.');
401
+ }
402
+ const result = await runToolCommand({
403
+ runtime: { ...ctx.runtime, debugEntitlementOverride: null },
404
+ toolName: CREATE_DEBUG_ENTITLEMENT_OVERRIDE_TOOL,
405
+ arguments_: {
406
+ reference_user: referenceUser,
407
+ ttl_seconds: requestedTtl,
408
+ ...(actorUser ? { actor_user: actorUser } : {}),
409
+ },
410
+ mutating: false,
411
+ });
412
+ const minted = result.payload || {};
413
+ if (!minted.override || !minted.encoded || minted.persistent !== false) {
414
+ throw usageError('The backend did not return a signed non-persistent entitlement override.');
415
+ }
416
+ return { ...minted, requestId: result.requestId || minted.request_id };
417
+ }
418
+
419
+ async function debugEntitlementOverrideHandler(ctx) {
420
+ const minted = await mintEntitlementOverride(
421
+ ctx,
422
+ ctx.args.referenceUser,
423
+ ctx.options.actorUser,
424
+ );
425
+ return ctx.output.emitSuccess({
426
+ command: ctx.spec.command_path.join(' '),
427
+ data: {
428
+ ...minted,
429
+ shell: `export NOTIS_DEBUG_ENTITLEMENT_OVERRIDE=${minted.encoded}`,
430
+ },
431
+ humanSummary: 'Created a request-scoped entitlement parity override; no account row was modified.',
432
+ requestId: minted.requestId,
433
+ meta: { mutating: false },
434
+ renderHuman: () => [
435
+ 'Request-scoped entitlement parity override created (no database mutation).',
436
+ `Actor: ${minted.actor_user_id}`,
437
+ `Expires: ${minted.override.expires_at}`,
438
+ `export NOTIS_DEBUG_ENTITLEMENT_OVERRIDE=${minted.encoded}`,
439
+ ].join('\n'),
440
+ });
441
+ }
442
+
443
+ function buildTraceCostSql(reference) {
444
+ const literal = sqlLiteral(reference);
445
+ return `
446
+ SELECT jsonb_build_object(
447
+ 'id', i.id,
448
+ 'trace_id', to_jsonb(i)->>'trace_id',
449
+ 'created_at', to_jsonb(i)->>'created_at',
450
+ 'updated_at', to_jsonb(i)->>'updated_at',
451
+ 'completed_at', COALESCE(to_jsonb(i)->>'completed_at', to_jsonb(i)->>'ended_at'),
452
+ 'status', COALESCE(to_jsonb(i)->>'status', to_jsonb(i)->>'state'),
453
+ 'total_cost', COALESCE((to_jsonb(i)->>'total_cost')::numeric, 0),
454
+ 'model', COALESCE(to_jsonb(i)->>'model', to_jsonb(i)->>'model_name'),
455
+ 'usage_details', COALESCE(to_jsonb(i)->'usage_details', '{}'::jsonb),
456
+ 'error', COALESCE(to_jsonb(i)->>'error', to_jsonb(i)->>'error_message'),
457
+ 'retry_count', COALESCE((to_jsonb(i)->>'retry_count')::integer, 0)
458
+ ) AS interaction
459
+ FROM public.interactions AS i
460
+ WHERE i.id::text = ${literal}
461
+ OR to_jsonb(i)->>'trace_id' = ${literal}
462
+ ORDER BY i.created_at ASC;`;
463
+ }
464
+
465
+ export function traceFileDiagnostics(path) {
466
+ if (!path) return null;
467
+ if (!existsSync(path)) throw usageError(`Trace file not found: ${path}`);
468
+ const trace = JSON.parse(readFileSync(path, 'utf-8'));
469
+ const text = JSON.stringify(trace).toLowerCase();
470
+ const modelCounts = {};
471
+ const generationIds = new Set();
472
+ const retryEventIds = new Set();
473
+ const toolFailureIds = new Set();
474
+ let anonymousGenerations = 0;
475
+ let anonymousRetryEvents = 0;
476
+ let anonymousToolFailures = 0;
477
+ let explicitRetries = 0;
478
+ const visit = (value) => {
479
+ if (!value || typeof value !== 'object') return;
480
+ if (!Array.isArray(value)) {
481
+ const observationType = String(value.type || value.observation_type || '').toLowerCase();
482
+ const eventName = String(value.name || value.event || '').toLowerCase();
483
+ const status = String(value.status || value.level || '').toLowerCase();
484
+ const recordId = String(value.id || value.observation_id || '');
485
+ if (observationType.includes('generation')) {
486
+ if (recordId) generationIds.add(recordId);
487
+ else anonymousGenerations += 1;
488
+ if (typeof value.model === 'string' && value.model) {
489
+ modelCounts[value.model] = (modelCounts[value.model] || 0) + 1;
490
+ }
491
+ }
492
+ const retryCount = Number(value.retry_count ?? value.retryCount ?? value.retries);
493
+ if (Number.isFinite(retryCount) && retryCount > 0) {
494
+ explicitRetries += retryCount;
495
+ } else if (/\bretr(?:y|ies|ied|ying)\b/.test(`${observationType} ${eventName}`)) {
496
+ if (recordId) retryEventIds.add(recordId);
497
+ else anonymousRetryEvents += 1;
498
+ }
499
+ const isToolRecord = observationType.includes('tool')
500
+ || eventName.includes('tool');
501
+ const isFailure = ['error', 'failed', 'failure'].some((token) => (
502
+ status.includes(token)
503
+ || String(value.error || '').toLowerCase().includes(token)
504
+ ));
505
+ if (isToolRecord && isFailure) {
506
+ if (recordId) toolFailureIds.add(recordId);
507
+ else anonymousToolFailures += 1;
508
+ }
509
+ }
510
+ for (const nested of Object.values(value)) visit(nested);
511
+ };
512
+ visit(trace);
513
+ return {
514
+ generations: generationIds.size + anonymousGenerations
515
+ || (text.match(/"generation"/g) || []).length,
516
+ retries: explicitRetries + retryEventIds.size + anonymousRetryEvents,
517
+ tool_failures: toolFailureIds.size + anonymousToolFailures,
518
+ model_counts: modelCounts,
519
+ source: path,
520
+ };
521
+ }
522
+
523
+ function usageModelCounts(interactions) {
524
+ const counts = {};
525
+ for (const interaction of interactions) {
526
+ if (typeof interaction.model === 'string' && interaction.model) {
527
+ counts[interaction.model] = (counts[interaction.model] || 0) + 1;
528
+ }
529
+ for (const usage of Object.values(interaction.usage_details || {})) {
530
+ if (usage && typeof usage === 'object' && typeof usage.model === 'string' && usage.model) {
531
+ counts[usage.model] = (counts[usage.model] || 0) + 1;
532
+ }
533
+ }
534
+ }
535
+ return counts;
536
+ }
537
+
538
+ async function debugTraceCostHandler(ctx) {
539
+ const execution = await executeReadOnlySql(ctx, buildTraceCostSql(ctx.args.traceOrInteraction), 'trace-cost');
540
+ const interactions = execution.rows.map((row) => row.interaction ?? row);
541
+ if (!interactions.length) {
542
+ throw usageError(`No interaction or trace matched "${ctx.args.traceOrInteraction}".`);
543
+ }
544
+ const costs = interactions.map((item) => Number(item.total_cost || 0));
545
+ const totalCost = costs.reduce((sum, cost) => sum + cost, 0);
546
+ const attributable = ctx.options.attributableCost === undefined
547
+ ? null
548
+ : Number(ctx.options.attributableCost);
549
+ if (attributable !== null && (!Number.isFinite(attributable) || attributable < 0)) {
550
+ throw usageError('--attributable-cost must be a non-negative number.');
551
+ }
552
+ const confirmedBug = Boolean(ctx.options.confirmedBug);
553
+ const refundableEstimate = confirmedBug && attributable !== null
554
+ ? Math.min(totalCost, attributable)
555
+ : null;
556
+ const traceDiagnostics = traceFileDiagnostics(ctx.options.traceFile);
557
+ const recordedModelCounts = usageModelCounts(interactions);
558
+ const modelCounts = traceDiagnostics && Object.keys(traceDiagnostics.model_counts).length
559
+ ? traceDiagnostics.model_counts
560
+ : recordedModelCounts;
561
+ const result = {
562
+ interaction_count: interactions.length,
563
+ latency: {
564
+ started_at: interactions[0].created_at,
565
+ completed_at: interactions.at(-1).completed_at || interactions.at(-1).updated_at,
566
+ },
567
+ generations: traceDiagnostics?.generations ?? null,
568
+ retries: traceDiagnostics?.retries
569
+ ?? interactions.reduce((sum, item) => sum + Number(item.retry_count || 0), 0),
570
+ tool_failures: traceDiagnostics?.tool_failures ?? null,
571
+ model_counts: modelCounts,
572
+ total_provider_cost: totalCost,
573
+ refundable_cost_estimate: refundableEstimate,
574
+ refund_basis: refundableEstimate === null
575
+ ? 'Requires confirmed Notis bug evidence and an explicit bug-attributable cost. Misunderstandings and service limitations are not goodwill bugs.'
576
+ : 'Capped at the lower of recorded provider cost and the explicitly attributed true-bug amount.',
577
+ interactions,
578
+ trace_file: traceDiagnostics,
579
+ };
580
+ return ctx.output.emitSuccess({
581
+ command: ctx.spec.command_path.join(' '),
582
+ data: result,
583
+ humanSummary: `Attributed ${interactions.length} interaction(s) and ${totalCost.toFixed(4)} in recorded provider cost.`,
584
+ requestId: execution.requestId,
585
+ meta: { mutating: false, sql_tool: execution.toolName },
586
+ renderHuman: () => JSON.stringify(result, null, 2),
587
+ });
588
+ }
589
+
590
+ async function debugWorkerIdentityHandler(ctx) {
591
+ ctx.output.emitProgress({ phase: 'worker-identity', message: 'Reading backend worker identity' });
592
+ const result = await healthCheck(ctx.runtime);
593
+ const identity = result.payload?.runtime || result.payload;
594
+ return ctx.output.emitSuccess({
595
+ command: ctx.spec.command_path.join(' '),
596
+ data: identity,
597
+ humanSummary: `Worker ${identity.build_sha || 'unknown'} started ${identity.process_started_at || 'unknown'}.`,
598
+ requestId: result.requestId,
599
+ meta: { mutating: false },
600
+ renderHuman: () => JSON.stringify(identity, null, 2),
601
+ });
602
+ }
603
+
604
+ export const diagnosticCommandSpecs = [
605
+ {
606
+ command_path: ['debug', 'user-context'],
607
+ summary: 'Resolve a user’s effective redacted runtime and billing context.',
608
+ when_to_use: 'Use before reproducing user-specific plan, team, model, channel, or integration behavior.',
609
+ args_schema: { arguments: [{ token: '<user>', key: 'user', description: 'Notis user UUID or primary email.' }], options: [] },
610
+ examples: ['notis debug user-context user@example.com --json'],
611
+ output_schema: 'Returns redacted identity, effective billing scope, plan features, usage settings, defaults, channels, integrations, and a parity hash.',
612
+ mutates: false,
613
+ idempotent: true,
614
+ related_commands: ['notis debug entitlement-override <reference-user>', 'notis debug trace-cost <trace-or-interaction>'],
615
+ backend_call: { type: 'tool-discovery', name: 'Supabase execute SQL capability' },
616
+ handler: debugUserContextHandler,
617
+ },
618
+ {
619
+ command_path: ['debug', 'entitlement-override'],
620
+ summary: 'Create a short-lived request-scoped entitlement parity assertion.',
621
+ when_to_use: 'Use to reproduce a reference user’s plan gates without modifying any user or team row.',
622
+ args_schema: {
623
+ arguments: [{ token: '<reference-user>', key: 'referenceUser', description: 'Reference user UUID or primary email.' }],
624
+ options: [
625
+ { flags: '--ttl-seconds <n>', description: 'Lifetime from 30 to 900 seconds (default 600).' },
626
+ { flags: '--actor-user <user>', description: 'Bind the assertion to this test actor (defaults to the authenticated CLI user).' },
627
+ ],
628
+ },
629
+ examples: [
630
+ 'notis debug entitlement-override user@example.com --ttl-seconds 600',
631
+ 'notis debug entitlement-override user@example.com --actor-user <test-user-id> --ttl-seconds 600',
632
+ ],
633
+ output_schema: 'Returns a base64url request-scoped assertion for NOTIS_DEBUG_ENTITLEMENT_OVERRIDE.',
634
+ mutates: false,
635
+ idempotent: true,
636
+ related_commands: ['notis debug user-context <user>', 'notis smoke file-upload --parity-user <user>'],
637
+ backend_call: { type: 'tool', name: CREATE_DEBUG_ENTITLEMENT_OVERRIDE_TOOL },
638
+ handler: debugEntitlementOverrideHandler,
639
+ },
640
+ {
641
+ command_path: ['debug', 'trace-cost'],
642
+ summary: 'Attribute latency, retries, failures, generations, and refundable cost for a trace.',
643
+ when_to_use: 'Use when investigating looping, unexpectedly expensive, or failed Notis work.',
644
+ args_schema: {
645
+ arguments: [{ token: '<trace-or-interaction>', key: 'traceOrInteraction', description: 'Interaction UUID or Langfuse trace id.' }],
646
+ options: [
647
+ { flags: '--trace-file <path>', description: 'Optional exported trace JSON for generation and tool-failure attribution.' },
648
+ { flags: '--confirmed-bug', description: 'Mark that evidence confirms a true Notis bug.' },
649
+ { flags: '--attributable-cost <amount>', description: 'Cost attributed specifically to the confirmed bug.' },
650
+ ],
651
+ },
652
+ examples: ['notis debug trace-cost trace_123 --trace-file trace.json --confirmed-bug --attributable-cost 9'],
653
+ output_schema: 'Returns structured cost, latency, retry, generation, tool-failure, and refund-attribution data.',
654
+ mutates: false,
655
+ idempotent: true,
656
+ related_commands: ['notis debug user-context <user>'],
657
+ backend_call: { type: 'tool-discovery', name: 'Supabase execute SQL capability' },
658
+ handler: debugTraceCostHandler,
659
+ },
660
+ {
661
+ command_path: ['debug', 'worker-identity'],
662
+ summary: 'Read the active backend worker identity.',
663
+ when_to_use: 'Use to confirm which process, environment, build, and Supabase project handled a reproduction.',
664
+ args_schema: { arguments: [], options: [] },
665
+ examples: ['notis debug worker-identity --json'],
666
+ output_schema: 'Returns process start time, environment, build SHA, process id, and Supabase project.',
667
+ mutates: false,
668
+ idempotent: true,
669
+ require_auth: false,
670
+ related_commands: ['notis doctor'],
671
+ backend_call: { type: 'http', path: '/health' },
672
+ handler: debugWorkerIdentityHandler,
673
+ },
674
+ ];
@@ -115,7 +115,10 @@ export async function fetchToolSchema(runtime, toolName) {
115
115
  description: schema.description || '',
116
116
  parameters: schema.input_schema || { type: 'object', properties: {} },
117
117
  output_schema: schema.output_schema || {},
118
- schema_available: Boolean(schema.hasFullSchema),
118
+ schema_available: Boolean(
119
+ schema.hasFullSchema
120
+ || (schema.input_schema && typeof schema.input_schema === 'object'),
121
+ ),
119
122
  };
120
123
  }
121
124