@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.
- package/README.md +17 -0
- package/dist/scaffolds/notis-database/app/page.tsx +38 -41
- package/dist/scaffolds/notis-database/lib/types.ts +5 -7
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +1839 -0
- package/dist/scaffolds/notis-database/notis.config.ts +48 -1
- package/dist/scaffolds/notis-database/package-lock.json +3935 -0
- package/dist/scaffolds/notis-database/package.json +2 -1
- package/dist/scaffolds/notis-database/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +4 -1
- package/dist/scaffolds/notis-journal/CHANGELOG.md +4 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +1 -0
- package/dist/scaffolds/notis-journal/package-lock.json +4615 -0
- package/dist/scaffolds/notis-journal/package.json +2 -2
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +52 -1
- package/dist/scaffolds/notis-notes/app/page.tsx +39 -46
- package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +596 -0
- package/dist/scaffolds/notis-notes/lib/visible-properties.ts +144 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +752 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +48 -3
- package/dist/scaffolds/notis-notes/package-lock.json +4636 -0
- package/dist/scaffolds/notis-notes/package.json +7 -3
- package/dist/scaffolds/notis-notes/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-random/CHANGELOG.md +12 -2
- package/dist/scaffolds/notis-random/app/history/page.tsx +5 -4
- package/dist/scaffolds/notis-random/app/page.tsx +141 -74
- package/dist/scaffolds/notis-random/components/ui/button.tsx +50 -0
- package/dist/scaffolds/notis-random/components/ui/card.tsx +16 -0
- package/dist/scaffolds/notis-random/components/ui/input.tsx +23 -0
- package/dist/scaffolds/notis-random/components.json +20 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +26 -7
- package/dist/scaffolds/notis-random/lib/rng.ts +179 -19
- package/dist/scaffolds/notis-random/lib/roll-record.ts +103 -16
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +753 -0
- package/dist/scaffolds/notis-random/notis.config.ts +47 -3
- package/dist/scaffolds/notis-random/package-lock.json +4513 -0
- package/dist/scaffolds/notis-random/package.json +6 -2
- package/dist/scaffolds/notis-random/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +8 -1
- package/dist/scaffolds.json +8 -6
- package/package.json +2 -1
- package/skills/notis-apps/SKILL.md +449 -144
- package/skills/notis-apps/cli.md +15 -0
- package/skills/notis-cli/SKILL.md +8 -4
- package/skills/notis-onboarding/BRIEF.md +93 -0
- package/src/cli.js +52 -7
- package/src/command-specs/apps.js +154 -21
- package/src/command-specs/diagnostics.js +674 -0
- package/src/command-specs/helpers.js +4 -1
- package/src/command-specs/index.js +8 -0
- package/src/command-specs/onboarding.js +216 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +189 -5
- package/src/runtime/app-platform.js +65 -4
- package/src/runtime/desktop-auth.js +66 -17
- package/src/runtime/errors.js +1 -0
- package/src/runtime/output.js +21 -3
- package/src/runtime/profiles.js +214 -18
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +29 -1
- package/template/.harness/index.html.tmpl +30 -2
- package/template/packages/sdk/src/config.ts +38 -1
|
@@ -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(
|
|
118
|
+
schema_available: Boolean(
|
|
119
|
+
schema.hasFullSchema
|
|
120
|
+
|| (schema.input_schema && typeof schema.input_schema === 'object'),
|
|
121
|
+
),
|
|
119
122
|
};
|
|
120
123
|
}
|
|
121
124
|
|