@notis_ai/cli 0.2.0-beta.102.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +334 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_boundary_rules.json +50 -0
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/app/globals.css +44 -0
- package/dist/scaffolds/notis-database/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
- package/dist/scaffolds/notis-database/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-database/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-database/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
- package/dist/scaffolds/notis-database/components.json +20 -0
- package/dist/scaffolds/notis-database/index.html +12 -0
- package/dist/scaffolds/notis-database/lib/types.ts +134 -0
- package/dist/scaffolds/notis-database/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/notis.config.ts +26 -0
- package/dist/scaffolds/notis-database/package.json +31 -0
- package/dist/scaffolds/notis-database/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +558 -0
- package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
- package/dist/scaffolds/notis-database/tsconfig.json +23 -0
- package/dist/scaffolds/notis-database/vite.config.ts +22 -0
- package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
- package/dist/scaffolds/notis-journal/app/globals.css +37 -0
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
- package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-journal/components.json +20 -0
- package/dist/scaffolds/notis-journal/index.html +12 -0
- package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
- package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
- package/dist/scaffolds/notis-journal/package.json +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
- package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
- package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
- package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-notes/app/globals.css +3 -0
- package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +1950 -0
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-notes/components.json +20 -0
- package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +35 -0
- package/dist/scaffolds/notis-notes/package.json +31 -0
- package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-notes/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
- package/dist/scaffolds/notis-notes/vite.config.ts +10 -0
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/README.md +33 -0
- package/dist/scaffolds/notis-random/app/globals.css +11 -0
- package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
- package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-random/app/page.tsx +222 -0
- package/dist/scaffolds/notis-random/index.html +12 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
- package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
- package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
- package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
- 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/notis.config.ts +42 -0
- package/dist/scaffolds/notis-random/package.json +32 -0
- package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +129 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
- package/dist/scaffolds/notis-random/tsconfig.json +23 -0
- package/dist/scaffolds/notis-random/vite.config.ts +11 -0
- package/dist/scaffolds.json +47 -0
- package/package.json +45 -0
- package/skills/notis-apps/SKILL.md +219 -0
- package/skills/notis-apps/cli.md +244 -0
- package/skills/notis-cli/SKILL.md +269 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/cli.js +175 -0
- package/src/command-specs/apps.js +1939 -0
- package/src/command-specs/helpers.js +186 -0
- package/src/command-specs/index.js +14 -0
- package/src/command-specs/meta.js +161 -0
- package/src/command-specs/tools.js +499 -0
- package/src/runtime/agent-browser.js +464 -0
- package/src/runtime/app-boundary-validator.js +183 -0
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +712 -0
- package/src/runtime/app-dev-sessions.js +251 -0
- package/src/runtime/app-platform.js +1500 -0
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/cli-mode.generated.js +4 -0
- package/src/runtime/cli-mode.js +29 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/errors.js +55 -0
- package/src/runtime/help.js +60 -0
- package/src/runtime/output.js +180 -0
- package/src/runtime/ports.js +15 -0
- package/src/runtime/profiles.js +261 -0
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +347 -0
- package/template/.harness/index.html.tmpl +333 -0
- package/template/CHANGELOG.md +5 -0
- package/template/app/globals.css +3 -0
- package/template/app/layout.tsx +6 -0
- package/template/app/page.tsx +60 -0
- package/template/components/ui/badge.tsx +28 -0
- package/template/components/ui/button.tsx +53 -0
- package/template/components/ui/card.tsx +56 -0
- package/template/components.json +20 -0
- package/template/lib/utils.ts +6 -0
- package/template/metadata/screenshot-1.png +0 -0
- package/template/metadata/screenshot-2.png +0 -0
- package/template/metadata/screenshot-3.png +0 -0
- package/template/notis.config.ts +36 -0
- package/template/package-lock.json +4137 -0
- package/template/package.json +32 -0
- package/template/packages/sdk/package.json +36 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/template/packages/sdk/src/config.ts +145 -0
- package/template/packages/sdk/src/documents.ts +229 -0
- package/template/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/template/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/template/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/template/packages/sdk/src/hooks/useTool.ts +64 -0
- package/template/packages/sdk/src/hooks/useTools.ts +56 -0
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/template/packages/sdk/src/index.ts +83 -0
- package/template/packages/sdk/src/provider.tsx +43 -0
- package/template/packages/sdk/src/runtime.ts +220 -0
- package/template/packages/sdk/src/styles.css +186 -0
- package/template/packages/sdk/src/ui.ts +15 -0
- package/template/packages/sdk/src/vite.ts +56 -0
- package/template/packages/sdk/tsconfig.json +15 -0
- package/template/postcss.config.mjs +8 -0
- package/template/tailwind.config.ts +58 -0
- package/template/tsconfig.json +22 -0
- package/template/vite.config.ts +10 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const STORE_SCREENSHOT_ASPECT = 16 / 10;
|
|
5
|
+
const SHARED_BACKDROP_PATH = fileURLToPath(
|
|
6
|
+
new URL('./assets/store-screenshot-dark.png', import.meta.url),
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const ACCENT_PALETTES = {
|
|
10
|
+
blue: { glow: '#4f7cff', glow2: '#274690' },
|
|
11
|
+
violet: { glow: '#9b87f5', glow2: '#5038a1' },
|
|
12
|
+
emerald: { glow: '#34d399', glow2: '#176b55' },
|
|
13
|
+
amber: { glow: '#f2a94a', glow2: '#8b4f20' },
|
|
14
|
+
rose: { glow: '#fb7185', glow2: '#8f3048' },
|
|
15
|
+
sky: { glow: '#38bdf8', glow2: '#176887' },
|
|
16
|
+
fuchsia: { glow: '#d946ef', glow2: '#772786' },
|
|
17
|
+
teal: { glow: '#2dd4bf', glow2: '#176e68' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ACCENT_NAMES = Object.keys(ACCENT_PALETTES);
|
|
21
|
+
|
|
22
|
+
function stableHash(value) {
|
|
23
|
+
let hash = 2166136261;
|
|
24
|
+
for (const character of String(value || 'notis-app')) {
|
|
25
|
+
hash ^= character.codePointAt(0);
|
|
26
|
+
hash = Math.imul(hash, 16777619);
|
|
27
|
+
}
|
|
28
|
+
return hash >>> 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveStoreScreenshotAccent(accent, seed = 'notis-app') {
|
|
32
|
+
if (accent && ACCENT_PALETTES[accent]) {
|
|
33
|
+
return accent;
|
|
34
|
+
}
|
|
35
|
+
return ACCENT_NAMES[stableHash(seed) % ACCENT_NAMES.length];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function storeScreenshotLayout(width = 2000, height = 1250) {
|
|
39
|
+
const horizontalInset = Math.max(48, Math.round(width * 0.06));
|
|
40
|
+
const verticalInset = Math.max(40, Math.round(height * 0.06));
|
|
41
|
+
let cardWidth = width - horizontalInset * 2;
|
|
42
|
+
let cardHeight = Math.round(cardWidth / STORE_SCREENSHOT_ASPECT);
|
|
43
|
+
const maximumCardHeight = height - verticalInset * 2;
|
|
44
|
+
if (cardHeight > maximumCardHeight) {
|
|
45
|
+
cardHeight = maximumCardHeight;
|
|
46
|
+
cardWidth = Math.round(cardHeight * STORE_SCREENSHOT_ASPECT);
|
|
47
|
+
}
|
|
48
|
+
const cardLeft = Math.round((width - cardWidth) / 2);
|
|
49
|
+
const cardTop = Math.round((height - cardHeight) / 2) - Math.round(height * 0.008);
|
|
50
|
+
return {
|
|
51
|
+
width,
|
|
52
|
+
height,
|
|
53
|
+
cardWidth,
|
|
54
|
+
cardHeight,
|
|
55
|
+
cardLeft,
|
|
56
|
+
cardTop,
|
|
57
|
+
radius: Math.max(18, Math.round(width * 0.016)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function backdropImage(layout) {
|
|
62
|
+
return sharp(SHARED_BACKDROP_PATH)
|
|
63
|
+
.resize(layout.width, layout.height, { fit: 'cover', position: 'centre' })
|
|
64
|
+
.png()
|
|
65
|
+
.toBuffer();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function roundedRectSvg(width, height, radius, { fill = '#ffffff', stroke = null } = {}) {
|
|
69
|
+
const strokeAttribute = stroke
|
|
70
|
+
? ` fill="none" stroke="${stroke}" stroke-width="2"`
|
|
71
|
+
: ` fill="${fill}"`;
|
|
72
|
+
return Buffer.from(
|
|
73
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="${radius}" ry="${radius}"${strokeAttribute}/></svg>`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Composite a truthful browser capture into the deterministic Store frame.
|
|
79
|
+
* The source pixels are only resized/cropped; the app UI is never redrawn.
|
|
80
|
+
*/
|
|
81
|
+
export async function composeStoreScreenshot({
|
|
82
|
+
inputPath,
|
|
83
|
+
outputPath,
|
|
84
|
+
width = 2000,
|
|
85
|
+
height = 1250,
|
|
86
|
+
accent = null,
|
|
87
|
+
seed = 'notis-app',
|
|
88
|
+
focused = false,
|
|
89
|
+
theme = 'light',
|
|
90
|
+
}) {
|
|
91
|
+
const resolvedTheme = theme === 'dark' ? 'dark' : 'light';
|
|
92
|
+
const layout = storeScreenshotLayout(width, height);
|
|
93
|
+
const resolvedAccent = resolveStoreScreenshotAccent(accent, seed);
|
|
94
|
+
|
|
95
|
+
const roundedCapture = await sharp(inputPath)
|
|
96
|
+
.resize(layout.cardWidth, layout.cardHeight, {
|
|
97
|
+
fit: 'cover',
|
|
98
|
+
position: focused ? 'top' : 'centre',
|
|
99
|
+
})
|
|
100
|
+
.composite([{
|
|
101
|
+
input: roundedRectSvg(layout.cardWidth, layout.cardHeight, layout.radius),
|
|
102
|
+
blend: 'dest-in',
|
|
103
|
+
}])
|
|
104
|
+
.png()
|
|
105
|
+
.toBuffer();
|
|
106
|
+
|
|
107
|
+
const border = roundedRectSvg(
|
|
108
|
+
layout.cardWidth,
|
|
109
|
+
layout.cardHeight,
|
|
110
|
+
layout.radius,
|
|
111
|
+
{ stroke: resolvedTheme === 'dark' ? '#FFFFFF2A' : '#0F172A22' },
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await sharp(await backdropImage(layout))
|
|
115
|
+
.composite([
|
|
116
|
+
{ input: roundedCapture, left: layout.cardLeft, top: layout.cardTop },
|
|
117
|
+
{ input: border, left: layout.cardLeft, top: layout.cardTop },
|
|
118
|
+
])
|
|
119
|
+
.png({
|
|
120
|
+
compressionLevel: 9,
|
|
121
|
+
adaptiveFiltering: true,
|
|
122
|
+
})
|
|
123
|
+
.toFile(outputPath);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
mode: 'framed',
|
|
127
|
+
accent: resolvedAccent,
|
|
128
|
+
focused: Boolean(focused),
|
|
129
|
+
theme: resolvedTheme,
|
|
130
|
+
card: {
|
|
131
|
+
left: layout.cardLeft,
|
|
132
|
+
top: layout.cardTop,
|
|
133
|
+
width: layout.cardWidth,
|
|
134
|
+
height: layout.cardHeight,
|
|
135
|
+
radius: layout.radius,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { CliError, EXIT_CODES } from './errors.js';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_PROFILE,
|
|
6
|
+
getProfile,
|
|
7
|
+
isJwtExpired,
|
|
8
|
+
loadConfig,
|
|
9
|
+
} from './profiles.js';
|
|
10
|
+
import { createExpiredAuthError, createInvalidAuthHints } from './desktop-auth.js';
|
|
11
|
+
|
|
12
|
+
function escapeMultipartHeaderValue(value) {
|
|
13
|
+
return String(value ?? '')
|
|
14
|
+
.replace(/"/g, '%22')
|
|
15
|
+
.replace(/\r/g, '%0D')
|
|
16
|
+
.replace(/\n/g, '%0A');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function multipartTextPart(boundary, name, value) {
|
|
20
|
+
return [
|
|
21
|
+
`--${boundary}`,
|
|
22
|
+
`Content-Disposition: form-data; name="${escapeMultipartHeaderValue(name)}"`,
|
|
23
|
+
'',
|
|
24
|
+
value,
|
|
25
|
+
'',
|
|
26
|
+
].join('\r\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function multipartFileHeader(boundary, binding) {
|
|
30
|
+
const filename = escapeMultipartHeaderValue(binding.basename || binding.field_name);
|
|
31
|
+
return [
|
|
32
|
+
`--${boundary}`,
|
|
33
|
+
`Content-Disposition: form-data; name="${escapeMultipartHeaderValue(binding.field_name)}"; filename="${filename}"`,
|
|
34
|
+
`Content-Type: ${binding.contentType || 'application/octet-stream'}`,
|
|
35
|
+
'',
|
|
36
|
+
'',
|
|
37
|
+
].join('\r\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function* multipartFileBody(boundary, payload, bindingMetadata, fileBindings) {
|
|
41
|
+
yield Buffer.from(multipartTextPart(boundary, 'payload', JSON.stringify(payload)));
|
|
42
|
+
yield Buffer.from(multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)));
|
|
43
|
+
for (const binding of fileBindings) {
|
|
44
|
+
yield Buffer.from(multipartFileHeader(boundary, binding));
|
|
45
|
+
for await (const chunk of createReadStream(binding.localPath)) {
|
|
46
|
+
yield chunk;
|
|
47
|
+
}
|
|
48
|
+
yield Buffer.from('\r\n');
|
|
49
|
+
}
|
|
50
|
+
yield Buffer.from(`--${boundary}--\r\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createMultipartFileUpload(payload, bindingMetadata, fileBindings) {
|
|
54
|
+
const boundary = `----notis-cli-${randomUUID().replace(/-/g, '')}`;
|
|
55
|
+
const textParts = [
|
|
56
|
+
multipartTextPart(boundary, 'payload', JSON.stringify(payload)),
|
|
57
|
+
multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)),
|
|
58
|
+
];
|
|
59
|
+
const fileHeaderParts = fileBindings.map((binding) => multipartFileHeader(boundary, binding));
|
|
60
|
+
const closingPart = `--${boundary}--\r\n`;
|
|
61
|
+
const contentLength = [
|
|
62
|
+
...textParts,
|
|
63
|
+
...fileHeaderParts,
|
|
64
|
+
...fileBindings.map(() => '\r\n'),
|
|
65
|
+
closingPart,
|
|
66
|
+
].reduce((total, part) => total + Buffer.byteLength(part), 0)
|
|
67
|
+
+ fileBindings.reduce((total, binding) => total + binding.size, 0);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
body: multipartFileBody(boundary, payload, bindingMetadata, fileBindings),
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
73
|
+
'Content-Length': String(contentLength),
|
|
74
|
+
},
|
|
75
|
+
duplex: 'half',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeBackendError(status, payload, runtime) {
|
|
80
|
+
const backendError = payload?.error;
|
|
81
|
+
const message =
|
|
82
|
+
backendError?.message ||
|
|
83
|
+
backendError ||
|
|
84
|
+
payload?.message ||
|
|
85
|
+
payload?.error ||
|
|
86
|
+
`Request failed with status ${status}`;
|
|
87
|
+
|
|
88
|
+
if (status === 401) {
|
|
89
|
+
return new CliError({
|
|
90
|
+
code: 'auth_invalid',
|
|
91
|
+
message,
|
|
92
|
+
exitCode: EXIT_CODES.auth,
|
|
93
|
+
hints: [
|
|
94
|
+
...createInvalidAuthHints(runtime),
|
|
95
|
+
],
|
|
96
|
+
details: payload || {},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (status === 409) {
|
|
101
|
+
return new CliError({
|
|
102
|
+
code: 'conflict',
|
|
103
|
+
message,
|
|
104
|
+
exitCode: EXIT_CODES.conflict,
|
|
105
|
+
retryable: false,
|
|
106
|
+
hints: payload?.hints || [],
|
|
107
|
+
details: payload || {},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (status === 403) {
|
|
112
|
+
return new CliError({
|
|
113
|
+
code: 'forbidden',
|
|
114
|
+
message,
|
|
115
|
+
exitCode: EXIT_CODES.auth,
|
|
116
|
+
details: payload || {},
|
|
117
|
+
hints: [
|
|
118
|
+
{ command: 'notis tools toolkits', reason: 'Check which toolkits are available' },
|
|
119
|
+
{ command: 'notis whoami', reason: 'Verify your active profile and permissions' },
|
|
120
|
+
...(payload?.hints || []),
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (status >= 400 && status < 500) {
|
|
126
|
+
return new CliError({
|
|
127
|
+
code: 'usage_error',
|
|
128
|
+
message,
|
|
129
|
+
exitCode: EXIT_CODES.usage,
|
|
130
|
+
details: payload || {},
|
|
131
|
+
hints: payload?.hints || [],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return new CliError({
|
|
136
|
+
code: 'backend_error',
|
|
137
|
+
message,
|
|
138
|
+
exitCode: EXIT_CODES.backend,
|
|
139
|
+
retryable: status >= 500,
|
|
140
|
+
details: payload || {},
|
|
141
|
+
hints: payload?.hints || [],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function reloadJwtFromConfig(runtime) {
|
|
146
|
+
if (runtime.credentialSource === 'env') {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const profileName = runtime.profileName || DEFAULT_PROFILE;
|
|
150
|
+
let profile;
|
|
151
|
+
try {
|
|
152
|
+
profile = getProfile(loadConfig(), profileName);
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const nextJwt = typeof profile.jwt === 'string' && profile.jwt ? profile.jwt : null;
|
|
157
|
+
if (!nextJwt || nextJwt === runtime.jwt) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
runtime.jwt = nextJwt;
|
|
161
|
+
runtime.desktopAppName = profile.desktop_app_name;
|
|
162
|
+
runtime.desktopPid = profile.desktop_pid;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function httpRequest({
|
|
167
|
+
runtime,
|
|
168
|
+
method = 'POST',
|
|
169
|
+
path,
|
|
170
|
+
body,
|
|
171
|
+
multipart = false,
|
|
172
|
+
requireAuth = true,
|
|
173
|
+
}) {
|
|
174
|
+
// The desktop renderer is the single owner of the rotating Supabase refresh
|
|
175
|
+
// token. Each CLI request only consumes the newest access token it synced to
|
|
176
|
+
// disk, avoiding refresh-token races between independent CLI processes and
|
|
177
|
+
// the running desktop session.
|
|
178
|
+
reloadJwtFromConfig(runtime);
|
|
179
|
+
if (requireAuth && isJwtExpired(runtime.jwt)) {
|
|
180
|
+
throw createExpiredAuthError(runtime);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let controller = new AbortController();
|
|
184
|
+
let timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
|
|
185
|
+
const requestId = `req_${randomUUID().replace(/-/g, '')}`;
|
|
186
|
+
const resetTimeout = () => {
|
|
187
|
+
clearTimeout(timeout);
|
|
188
|
+
controller = new AbortController();
|
|
189
|
+
timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const headers = {
|
|
193
|
+
'X-Notis-CLI-Version': runtime.cliVersion,
|
|
194
|
+
'X-Notis-Request-Id': requestId,
|
|
195
|
+
};
|
|
196
|
+
if (!multipart) {
|
|
197
|
+
headers['Content-Type'] = 'application/json';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (requireAuth && runtime.jwt) {
|
|
201
|
+
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const resolveBody = () => {
|
|
205
|
+
if (!multipart) {
|
|
206
|
+
return {
|
|
207
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
208
|
+
headers: {},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (typeof body === 'function') {
|
|
212
|
+
return body();
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
body,
|
|
216
|
+
headers: {},
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
let requestBody = resolveBody();
|
|
222
|
+
let response = await fetch(`${runtime.apiBase}${path}`, {
|
|
223
|
+
method,
|
|
224
|
+
headers: { ...headers, ...requestBody.headers },
|
|
225
|
+
body: requestBody.body,
|
|
226
|
+
signal: controller.signal,
|
|
227
|
+
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
let payload = null;
|
|
231
|
+
try {
|
|
232
|
+
payload = await response.json();
|
|
233
|
+
} catch {
|
|
234
|
+
payload = null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (response.status === 401) {
|
|
238
|
+
const refreshed = reloadJwtFromConfig(runtime) && !isJwtExpired(runtime.jwt);
|
|
239
|
+
if (refreshed) {
|
|
240
|
+
if (requireAuth && runtime.jwt) {
|
|
241
|
+
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
242
|
+
}
|
|
243
|
+
resetTimeout();
|
|
244
|
+
requestBody = resolveBody();
|
|
245
|
+
response = await fetch(`${runtime.apiBase}${path}`, {
|
|
246
|
+
method,
|
|
247
|
+
headers: { ...headers, ...requestBody.headers },
|
|
248
|
+
body: requestBody.body,
|
|
249
|
+
signal: controller.signal,
|
|
250
|
+
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
251
|
+
});
|
|
252
|
+
try {
|
|
253
|
+
payload = await response.json();
|
|
254
|
+
} catch {
|
|
255
|
+
payload = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
clearTimeout(timeout);
|
|
261
|
+
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
if (response.status === 401 && isJwtExpired(runtime.jwt)) {
|
|
264
|
+
throw createExpiredAuthError(runtime);
|
|
265
|
+
}
|
|
266
|
+
throw normalizeBackendError(response.status, payload, runtime);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
requestId,
|
|
271
|
+
payload: payload || {},
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
clearTimeout(timeout);
|
|
275
|
+
|
|
276
|
+
if (error instanceof CliError) {
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (error?.name === 'AbortError') {
|
|
281
|
+
throw new CliError({
|
|
282
|
+
code: 'network_timeout',
|
|
283
|
+
message: `Request timed out after ${runtime.timeoutMs}ms`,
|
|
284
|
+
exitCode: EXIT_CODES.network,
|
|
285
|
+
retryable: true,
|
|
286
|
+
hints: [
|
|
287
|
+
{ command: 'notis doctor', reason: 'Check API reachability' },
|
|
288
|
+
{ command: `--timeout-ms ${runtime.timeoutMs * 2}`, reason: 'Retry with a longer timeout' },
|
|
289
|
+
],
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
throw new CliError({
|
|
294
|
+
code: 'network_error',
|
|
295
|
+
message: error instanceof Error ? error.message : String(error),
|
|
296
|
+
exitCode: EXIT_CODES.network,
|
|
297
|
+
retryable: true,
|
|
298
|
+
cause: error,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function callTool({
|
|
304
|
+
runtime,
|
|
305
|
+
toolName,
|
|
306
|
+
arguments_: argumentsPayload = {},
|
|
307
|
+
idempotencyKey,
|
|
308
|
+
fileBindings = [],
|
|
309
|
+
}) {
|
|
310
|
+
const requestId = idempotencyKey || (runtime.mutating ? randomUUID() : null);
|
|
311
|
+
const payload = {
|
|
312
|
+
tool_name: toolName,
|
|
313
|
+
arguments: argumentsPayload,
|
|
314
|
+
idempotency_key: requestId,
|
|
315
|
+
cli_context: {
|
|
316
|
+
output_mode: runtime.outputMode,
|
|
317
|
+
profile: runtime.profileName,
|
|
318
|
+
cwd: process.cwd(),
|
|
319
|
+
agent_mode: runtime.agentMode,
|
|
320
|
+
cli_version: runtime.cliVersion,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
if (Array.isArray(fileBindings) && fileBindings.length) {
|
|
325
|
+
const bindingMetadata = fileBindings.map((binding) => {
|
|
326
|
+
const {
|
|
327
|
+
contentType,
|
|
328
|
+
localPath,
|
|
329
|
+
...metadata
|
|
330
|
+
} = binding;
|
|
331
|
+
return metadata;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return httpRequest({
|
|
335
|
+
runtime,
|
|
336
|
+
path: '/cli_tools',
|
|
337
|
+
body: () => createMultipartFileUpload(payload, bindingMetadata, fileBindings),
|
|
338
|
+
multipart: true,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return httpRequest({
|
|
343
|
+
runtime,
|
|
344
|
+
path: '/cli_tools',
|
|
345
|
+
body: payload,
|
|
346
|
+
});
|
|
347
|
+
}
|