@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.
Files changed (191) hide show
  1. package/README.md +334 -0
  2. package/bin/notis.js +2 -0
  3. package/config/notis_app_boundary_rules.json +50 -0
  4. package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
  5. package/dist/scaffolds/notis-database/app/globals.css +44 -0
  6. package/dist/scaffolds/notis-database/app/layout.tsx +6 -0
  7. package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
  8. package/dist/scaffolds/notis-database/components/ui/badge.tsx +28 -0
  9. package/dist/scaffolds/notis-database/components/ui/button.tsx +53 -0
  10. package/dist/scaffolds/notis-database/components/ui/card.tsx +56 -0
  11. package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
  12. package/dist/scaffolds/notis-database/components.json +20 -0
  13. package/dist/scaffolds/notis-database/index.html +12 -0
  14. package/dist/scaffolds/notis-database/lib/types.ts +134 -0
  15. package/dist/scaffolds/notis-database/lib/utils.ts +6 -0
  16. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  17. package/dist/scaffolds/notis-database/notis.config.ts +26 -0
  18. package/dist/scaffolds/notis-database/package.json +31 -0
  19. package/dist/scaffolds/notis-database/postcss.config.mjs +8 -0
  20. package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
  21. package/dist/scaffolds/notis-database/src/mock-runtime.ts +558 -0
  22. package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
  23. package/dist/scaffolds/notis-database/tsconfig.json +23 -0
  24. package/dist/scaffolds/notis-database/vite.config.ts +22 -0
  25. package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
  26. package/dist/scaffolds/notis-journal/app/globals.css +37 -0
  27. package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
  28. package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
  29. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
  30. package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
  31. package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
  32. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
  33. package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
  34. package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
  35. package/dist/scaffolds/notis-journal/components.json +20 -0
  36. package/dist/scaffolds/notis-journal/index.html +12 -0
  37. package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
  38. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  39. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  40. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  41. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  42. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  43. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  44. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
  45. package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
  46. package/dist/scaffolds/notis-journal/package.json +34 -0
  47. package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
  48. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  49. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
  50. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  51. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  52. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  53. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
  54. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
  55. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
  56. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  57. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
  58. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
  59. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  60. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
  61. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  62. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
  63. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
  64. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  65. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  66. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
  67. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
  68. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
  69. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
  70. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
  71. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
  72. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
  73. package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
  74. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
  75. package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
  76. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
  77. package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
  78. package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
  79. package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
  80. package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
  81. package/dist/scaffolds/notis-notes/app/globals.css +3 -0
  82. package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
  83. package/dist/scaffolds/notis-notes/app/page.tsx +1950 -0
  84. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
  85. package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
  86. package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
  87. package/dist/scaffolds/notis-notes/components.json +20 -0
  88. package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
  89. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  90. package/dist/scaffolds/notis-notes/notis.config.ts +35 -0
  91. package/dist/scaffolds/notis-notes/package.json +31 -0
  92. package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
  93. package/dist/scaffolds/notis-notes/tailwind.config.ts +58 -0
  94. package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
  95. package/dist/scaffolds/notis-notes/vite.config.ts +10 -0
  96. package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
  97. package/dist/scaffolds/notis-random/README.md +33 -0
  98. package/dist/scaffolds/notis-random/app/globals.css +11 -0
  99. package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
  100. package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
  101. package/dist/scaffolds/notis-random/app/page.tsx +222 -0
  102. package/dist/scaffolds/notis-random/index.html +12 -0
  103. package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
  104. package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
  105. package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
  106. package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
  107. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  108. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  109. package/dist/scaffolds/notis-random/notis.config.ts +42 -0
  110. package/dist/scaffolds/notis-random/package.json +32 -0
  111. package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
  112. package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
  113. package/dist/scaffolds/notis-random/src/mock-runtime.ts +129 -0
  114. package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
  115. package/dist/scaffolds/notis-random/tsconfig.json +23 -0
  116. package/dist/scaffolds/notis-random/vite.config.ts +11 -0
  117. package/dist/scaffolds.json +47 -0
  118. package/package.json +45 -0
  119. package/skills/notis-apps/SKILL.md +219 -0
  120. package/skills/notis-apps/cli.md +244 -0
  121. package/skills/notis-cli/SKILL.md +269 -0
  122. package/skills/notis-query/cli.md +39 -0
  123. package/src/cli.js +175 -0
  124. package/src/command-specs/apps.js +1939 -0
  125. package/src/command-specs/helpers.js +186 -0
  126. package/src/command-specs/index.js +14 -0
  127. package/src/command-specs/meta.js +161 -0
  128. package/src/command-specs/tools.js +499 -0
  129. package/src/runtime/agent-browser.js +464 -0
  130. package/src/runtime/app-boundary-validator.js +183 -0
  131. package/src/runtime/app-changelog.js +79 -0
  132. package/src/runtime/app-dev-server.js +712 -0
  133. package/src/runtime/app-dev-sessions.js +251 -0
  134. package/src/runtime/app-platform.js +1500 -0
  135. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  136. package/src/runtime/cli-mode.generated.js +4 -0
  137. package/src/runtime/cli-mode.js +29 -0
  138. package/src/runtime/desktop-auth.js +93 -0
  139. package/src/runtime/errors.js +55 -0
  140. package/src/runtime/help.js +60 -0
  141. package/src/runtime/output.js +180 -0
  142. package/src/runtime/ports.js +15 -0
  143. package/src/runtime/profiles.js +261 -0
  144. package/src/runtime/store-screenshot.js +138 -0
  145. package/src/runtime/transport.js +347 -0
  146. package/template/.harness/index.html.tmpl +333 -0
  147. package/template/CHANGELOG.md +5 -0
  148. package/template/app/globals.css +3 -0
  149. package/template/app/layout.tsx +6 -0
  150. package/template/app/page.tsx +60 -0
  151. package/template/components/ui/badge.tsx +28 -0
  152. package/template/components/ui/button.tsx +53 -0
  153. package/template/components/ui/card.tsx +56 -0
  154. package/template/components.json +20 -0
  155. package/template/lib/utils.ts +6 -0
  156. package/template/metadata/screenshot-1.png +0 -0
  157. package/template/metadata/screenshot-2.png +0 -0
  158. package/template/metadata/screenshot-3.png +0 -0
  159. package/template/notis.config.ts +36 -0
  160. package/template/package-lock.json +4137 -0
  161. package/template/package.json +32 -0
  162. package/template/packages/sdk/package.json +36 -0
  163. package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  164. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  165. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  166. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  167. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  168. package/template/packages/sdk/src/config.ts +145 -0
  169. package/template/packages/sdk/src/documents.ts +229 -0
  170. package/template/packages/sdk/src/hooks/useBackend.ts +41 -0
  171. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  172. package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
  173. package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
  174. package/template/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  175. package/template/packages/sdk/src/hooks/useNotis.ts +34 -0
  176. package/template/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  177. package/template/packages/sdk/src/hooks/useTool.ts +64 -0
  178. package/template/packages/sdk/src/hooks/useTools.ts +56 -0
  179. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  180. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  181. package/template/packages/sdk/src/index.ts +83 -0
  182. package/template/packages/sdk/src/provider.tsx +43 -0
  183. package/template/packages/sdk/src/runtime.ts +220 -0
  184. package/template/packages/sdk/src/styles.css +186 -0
  185. package/template/packages/sdk/src/ui.ts +15 -0
  186. package/template/packages/sdk/src/vite.ts +56 -0
  187. package/template/packages/sdk/tsconfig.json +15 -0
  188. package/template/postcss.config.mjs +8 -0
  189. package/template/tailwind.config.ts +58 -0
  190. package/template/tsconfig.json +22 -0
  191. 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
+ }