@notis_ai/cli 0.2.0-beta.16.1 → 0.2.0-beta.160.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 +433 -133
- package/config/notis_app_boundary_rules.json +50 -0
- package/config/notis_app_design_rules.json +135 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +18672 -0
- package/dist/base-skills/notis-apps/SKILL.md +70 -0
- package/dist/base-skills/notis-apps/references/architecture.md +164 -0
- package/dist/base-skills/notis-apps/references/context.md +81 -0
- package/dist/base-skills/notis-apps/references/design.md +165 -0
- package/dist/base-skills/notis-apps/references/reading.md +89 -0
- package/dist/base-skills/notis-apps/references/release.md +99 -0
- package/dist/base-skills/notis-apps/references/sdk.md +62 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
- package/dist/base-skills/notis-cli/SKILL.md +140 -0
- package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
- package/dist/base-skills/notis-query/SKILL.md +67 -0
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
- package/dist/base-skills/notis-query/references/documents.md +50 -0
- package/dist/base-skills/notis-query/references/query.md +543 -0
- package/dist/skill-sync/index.js +1626 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/dist/skill-sync-worker.mjs +2990 -0
- package/package.json +16 -6
- package/skills/notis-apps/cli.md +313 -0
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +129 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/agent-hook-entry.js +5 -0
- package/src/cli.js +294 -25
- package/src/command-specs/agents.js +392 -0
- package/src/command-specs/apps.js +1470 -202
- package/src/command-specs/auth.js +114 -137
- package/src/command-specs/diagnostics.js +716 -0
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/helpers.js +84 -82
- package/src/command-specs/index.js +25 -6
- package/src/command-specs/meta.js +150 -18
- package/src/command-specs/onboarding.js +290 -0
- package/src/command-specs/profile.js +358 -0
- package/src/command-specs/reports.js +86 -0
- package/src/command-specs/skills.js +75 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +455 -139
- package/src/runtime/agent-browser.js +632 -0
- package/src/runtime/agent-memory-state.js +126 -0
- package/src/runtime/agent-setup.js +383 -0
- package/src/runtime/app-boundary-validator.js +404 -0
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-platform.js +2633 -210
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/app-test-server.js +292 -0
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/auth-recovery.js +110 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/channel.js +133 -0
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/errors.js +1 -0
- package/src/runtime/git.js +233 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/oauth.js +2622 -0
- package/src/runtime/output.js +37 -5
- package/src/runtime/ports.js +31 -0
- package/src/runtime/profiles.js +906 -55
- package/src/runtime/skill-sync/cloud-client.ts +99 -0
- package/src/runtime/skill-sync/index.ts +697 -0
- package/src/runtime/skill-sync/local-scanner.ts +1046 -0
- package/src/runtime/skill-sync/symlink-manager.ts +433 -0
- package/src/runtime/skill-sync/sync-plan.ts +22 -0
- package/src/runtime/skill-sync/types.ts +110 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/store-screenshot.js +143 -0
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +231 -0
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +324 -45
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +50 -0
- package/template/.harness/index.html.tmpl +430 -0
- package/template/CHANGELOG.md +5 -0
- package/template/app/layout.tsx +5 -2
- package/template/app/page.tsx +49 -42
- package/template/components/page-heading.tsx +23 -0
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/card.tsx +24 -11
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +24 -6
- package/template/package-lock.json +4137 -0
- package/template/package.json +5 -5
- package/template/packages/{notis-sdk → sdk}/package.json +13 -3
- package/template/packages/sdk/src/agentContext.ts +36 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +103 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +285 -0
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
- package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
- package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
- package/template/packages/sdk/src/config.ts +257 -0
- package/template/packages/sdk/src/documents.ts +256 -0
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +64 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +836 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +49 -0
- package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +43 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +84 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
- package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +95 -0
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +10 -4
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +11 -8
- package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
- package/template/packages/sdk/src/hooks/useTool.ts +65 -0
- package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +81 -0
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/template/packages/sdk/src/index.ts +161 -0
- package/template/packages/sdk/src/interactions/actions.ts +59 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +694 -0
- package/template/packages/sdk/src/interactions/visibility.ts +13 -0
- package/template/packages/sdk/src/interactions.ts +45 -0
- package/template/packages/sdk/src/provider.tsx +44 -0
- package/template/packages/sdk/src/queryCache.ts +170 -0
- package/template/packages/sdk/src/runtime.ts +451 -0
- package/template/packages/sdk/src/styles.css +213 -0
- package/template/packages/sdk/src/tailwind.ts +56 -0
- package/template/packages/{notis-sdk → sdk}/src/vite.ts +5 -1
- package/template/tailwind.config.ts +1 -0
- package/src/command-specs/db.js +0 -163
- package/src/runtime/app-preview-server.js +0 -312
- package/template/packages/notis-sdk/src/config.ts +0 -48
- package/template/packages/notis-sdk/src/helpers.ts +0 -131
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
- package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
- package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
- package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
- package/template/packages/notis-sdk/src/index.ts +0 -47
- package/template/packages/notis-sdk/src/provider.tsx +0 -44
- package/template/packages/notis-sdk/src/runtime.ts +0 -159
- package/template/packages/notis-sdk/src/styles.css +0 -123
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
package/src/runtime/transport.js
CHANGED
|
@@ -1,34 +1,88 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
2
4
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
5
|
+
import { delegatedContextReason } from './delegated-context.js';
|
|
6
|
+
import {
|
|
7
|
+
credentialIsExpired,
|
|
8
|
+
getJwtExpiration,
|
|
9
|
+
getJwtSubject,
|
|
10
|
+
getProfile,
|
|
11
|
+
isJwtExpired,
|
|
12
|
+
loadConfig,
|
|
13
|
+
resolveWorktreeRuntime,
|
|
14
|
+
} from './profiles.js';
|
|
15
|
+
import { createExpiredAuthError, createInvalidAuthHints } from './auth-recovery.js';
|
|
16
|
+
import { refreshOAuthCredential } from './oauth.js';
|
|
3
17
|
|
|
4
|
-
|
|
5
|
-
'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
18
|
+
function escapeMultipartHeaderValue(value) {
|
|
19
|
+
return String(value ?? '')
|
|
20
|
+
.replace(/"/g, '%22')
|
|
21
|
+
.replace(/\r/g, '%0D')
|
|
22
|
+
.replace(/\n/g, '%0A');
|
|
23
|
+
}
|
|
10
24
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
function multipartTextPart(boundary, name, value) {
|
|
26
|
+
return [
|
|
27
|
+
`--${boundary}`,
|
|
28
|
+
`Content-Disposition: form-data; name="${escapeMultipartHeaderValue(name)}"`,
|
|
29
|
+
'',
|
|
30
|
+
value,
|
|
31
|
+
'',
|
|
32
|
+
].join('\r\n');
|
|
33
|
+
}
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
function multipartFileHeader(boundary, binding) {
|
|
36
|
+
const filename = escapeMultipartHeaderValue(binding.basename || binding.field_name);
|
|
37
|
+
return [
|
|
38
|
+
`--${boundary}`,
|
|
39
|
+
`Content-Disposition: form-data; name="${escapeMultipartHeaderValue(binding.field_name)}"; filename="${filename}"`,
|
|
40
|
+
`Content-Type: ${binding.contentType || 'application/octet-stream'}`,
|
|
41
|
+
'',
|
|
42
|
+
'',
|
|
43
|
+
].join('\r\n');
|
|
44
|
+
}
|
|
19
45
|
|
|
20
|
-
|
|
21
|
-
|
|
46
|
+
async function* multipartFileBody(boundary, payload, bindingMetadata, fileBindings) {
|
|
47
|
+
yield Buffer.from(multipartTextPart(boundary, 'payload', JSON.stringify(payload)));
|
|
48
|
+
yield Buffer.from(multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)));
|
|
49
|
+
for (const binding of fileBindings) {
|
|
50
|
+
yield Buffer.from(multipartFileHeader(boundary, binding));
|
|
51
|
+
for await (const chunk of createReadStream(binding.localPath)) {
|
|
52
|
+
yield chunk;
|
|
53
|
+
}
|
|
54
|
+
yield Buffer.from('\r\n');
|
|
22
55
|
}
|
|
56
|
+
yield Buffer.from(`--${boundary}--\r\n`);
|
|
57
|
+
}
|
|
23
58
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
59
|
+
function createMultipartFileUpload(payload, bindingMetadata, fileBindings) {
|
|
60
|
+
const boundary = `----notis-cli-${randomUUID().replace(/-/g, '')}`;
|
|
61
|
+
const textParts = [
|
|
62
|
+
multipartTextPart(boundary, 'payload', JSON.stringify(payload)),
|
|
63
|
+
multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)),
|
|
64
|
+
];
|
|
65
|
+
const fileHeaderParts = fileBindings.map((binding) => multipartFileHeader(boundary, binding));
|
|
66
|
+
const closingPart = `--${boundary}--\r\n`;
|
|
67
|
+
const contentLength = [
|
|
68
|
+
...textParts,
|
|
69
|
+
...fileHeaderParts,
|
|
70
|
+
...fileBindings.map(() => '\r\n'),
|
|
71
|
+
closingPart,
|
|
72
|
+
].reduce((total, part) => total + Buffer.byteLength(part), 0)
|
|
73
|
+
+ fileBindings.reduce((total, binding) => total + binding.size, 0);
|
|
27
74
|
|
|
28
|
-
return
|
|
75
|
+
return {
|
|
76
|
+
body: multipartFileBody(boundary, payload, bindingMetadata, fileBindings),
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
79
|
+
'Content-Length': String(contentLength),
|
|
80
|
+
},
|
|
81
|
+
duplex: 'half',
|
|
82
|
+
};
|
|
29
83
|
}
|
|
30
84
|
|
|
31
|
-
function normalizeBackendError(status, payload) {
|
|
85
|
+
function normalizeBackendError(status, payload, runtime) {
|
|
32
86
|
const backendError = payload?.error;
|
|
33
87
|
const message =
|
|
34
88
|
backendError?.message ||
|
|
@@ -43,10 +97,7 @@ function normalizeBackendError(status, payload) {
|
|
|
43
97
|
message,
|
|
44
98
|
exitCode: EXIT_CODES.auth,
|
|
45
99
|
hints: [
|
|
46
|
-
|
|
47
|
-
command: 'notis auth login --jwt <token>',
|
|
48
|
-
reason: 'Refresh the stored JWT for this profile',
|
|
49
|
-
},
|
|
100
|
+
...createInvalidAuthHints(runtime),
|
|
50
101
|
],
|
|
51
102
|
details: payload || {},
|
|
52
103
|
});
|
|
@@ -77,6 +128,18 @@ function normalizeBackendError(status, payload) {
|
|
|
77
128
|
});
|
|
78
129
|
}
|
|
79
130
|
|
|
131
|
+
if (status === 402) {
|
|
132
|
+
// Distinct from auth on purpose: an agent that sees an auth exit code will
|
|
133
|
+
// loop trying to re-authenticate, when the actual fix is to add credit.
|
|
134
|
+
return new CliError({
|
|
135
|
+
code: 'payment_required',
|
|
136
|
+
message,
|
|
137
|
+
exitCode: EXIT_CODES.payment,
|
|
138
|
+
details: payload || {},
|
|
139
|
+
hints: payload?.hints || [],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
80
143
|
if (status >= 400 && status < 500) {
|
|
81
144
|
return new CliError({
|
|
82
145
|
code: 'usage_error',
|
|
@@ -97,45 +160,233 @@ function normalizeBackendError(status, payload) {
|
|
|
97
160
|
});
|
|
98
161
|
}
|
|
99
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Pick up a dev credential that `./dev.sh` re-minted mid-process.
|
|
165
|
+
*
|
|
166
|
+
* Restarting the local runtime rewrites the worktree lease in place, so a
|
|
167
|
+
* long-running command can recover from a single 401 instead of failing and
|
|
168
|
+
* making the caller rerun it. The refreshed token still has to belong to the
|
|
169
|
+
* worktree's approved test user.
|
|
170
|
+
*/
|
|
171
|
+
function reloadDevJwt(runtime, loadedProfile = null) {
|
|
172
|
+
if (runtime.credentialKind !== 'worktree') {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
let nextJwt = null;
|
|
176
|
+
const expectedUserId = runtime.worktreeRuntime?.expected_user_id;
|
|
177
|
+
const runtimePath = runtime.worktreeRuntime?.runtime_path;
|
|
178
|
+
if (runtimePath) {
|
|
179
|
+
const refreshedRuntime = resolveWorktreeRuntime(dirname(runtimePath));
|
|
180
|
+
if (
|
|
181
|
+
refreshedRuntime
|
|
182
|
+
&& !refreshedRuntime.unavailable
|
|
183
|
+
&& refreshedRuntime.runtime_path === runtimePath
|
|
184
|
+
&& refreshedRuntime.profile === runtime.profileName
|
|
185
|
+
&& refreshedRuntime.api_base === runtime.apiBase
|
|
186
|
+
) {
|
|
187
|
+
if (
|
|
188
|
+
expectedUserId
|
|
189
|
+
&& refreshedRuntime.expected_user_id !== expectedUserId
|
|
190
|
+
) {
|
|
191
|
+
throw new CliError({
|
|
192
|
+
code: 'dev_runtime_identity_mismatch',
|
|
193
|
+
message: 'The restarted worktree runtime belongs to a different test user',
|
|
194
|
+
exitCode: EXIT_CODES.auth,
|
|
195
|
+
hints: [
|
|
196
|
+
{ message: 'Restart this CLI command under the worktree\'s current dev identity.' },
|
|
197
|
+
{ message: `Expected user: ${expectedUserId}` },
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
runtime.worktreeRuntime = refreshedRuntime;
|
|
202
|
+
nextJwt = refreshedRuntime.dev_access_token;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
let profile = loadedProfile;
|
|
206
|
+
if (!nextJwt && !profile) {
|
|
207
|
+
try {
|
|
208
|
+
profile = getProfile(loadConfig(), runtime.profileName);
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
nextJwt = nextJwt || (
|
|
214
|
+
typeof profile?.dev_access_token === 'string' && profile.dev_access_token
|
|
215
|
+
? profile.dev_access_token
|
|
216
|
+
: null
|
|
217
|
+
);
|
|
218
|
+
if (!nextJwt || nextJwt === runtime.jwt) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
if (expectedUserId && getJwtSubject(nextJwt) !== expectedUserId) {
|
|
222
|
+
throw new CliError({
|
|
223
|
+
code: 'dev_runtime_identity_mismatch',
|
|
224
|
+
message: 'The refreshed dev credential does not belong to this worktree test user',
|
|
225
|
+
exitCode: EXIT_CODES.auth,
|
|
226
|
+
hints: [
|
|
227
|
+
{ message: 'Restart ./dev.sh to restore the approved worktree identity.' },
|
|
228
|
+
{ message: `Expected user: ${expectedUserId}` },
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
runtime.jwt = nextJwt;
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A live worktree lease is authoritative over any stale dev metadata left in
|
|
238
|
+
* the shared profile store. Prefer its explicit expiry, then the active JWT's
|
|
239
|
+
* expiry, and deliberately fail closed when neither can be verified.
|
|
240
|
+
*/
|
|
241
|
+
export function getActiveWorktreeCredentialProfile(runtime, loadedProfile = {}) {
|
|
242
|
+
if (runtime.credentialKind !== 'worktree') {
|
|
243
|
+
return loadedProfile;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
...loadedProfile,
|
|
247
|
+
dev_access_expires_at:
|
|
248
|
+
runtime.worktreeRuntime?.dev_access_expires_at
|
|
249
|
+
?? getJwtExpiration(runtime.jwt)
|
|
250
|
+
?? loadedProfile.dev_access_expires_at,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
100
254
|
export async function httpRequest({
|
|
101
255
|
runtime,
|
|
102
256
|
method = 'POST',
|
|
103
257
|
path,
|
|
104
258
|
body,
|
|
259
|
+
multipart = false,
|
|
105
260
|
requireAuth = true,
|
|
106
261
|
}) {
|
|
107
|
-
|
|
108
|
-
|
|
262
|
+
// Refresh before spending the credential rather than after a rejection: the
|
|
263
|
+
// rotating refresh token is shared with every other `notis` process reading
|
|
264
|
+
// the same profile, so a lapsed access token is renewed once, under the
|
|
265
|
+
// config write lock, instead of racing on a 401 retry.
|
|
266
|
+
let currentProfile;
|
|
267
|
+
if (runtime.credentialKind === 'oauth') {
|
|
268
|
+
if (requireAuth && credentialIsExpired(runtime, {
|
|
269
|
+
oauth_access_expires_at: runtime.oauthAccessExpiresAt,
|
|
270
|
+
})) {
|
|
271
|
+
await refreshOAuthCredential(runtime);
|
|
272
|
+
}
|
|
273
|
+
currentProfile = getProfile(loadConfig(), runtime.profileName);
|
|
274
|
+
} else {
|
|
275
|
+
currentProfile = getProfile(loadConfig(), runtime.profileName);
|
|
276
|
+
reloadDevJwt(runtime, currentProfile);
|
|
277
|
+
currentProfile = getActiveWorktreeCredentialProfile(runtime, currentProfile);
|
|
278
|
+
}
|
|
279
|
+
if (requireAuth && credentialIsExpired(runtime, currentProfile)) {
|
|
280
|
+
throw createExpiredAuthError(runtime);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let controller = new AbortController();
|
|
284
|
+
let timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
|
|
109
285
|
const requestId = `req_${randomUUID().replace(/-/g, '')}`;
|
|
286
|
+
const resetTimeout = () => {
|
|
287
|
+
clearTimeout(timeout);
|
|
288
|
+
controller = new AbortController();
|
|
289
|
+
timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
|
|
290
|
+
};
|
|
110
291
|
|
|
111
292
|
const headers = {
|
|
112
|
-
'Content-Type': 'application/json',
|
|
113
293
|
'X-Notis-CLI-Version': runtime.cliVersion,
|
|
114
294
|
'X-Notis-Request-Id': requestId,
|
|
115
295
|
};
|
|
296
|
+
if (!multipart) {
|
|
297
|
+
headers['Content-Type'] = 'application/json';
|
|
298
|
+
}
|
|
116
299
|
|
|
117
300
|
if (requireAuth && runtime.jwt) {
|
|
118
301
|
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
119
302
|
}
|
|
120
303
|
|
|
304
|
+
const resolveBody = () => {
|
|
305
|
+
if (!multipart) {
|
|
306
|
+
return {
|
|
307
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
308
|
+
headers: {},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (typeof body === 'function') {
|
|
312
|
+
return body();
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
body,
|
|
316
|
+
headers: {},
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
|
|
121
320
|
try {
|
|
122
|
-
|
|
321
|
+
let requestBody = resolveBody();
|
|
322
|
+
let response = await fetch(`${runtime.apiBase}${path}`, {
|
|
123
323
|
method,
|
|
124
|
-
headers,
|
|
125
|
-
body:
|
|
324
|
+
headers: { ...headers, ...requestBody.headers },
|
|
325
|
+
body: requestBody.body,
|
|
126
326
|
signal: controller.signal,
|
|
327
|
+
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
127
328
|
});
|
|
128
|
-
clearTimeout(timeout);
|
|
129
329
|
|
|
130
330
|
let payload = null;
|
|
331
|
+
let payloadReadError = null;
|
|
131
332
|
try {
|
|
132
333
|
payload = await response.json();
|
|
133
|
-
} catch {
|
|
334
|
+
} catch (error) {
|
|
335
|
+
payloadReadError = error;
|
|
134
336
|
payload = null;
|
|
135
337
|
}
|
|
136
338
|
|
|
339
|
+
if (response.status === 401) {
|
|
340
|
+
const refreshed = runtime.credentialKind === 'oauth'
|
|
341
|
+
? await refreshOAuthCredential(runtime)
|
|
342
|
+
: reloadDevJwt(runtime) && !isJwtExpired(runtime.jwt);
|
|
343
|
+
if (refreshed) {
|
|
344
|
+
if (requireAuth && runtime.jwt) {
|
|
345
|
+
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
346
|
+
}
|
|
347
|
+
resetTimeout();
|
|
348
|
+
requestBody = resolveBody();
|
|
349
|
+
response = await fetch(`${runtime.apiBase}${path}`, {
|
|
350
|
+
method,
|
|
351
|
+
headers: { ...headers, ...requestBody.headers },
|
|
352
|
+
body: requestBody.body,
|
|
353
|
+
signal: controller.signal,
|
|
354
|
+
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
355
|
+
});
|
|
356
|
+
payloadReadError = null;
|
|
357
|
+
try {
|
|
358
|
+
payload = await response.json();
|
|
359
|
+
} catch (error) {
|
|
360
|
+
payloadReadError = error;
|
|
361
|
+
payload = null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
clearTimeout(timeout);
|
|
367
|
+
|
|
137
368
|
if (!response.ok) {
|
|
138
|
-
|
|
369
|
+
if (
|
|
370
|
+
response.status === 401
|
|
371
|
+
&& credentialIsExpired(
|
|
372
|
+
runtime,
|
|
373
|
+
getActiveWorktreeCredentialProfile(
|
|
374
|
+
runtime,
|
|
375
|
+
getProfile(loadConfig(), runtime.profileName),
|
|
376
|
+
),
|
|
377
|
+
)
|
|
378
|
+
) {
|
|
379
|
+
throw createExpiredAuthError(runtime);
|
|
380
|
+
}
|
|
381
|
+
throw normalizeBackendError(response.status, payload, runtime);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// A successful status is not a successful tool call until its response
|
|
385
|
+
// body has been read. Mutations may already have committed before a socket
|
|
386
|
+
// reset truncates the JSON; surface that as an ambiguous network failure
|
|
387
|
+
// so callers never persist an undefined result or attempt a second write.
|
|
388
|
+
if (response.status !== 204 && payloadReadError) {
|
|
389
|
+
throw payloadReadError;
|
|
139
390
|
}
|
|
140
391
|
|
|
141
392
|
return {
|
|
@@ -177,22 +428,50 @@ export async function callTool({
|
|
|
177
428
|
toolName,
|
|
178
429
|
arguments_: argumentsPayload = {},
|
|
179
430
|
idempotencyKey,
|
|
431
|
+
fileBindings = [],
|
|
180
432
|
}) {
|
|
181
433
|
const requestId = idempotencyKey || (runtime.mutating ? randomUUID() : null);
|
|
434
|
+
const payload = {
|
|
435
|
+
tool_name: toolName,
|
|
436
|
+
arguments: argumentsPayload,
|
|
437
|
+
idempotency_key: requestId,
|
|
438
|
+
cli_context: {
|
|
439
|
+
output_mode: runtime.outputMode,
|
|
440
|
+
profile: runtime.profileName,
|
|
441
|
+
cwd: process.cwd(),
|
|
442
|
+
agent_mode: runtime.agentMode,
|
|
443
|
+
cli_version: runtime.cliVersion,
|
|
444
|
+
// Reported so the server can refuse work-handing tools from a run Notis
|
|
445
|
+
// is itself driving. Advisory -- a determined agent can unset the markers
|
|
446
|
+
// -- so the server treats it as one layer, not the whole guard.
|
|
447
|
+
...(delegatedContextReason() ? { delegated_context: true } : {}),
|
|
448
|
+
...(runtime.debugEntitlementOverride
|
|
449
|
+
? { debug_entitlement_override: runtime.debugEntitlementOverride }
|
|
450
|
+
: {}),
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
if (Array.isArray(fileBindings) && fileBindings.length) {
|
|
455
|
+
const bindingMetadata = fileBindings.map((binding) => {
|
|
456
|
+
const {
|
|
457
|
+
contentType,
|
|
458
|
+
localPath,
|
|
459
|
+
...metadata
|
|
460
|
+
} = binding;
|
|
461
|
+
return metadata;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return httpRequest({
|
|
465
|
+
runtime,
|
|
466
|
+
path: '/cli_tools',
|
|
467
|
+
body: () => createMultipartFileUpload(payload, bindingMetadata, fileBindings),
|
|
468
|
+
multipart: true,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
182
472
|
return httpRequest({
|
|
183
473
|
runtime,
|
|
184
474
|
path: '/cli_tools',
|
|
185
|
-
body:
|
|
186
|
-
tool_name: canonicalizeToolName(toolName),
|
|
187
|
-
arguments: argumentsPayload,
|
|
188
|
-
idempotency_key: requestId,
|
|
189
|
-
cli_context: {
|
|
190
|
-
output_mode: runtime.outputMode,
|
|
191
|
-
profile: runtime.profileName,
|
|
192
|
-
cwd: process.cwd(),
|
|
193
|
-
agent_mode: runtime.agentMode,
|
|
194
|
-
cli_version: runtime.cliVersion,
|
|
195
|
-
},
|
|
196
|
-
},
|
|
475
|
+
body: payload,
|
|
197
476
|
});
|
|
198
477
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { resolveRuntimeProfile } from './runtime/profiles.js';
|
|
5
|
+
import { ensureFreshOAuthCredential } from './runtime/oauth.js';
|
|
6
|
+
import { runSkillSync, fetchSyncSettings } from '../dist/skill-sync/index.js';
|
|
7
|
+
import { withSkillSyncLock } from './runtime/sync-skills.js';
|
|
8
|
+
|
|
9
|
+
export async function runAutomaticSkillSync({ profile, apiBase, userId }, {
|
|
10
|
+
resolveRuntime = resolveRuntimeProfile, refresh = ensureFreshOAuthCredential,
|
|
11
|
+
settings = fetchSyncSettings, sync = runSkillSync, lock = withSkillSyncLock,
|
|
12
|
+
} = {}) {
|
|
13
|
+
const runtime = resolveRuntime({ profile }, { requireAuth: true });
|
|
14
|
+
if (runtime.credentialKind !== 'oauth' || runtime.apiBase !== apiBase
|
|
15
|
+
|| runtime.oauthUserId !== userId) throw new Error('Automatic skill sync account changed; run notis skills sync to rebind');
|
|
16
|
+
await refresh(runtime);
|
|
17
|
+
const saved = await settings(runtime.apiBase, runtime.jwt);
|
|
18
|
+
if (saved.user_id !== userId) throw new Error('Automatic skill sync identity mismatch');
|
|
19
|
+
// Do not gather, unlink foreign accounts or write anything when opted out.
|
|
20
|
+
if (!saved.sync_enabled) return { status: 'disabled' };
|
|
21
|
+
const result = await lock(() => sync(runtime.apiBase, runtime.jwt, {
|
|
22
|
+
fetchSyncSettings: async () => saved,
|
|
23
|
+
}, { honorSyncEnabled: true }));
|
|
24
|
+
return { status: (result.failedLinks?.length || result.failedPushes?.length) ? 'partial' : 'synced', ...result };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function main(args = process.argv.slice(2)) {
|
|
28
|
+
const [profile, apiBase, userId] = args;
|
|
29
|
+
const root = join(homedir(), '.notis', 'skills', 'service');
|
|
30
|
+
const record = (value) => {
|
|
31
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
32
|
+
const target = join(root, 'status.json');
|
|
33
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
34
|
+
writeFileSync(temporary, JSON.stringify({ ...value, profile, at: new Date().toISOString() }, null, 2), { mode: 0o600 });
|
|
35
|
+
renameSync(temporary, target);
|
|
36
|
+
};
|
|
37
|
+
const deadline = setTimeout(() => {
|
|
38
|
+
record({ status: 'error', code: 'sync_timeout' });
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}, 240_000);
|
|
41
|
+
try {
|
|
42
|
+
record(await runAutomaticSkillSync({ profile, apiBase, userId }));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Persist only a classified error, never a bearer, signed URL or response body.
|
|
45
|
+
record({ status: 'error', code: error.code || 'sync_failed' });
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(deadline);
|
|
49
|
+
}
|
|
50
|
+
}
|