@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.
Files changed (154) hide show
  1. package/README.md +433 -133
  2. package/config/notis_app_boundary_rules.json +50 -0
  3. package/config/notis_app_design_rules.json +135 -0
  4. package/dist/agent-hooks/notis-agent-hook.mjs +18672 -0
  5. package/dist/base-skills/notis-apps/SKILL.md +70 -0
  6. package/dist/base-skills/notis-apps/references/architecture.md +164 -0
  7. package/dist/base-skills/notis-apps/references/context.md +81 -0
  8. package/dist/base-skills/notis-apps/references/design.md +165 -0
  9. package/dist/base-skills/notis-apps/references/reading.md +89 -0
  10. package/dist/base-skills/notis-apps/references/release.md +99 -0
  11. package/dist/base-skills/notis-apps/references/sdk.md +62 -0
  12. package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +140 -0
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
  15. package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
  16. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  17. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  18. package/dist/base-skills/notis-query/SKILL.md +67 -0
  19. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  20. package/dist/base-skills/notis-query/references/documents.md +50 -0
  21. package/dist/base-skills/notis-query/references/query.md +543 -0
  22. package/dist/skill-sync/index.js +1626 -0
  23. package/dist/skill-sync/index.js.map +7 -0
  24. package/dist/skill-sync-worker.mjs +2990 -0
  25. package/package.json +16 -6
  26. package/skills/notis-apps/cli.md +313 -0
  27. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  28. package/skills/notis-onboarding/BRIEF.md +129 -0
  29. package/skills/notis-query/cli.md +39 -0
  30. package/src/agent-hook-entry.js +5 -0
  31. package/src/cli.js +294 -25
  32. package/src/command-specs/agents.js +392 -0
  33. package/src/command-specs/apps.js +1470 -202
  34. package/src/command-specs/auth.js +114 -137
  35. package/src/command-specs/diagnostics.js +716 -0
  36. package/src/command-specs/handover.js +374 -0
  37. package/src/command-specs/helpers.js +84 -82
  38. package/src/command-specs/index.js +25 -6
  39. package/src/command-specs/meta.js +150 -18
  40. package/src/command-specs/onboarding.js +290 -0
  41. package/src/command-specs/profile.js +358 -0
  42. package/src/command-specs/reports.js +86 -0
  43. package/src/command-specs/skills.js +75 -0
  44. package/src/command-specs/smoke.js +386 -0
  45. package/src/command-specs/tools.js +455 -139
  46. package/src/runtime/agent-browser.js +632 -0
  47. package/src/runtime/agent-memory-state.js +126 -0
  48. package/src/runtime/agent-setup.js +383 -0
  49. package/src/runtime/app-boundary-validator.js +404 -0
  50. package/src/runtime/app-changelog.js +79 -0
  51. package/src/runtime/app-platform.js +2633 -210
  52. package/src/runtime/app-registry-scaffolds.js +367 -0
  53. package/src/runtime/app-test-server.js +292 -0
  54. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  55. package/src/runtime/auth-recovery.js +110 -0
  56. package/src/runtime/base-skills.d.ts +20 -0
  57. package/src/runtime/base-skills.js +167 -0
  58. package/src/runtime/channel.js +133 -0
  59. package/src/runtime/delegated-context.js +68 -0
  60. package/src/runtime/errors.js +1 -0
  61. package/src/runtime/git.js +233 -0
  62. package/src/runtime/login-listener.js +15 -0
  63. package/src/runtime/oauth.js +2622 -0
  64. package/src/runtime/output.js +37 -5
  65. package/src/runtime/ports.js +31 -0
  66. package/src/runtime/profiles.js +906 -55
  67. package/src/runtime/skill-sync/cloud-client.ts +99 -0
  68. package/src/runtime/skill-sync/index.ts +697 -0
  69. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  70. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  71. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  72. package/src/runtime/skill-sync/types.ts +110 -0
  73. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  74. package/src/runtime/skill-sync-service.js +109 -0
  75. package/src/runtime/store-screenshot.js +143 -0
  76. package/src/runtime/sync-skills.d.ts +37 -0
  77. package/src/runtime/sync-skills.js +231 -0
  78. package/src/runtime/telemetry.js +92 -0
  79. package/src/runtime/transport.js +324 -45
  80. package/src/skill-sync-worker-entry.js +2 -0
  81. package/src/skill-sync-worker.js +50 -0
  82. package/template/.harness/index.html.tmpl +430 -0
  83. package/template/CHANGELOG.md +5 -0
  84. package/template/app/layout.tsx +5 -2
  85. package/template/app/page.tsx +49 -42
  86. package/template/components/page-heading.tsx +23 -0
  87. package/template/components/ui/badge.tsx +7 -4
  88. package/template/components/ui/card.tsx +24 -11
  89. package/template/components/ui/native-select.tsx +24 -0
  90. package/template/notis.config.ts +24 -6
  91. package/template/package-lock.json +4137 -0
  92. package/template/package.json +5 -5
  93. package/template/packages/{notis-sdk → sdk}/package.json +13 -3
  94. package/template/packages/sdk/src/agentContext.ts +36 -0
  95. package/template/packages/sdk/src/components/DocumentEditor.tsx +103 -0
  96. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  97. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  98. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  99. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  100. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  101. package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  102. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  103. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  104. package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
  105. package/template/packages/sdk/src/config.ts +257 -0
  106. package/template/packages/sdk/src/documents.ts +256 -0
  107. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  108. package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
  109. package/template/packages/sdk/src/hooks/useCloudComputer.ts +64 -0
  110. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +836 -0
  111. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  112. package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  113. package/template/packages/sdk/src/hooks/useDocument.ts +43 -0
  114. package/template/packages/sdk/src/hooks/useDocuments.ts +84 -0
  115. package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
  116. package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
  117. package/template/packages/sdk/src/hooks/useMultiSelect.ts +95 -0
  118. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +10 -4
  119. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +11 -8
  120. package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
  121. package/template/packages/sdk/src/hooks/useTool.ts +65 -0
  122. package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
  123. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +81 -0
  124. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  125. package/template/packages/sdk/src/index.ts +161 -0
  126. package/template/packages/sdk/src/interactions/actions.ts +59 -0
  127. package/template/packages/sdk/src/interactions/shortcuts.tsx +694 -0
  128. package/template/packages/sdk/src/interactions/visibility.ts +13 -0
  129. package/template/packages/sdk/src/interactions.ts +45 -0
  130. package/template/packages/sdk/src/provider.tsx +44 -0
  131. package/template/packages/sdk/src/queryCache.ts +170 -0
  132. package/template/packages/sdk/src/runtime.ts +451 -0
  133. package/template/packages/sdk/src/styles.css +213 -0
  134. package/template/packages/sdk/src/tailwind.ts +56 -0
  135. package/template/packages/{notis-sdk → sdk}/src/vite.ts +5 -1
  136. package/template/tailwind.config.ts +1 -0
  137. package/src/command-specs/db.js +0 -163
  138. package/src/runtime/app-preview-server.js +0 -312
  139. package/template/packages/notis-sdk/src/config.ts +0 -48
  140. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  141. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  142. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  143. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  144. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  145. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  146. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  147. package/template/packages/notis-sdk/src/index.ts +0 -47
  148. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  149. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  150. package/template/packages/notis-sdk/src/styles.css +0 -123
  151. /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
  152. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
  153. /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
  154. /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
@@ -0,0 +1,358 @@
1
+ import { CliError, EXIT_CODES } from '../runtime/errors.js';
2
+ import { quoteShellArgument } from '../runtime/auth-recovery.js';
3
+ import {
4
+ DEFAULT_PROFILE,
5
+ getProfile,
6
+ listProfiles,
7
+ loadConfig,
8
+ normalizeConfig,
9
+ profileHasCredential,
10
+ profileExists,
11
+ resolveWorktreeRuntime,
12
+ updateConfig,
13
+ } from '../runtime/profiles.js';
14
+
15
+ function describeProfile(config, name, worktreeRuntime) {
16
+ const entry = listProfiles(config).find((candidate) => candidate.name === name);
17
+ if (worktreeRuntime?.profile === name) {
18
+ return {
19
+ name,
20
+ active: false,
21
+ api_base: worktreeRuntime.api_base,
22
+ label: './dev.sh worktree',
23
+ credential_kind: 'dev',
24
+ user_id: worktreeRuntime.expected_user_id || null,
25
+ authenticated: true,
26
+ dev_runtime_live: true,
27
+ };
28
+ }
29
+ if (!entry) return null;
30
+ return { ...entry, dev_runtime_live: false };
31
+ }
32
+
33
+ function unknownProfileError(name, config) {
34
+ return new CliError({
35
+ code: 'profile_unknown',
36
+ message: `No CLI profile named "${name}"`,
37
+ exitCode: EXIT_CODES.usage,
38
+ details: { known_profiles: Object.keys(normalizeConfig(config).profiles) },
39
+ hints: [
40
+ { command: 'notis profile list', reason: 'See which profiles this machine has' },
41
+ { command: `notis login --profile ${quoteShellArgument(name)}`, reason: 'Authorize a new account under this name' },
42
+ ],
43
+ });
44
+ }
45
+
46
+ function currentWorktreeState() {
47
+ const resolved = resolveWorktreeRuntime();
48
+ return resolved?.unavailable
49
+ ? { runtime: null, unavailable: resolved.unavailable }
50
+ : { runtime: resolved, unavailable: null };
51
+ }
52
+
53
+ function unavailableForEffectiveRoute(ctx, unavailable) {
54
+ return ctx.runtime.profileSource === 'explicit' ? null : unavailable;
55
+ }
56
+
57
+ async function listHandler(ctx) {
58
+ const config = loadConfig();
59
+ const { runtime: worktreeRuntime, unavailable: worktreeUnavailable } = currentWorktreeState();
60
+ const effectiveWorktreeUnavailable = unavailableForEffectiveRoute(ctx, worktreeUnavailable);
61
+ const profiles = listProfiles(config).map((entry) =>
62
+ describeProfile(config, entry.name, worktreeRuntime));
63
+ if (
64
+ worktreeRuntime
65
+ && !profiles.some((entry) => entry.name === worktreeRuntime.profile)
66
+ ) {
67
+ profiles.unshift(describeProfile(config, worktreeRuntime.profile, worktreeRuntime));
68
+ }
69
+ // In a hosted shell every profile reads as signed out while commands work
70
+ // fine, because NOTIS_JWT overrides all of them. Say so rather than letting
71
+ // an agent conclude it needs to authorize something.
72
+ const envOverride = ctx.runtime.credentialKind === 'env';
73
+
74
+ return ctx.output.emitSuccess({
75
+ command: ctx.spec.command_path.join(' '),
76
+ data: {
77
+ active_profile: normalizeConfig(config).current_profile,
78
+ effective_profile: effectiveWorktreeUnavailable ? null : ctx.runtime.profileName,
79
+ effective_profile_source: effectiveWorktreeUnavailable
80
+ ? 'worktree-unavailable'
81
+ : ctx.runtime.profileSource,
82
+ worktree_runtime_unavailable: Boolean(effectiveWorktreeUnavailable),
83
+ effective_credential_kind: ctx.runtime.credentialKind || null,
84
+ env_credential_override: envOverride,
85
+ profiles,
86
+ },
87
+ humanSummary: effectiveWorktreeUnavailable
88
+ ? `${profiles.length} stored CLI profile${profiles.length === 1 ? '' : 's'}; this local-only worktree is stopped`
89
+ : envOverride
90
+ ? `NOTIS_JWT overrides all ${profiles.length} stored profile${profiles.length === 1 ? '' : 's'}`
91
+ : `${profiles.length} CLI profile${profiles.length === 1 ? '' : 's'} on this machine`,
92
+ renderHuman: () =>
93
+ [
94
+ ...(envOverride
95
+ ? ['NOTIS_JWT is set and takes precedence over every profile below.', '']
96
+ : []),
97
+ ...profiles.map((entry) => {
98
+ const marker = !envOverride && entry.name === ctx.runtime.profileName ? '*' : ' ';
99
+ const auth = entry.authenticated ? entry.credential_kind : 'signed out';
100
+ const live = entry.dev_runtime_live ? ' (dev.sh running)' : '';
101
+ return `${marker} ${entry.name.padEnd(16)} ${String(entry.api_base).padEnd(32)} ${auth}${live}`;
102
+ }),
103
+ ].join('\n'),
104
+ hints: [
105
+ ...(effectiveWorktreeUnavailable
106
+ ? [{
107
+ command: 'notis --profile <name> <command>',
108
+ reason: 'Explicitly escape the stopped local-only worktree for one command',
109
+ }]
110
+ : []),
111
+ { command: 'notis profile use <name>', reason: 'Switch the default account outside this worktree' },
112
+ { command: 'notis login --profile <name>', reason: 'Add another account without signing this one out' },
113
+ ],
114
+ });
115
+ }
116
+
117
+ async function useHandler(ctx) {
118
+ const requested = ctx.args.name;
119
+ const config = loadConfig();
120
+ if (!profileExists(config, requested)) {
121
+ throw unknownProfileError(requested, config);
122
+ }
123
+
124
+ // Switching only moves the pointer. Every profile keeps its own credential so
125
+ // going back is another `profile use`, never another browser authorization.
126
+ const next = updateConfig((latest) => {
127
+ latest.current_profile = requested;
128
+ return latest;
129
+ });
130
+ const { runtime: worktreeRuntime, unavailable: worktreeUnavailable } = currentWorktreeState();
131
+ const profile = getProfile(next, requested);
132
+ const worktreeOverride = Boolean(worktreeRuntime);
133
+ const worktreeBlocked = Boolean(worktreeUnavailable);
134
+ const authenticated = profileHasCredential(profile);
135
+
136
+ return ctx.output.emitSuccess({
137
+ command: ctx.spec.command_path.join(' '),
138
+ data: {
139
+ active_profile: requested,
140
+ ...describeProfile(next, requested, worktreeRuntime),
141
+ effective_profile: worktreeBlocked
142
+ ? null
143
+ : worktreeOverride ? worktreeRuntime.profile : requested,
144
+ effective_profile_source: worktreeBlocked
145
+ ? 'worktree-unavailable'
146
+ : worktreeOverride ? 'worktree' : 'current',
147
+ worktree_runtime_unavailable: worktreeBlocked,
148
+ },
149
+ humanSummary: worktreeBlocked
150
+ ? `Saved profile "${requested}" as the default outside this worktree; this local-only checkout is stopped, so use --profile explicitly or restart ./dev.sh.`
151
+ : worktreeOverride
152
+ ? `Saved profile "${requested}" as the default outside this worktree; this checkout still uses "${worktreeRuntime.profile}" unless --profile is explicit.`
153
+ : authenticated
154
+ ? `Switched to profile "${requested}".`
155
+ : `Switched to profile "${requested}", which has no credential yet.`,
156
+ hints: worktreeBlocked
157
+ ? [
158
+ {
159
+ command: `notis --profile ${quoteShellArgument(requested)} whoami`,
160
+ reason: 'Explicitly use and confirm this account while the local worktree is stopped',
161
+ },
162
+ { message: 'Restart ./dev.sh to restore the worktree test identity.' },
163
+ ]
164
+ : worktreeOverride
165
+ ? [
166
+ {
167
+ command: `notis --profile ${quoteShellArgument(requested)} whoami`,
168
+ reason: 'Use and confirm this account explicitly inside the active worktree',
169
+ },
170
+ ...(!authenticated
171
+ ? [{
172
+ command: `notis login --profile ${quoteShellArgument(requested)}`,
173
+ reason: 'Authorize an account for this profile',
174
+ }]
175
+ : []),
176
+ ]
177
+ : authenticated
178
+ ? [{ command: 'notis whoami', reason: 'Confirm the account and API this profile targets' }]
179
+ : [{ command: `notis login --profile ${quoteShellArgument(requested)}`, reason: 'Authorize an account for this profile' }],
180
+ });
181
+ }
182
+
183
+ async function showHandler(ctx) {
184
+ const config = loadConfig();
185
+ const name = ctx.args.name || ctx.runtime.profileName;
186
+ const { runtime: worktreeRuntime, unavailable: worktreeUnavailable } = currentWorktreeState();
187
+ const effectiveWorktreeUnavailable = unavailableForEffectiveRoute(ctx, worktreeUnavailable);
188
+ const described = describeProfile(config, name, worktreeRuntime);
189
+ if (!described) {
190
+ throw unknownProfileError(name, config);
191
+ }
192
+ const profile = getProfile(config, name);
193
+ const envOverride = ctx.runtime.credentialKind === 'env';
194
+
195
+ return ctx.output.emitSuccess({
196
+ command: ctx.spec.command_path.join(' '),
197
+ data: {
198
+ ...described,
199
+ env_credential_override: envOverride,
200
+ worktree_runtime_unavailable: Boolean(effectiveWorktreeUnavailable),
201
+ oauth_scopes: profile.oauth_scopes || [],
202
+ oauth_access_expires_at: profile.oauth_access_expires_at || null,
203
+ oauth_refresh_expires_at: profile.oauth_refresh_expires_at || null,
204
+ dev_workspace_root:
205
+ profile.dev_workspace_root || (
206
+ worktreeRuntime?.profile === name
207
+ ? worktreeRuntime.workspace_root || null
208
+ : null
209
+ ),
210
+ },
211
+ humanSummary: `Profile "${name}" targets ${described.api_base}`,
212
+ renderHuman: () =>
213
+ [
214
+ `Profile: ${name}`,
215
+ `API: ${described.api_base}`,
216
+ `User: ${described.user_id || 'unknown'}`,
217
+ `Credential:${described.credential_kind ? ` ${described.credential_kind}` : ' none'}`,
218
+ `Active: ${described.active ? 'yes' : 'no'}`,
219
+ ...(envOverride
220
+ ? ['', 'NOTIS_JWT is set and overrides this profile\'s credential.']
221
+ : []),
222
+ ].join('\n'),
223
+ hints: effectiveWorktreeUnavailable
224
+ ? [{
225
+ command: `notis --profile ${quoteShellArgument(name)} whoami`,
226
+ reason: 'Explicitly use this profile while the local-only worktree is stopped',
227
+ }]
228
+ : [],
229
+ });
230
+ }
231
+
232
+ async function removeHandler(ctx) {
233
+ const requested = ctx.args.name;
234
+ const config = loadConfig();
235
+ if (!profileExists(config, requested)) {
236
+ throw unknownProfileError(requested, config);
237
+ }
238
+ if (requested === DEFAULT_PROFILE) {
239
+ throw new CliError({
240
+ code: 'profile_not_removable',
241
+ message: 'The "default" profile cannot be removed',
242
+ exitCode: EXIT_CODES.usage,
243
+ hints: [{ command: 'notis logout', reason: 'Clear its credential instead of removing the profile' }],
244
+ });
245
+ }
246
+ if (profileHasCredential(getProfile(config, requested)) && !ctx.options.force) {
247
+ throw new CliError({
248
+ code: 'profile_still_authorized',
249
+ message: `Profile "${requested}" still holds a credential`,
250
+ exitCode: EXIT_CODES.usage,
251
+ hints: [
252
+ {
253
+ command: `notis logout --profile ${quoteShellArgument(requested)}`,
254
+ reason: 'Revoke the grant server-side before discarding it locally',
255
+ },
256
+ { command: `notis profile remove ${quoteShellArgument(requested)} --force`, reason: 'Discard the local credential without revoking it' },
257
+ ],
258
+ });
259
+ }
260
+
261
+ const next = updateConfig((latest) => {
262
+ delete latest.profiles[requested];
263
+ if (latest.current_profile === requested) {
264
+ latest.current_profile = DEFAULT_PROFILE;
265
+ }
266
+ return latest;
267
+ });
268
+
269
+ return ctx.output.emitSuccess({
270
+ command: ctx.spec.command_path.join(' '),
271
+ data: {
272
+ removed_profile: requested,
273
+ active_profile: normalizeConfig(next).current_profile,
274
+ },
275
+ humanSummary: `Removed profile "${requested}".`,
276
+ });
277
+ }
278
+
279
+ export const profileCommandSpecs = [
280
+ {
281
+ command_path: ['profile', 'list'],
282
+ summary: 'List every CLI profile with its account, API endpoint, and credential state.',
283
+ when_to_use:
284
+ 'Use this to see which accounts and environments this machine can reach before choosing one.',
285
+ args_schema: { arguments: [], options: [] },
286
+ examples: ['notis profile list', 'notis profile list --json'],
287
+ output_schema:
288
+ 'Returns active_profile, effective_profile, and a profiles array of {name, api_base, credential_kind, user_id, authenticated, dev_runtime_live}.',
289
+ mutates: false,
290
+ idempotent: true,
291
+ require_auth: false,
292
+ allow_unknown_profile: true,
293
+ related_commands: ['notis profile use', 'notis login', 'notis whoami'],
294
+ backend_call: { type: 'local_config' },
295
+ handler: listHandler,
296
+ },
297
+ {
298
+ command_path: ['profile', 'use'],
299
+ summary: 'Switch the default profile without signing any profile out.',
300
+ when_to_use:
301
+ 'Use this to change which account and API subsequent commands target. Every other profile keeps its credential.',
302
+ args_schema: {
303
+ arguments: [{ token: '<name>', key: 'name', description: 'Profile to make active.' }],
304
+ options: [],
305
+ },
306
+ examples: ['notis profile use work', 'notis profile use default'],
307
+ output_schema: 'Returns the newly active profile with its api_base, user_id, and credential kind.',
308
+ mutates: true,
309
+ idempotent: true,
310
+ require_auth: false,
311
+ allow_unknown_profile: true,
312
+ related_commands: ['notis profile list', 'notis login'],
313
+ backend_call: { type: 'local_config' },
314
+ handler: useHandler,
315
+ },
316
+ {
317
+ command_path: ['profile', 'show'],
318
+ summary: 'Show one profile in detail, including scopes and credential expiry.',
319
+ when_to_use: 'Use this to inspect exactly which account and endpoint a profile resolves to.',
320
+ args_schema: {
321
+ arguments: [
322
+ { token: '[name]', key: 'name', description: 'Profile to inspect; defaults to the active one.' },
323
+ ],
324
+ options: [],
325
+ },
326
+ examples: ['notis profile show', 'notis profile show work --json'],
327
+ output_schema:
328
+ 'Returns name, api_base, user_id, credential_kind, oauth scopes and expiries, and dev runtime state.',
329
+ mutates: false,
330
+ idempotent: true,
331
+ require_auth: false,
332
+ allow_unknown_profile: true,
333
+ related_commands: ['notis profile list', 'notis whoami'],
334
+ backend_call: { type: 'local_config' },
335
+ handler: showHandler,
336
+ },
337
+ {
338
+ command_path: ['profile', 'remove'],
339
+ summary: 'Delete a CLI profile from this machine.',
340
+ when_to_use:
341
+ 'Use this after logging a profile out. Removing a still-authorized profile requires --force and leaves the grant live server-side.',
342
+ args_schema: {
343
+ arguments: [{ token: '<name>', key: 'name', description: 'Profile to delete.' }],
344
+ options: [
345
+ { flags: '--force', description: 'Discard a profile that still holds a credential.' },
346
+ ],
347
+ },
348
+ examples: ['notis profile remove old-work', 'notis profile remove old-work --force'],
349
+ output_schema: 'Returns removed_profile and the resulting active_profile.',
350
+ mutates: true,
351
+ idempotent: true,
352
+ require_auth: false,
353
+ allow_unknown_profile: true,
354
+ related_commands: ['notis logout', 'notis profile list'],
355
+ backend_call: { type: 'local_config' },
356
+ handler: removeHandler,
357
+ },
358
+ ];
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { appsCommandSpecs } from './apps.js';
4
+ import { buildArtifact, prepareAppRelease, resolveProjectDir } from '../runtime/app-platform.js';
5
+ import { nextIdempotencyKey, runToolCommand } from './helpers.js';
6
+ import { usageError, EXIT_CODES } from '../runtime/errors.js';
7
+
8
+ const reuse = (command, name = command) => {
9
+ const spec = appsCommandSpecs.find(item => item.command_path.join(' ') === `apps ${command}`);
10
+ return {
11
+ ...spec,
12
+ handler: async ctx => {
13
+ const output = new Proxy(ctx.output, {
14
+ get(target, key) {
15
+ if (key === 'emitSuccess') return result => {
16
+ const value = { ...result, warnings: (result.warnings || []).filter(warning => !warning.startsWith('Store readiness:')) };
17
+ if (value.data?.listing) { value.data = { ...value.data }; delete value.data.listing; }
18
+ return target.emitSuccess(value);
19
+ };
20
+ const value = Reflect.get(target, key);
21
+ return typeof value === 'function' ? value.bind(target) : value;
22
+ },
23
+ });
24
+ return spec.handler({ ...ctx, options: { ...ctx.options, listing: false, ...(name === 'preview' ? { keepOpen: true } : {}) }, output });
25
+ },
26
+ command_path: ['reports', name],
27
+ summary: `${name[0].toUpperCase() + name.slice(1)} a record-owned SDK report locally.${name === 'preview' ? ' Keeps the preview server and browser session open.' : ''}`,
28
+ args_schema: {
29
+ ...spec.args_schema,
30
+ options: (spec.args_schema?.options || []).map(option => option.flags === '--listing'
31
+ ? { ...option, description: 'Ignored for reports; saving a report does not publish a Store listing.' }
32
+ : option),
33
+ },
34
+ examples: (spec.examples || []).filter(example => !example.includes('--listing')).map(example => example.replace(`apps ${command}`, `reports ${name}`)),
35
+ when_to_use: 'Author an independent report without deploying its owning app.',
36
+ };
37
+ };
38
+ export const reportsCommandSpecs = [
39
+ reuse('init'), reuse('build'), reuse('verify'), reuse('verify', 'preview'),
40
+ {
41
+ command_path: ['reports', 'save'],
42
+ summary: 'Build, verify and save a report into an app-owned database record.',
43
+ when_to_use: 'Persist an independently authored report, not an app release.',
44
+ args_schema: {
45
+ arguments: [{ token: '[dir]', key: 'dir', description: 'Report source directory.' }],
46
+ options: [
47
+ { flags: '--database-id <id>', description: 'Required. Owning app database.' },
48
+ { flags: '--document-id <id>', description: 'Existing record to update or attach to.' },
49
+ { flags: '--attach', description: 'Attach to an existing non-view record.' },
50
+ { flags: '--expected-revision <revision>', description: 'Fresh view revision (0 for a record without a view).' },
51
+ { flags: '--title <title>', description: 'Required, including updates. Record title.' },
52
+ { flags: '--context-file <file>', description: 'Required. UTF-8 readable report content and structure.' },
53
+ { flags: '--properties-file <file>', description: 'JSON database property values keyed by name.' },
54
+ ],
55
+ },
56
+ examples: ['notis reports save ./weekly-report --database-id <id> --title \"Weekly review\" --context-file ./context.md'], mutates: true, idempotent: true,
57
+ backend_call: { type: 'tool', name: 'LOCAL_NOTIS_SAVE_REPORT' },
58
+ async handler(ctx) {
59
+ const dir = resolveProjectDir(ctx.args.dir || '.');
60
+ if (!ctx.options.databaseId || !ctx.options.title || !ctx.options.contextFile) throw usageError('--database-id, --title and --context-file are required.');
61
+ if (ctx.options.documentId && (!/^\d+$/.test(String(ctx.options.expectedRevision ?? '')) || !Number.isSafeInteger(Number(ctx.options.expectedRevision)))) throw usageError('--expected-revision is required for update/attach.');
62
+ if (ctx.options.attach && !ctx.options.documentId) throw usageError('--attach requires --document-id.');
63
+ const context = readFileSync(resolve(ctx.options.contextFile), 'utf8');
64
+ const properties = ctx.options.propertiesFile ? JSON.parse(readFileSync(resolve(ctx.options.propertiesFile), 'utf8')) : {};
65
+ await buildArtifact(dir, { stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit' });
66
+ const release = prepareAppRelease(dir);
67
+ try {
68
+ if (release.manifest.routes?.length !== 1) throw usageError('Reports require exactly one SDK route.');
69
+ let verification;
70
+ const verify = appsCommandSpecs.find(item => item.command_path.join(' ') === 'apps verify');
71
+ const exit = await verify.handler({ ...ctx, args: { dir: release.projectDir }, options: { skipBuild: true, mode: 'stub' }, output: { ...ctx.output, isMachineMode: () => true, emitSuccess: value => { verification = value; } } });
72
+ if (exit !== EXIT_CODES.ok || verification?.data?.status !== 'passed') throw usageError('Report verification failed; nothing saved.');
73
+ const files = { ...release.files, ...Object.fromEntries(Object.entries(release.sourceFiles).map(([path, data]) => [`source/${path}`, data])) };
74
+ const result = await runToolCommand({ runtime: { ...ctx.runtime, timeoutMs: Math.max(ctx.runtime.timeoutMs || 0, 90000) }, toolName: 'LOCAL_NOTIS_SAVE_REPORT', mutating: true,
75
+ idempotencyKey: nextIdempotencyKey(ctx.globalOptions), arguments_: {
76
+ operation: ctx.options.attach ? 'attach' : ctx.options.documentId ? 'update' : 'create',
77
+ database_id: ctx.options.databaseId, title: ctx.options.title, properties,
78
+ ...(ctx.options.documentId ? { document_id: ctx.options.documentId, expected_revision: Number(ctx.options.expectedRevision) } : {}),
79
+ report: { schema: 'notis-report/v2', context, artifact: { manifest: release.manifest, files, encoding: 'base64' } },
80
+ } });
81
+ if (!result?.payload?.document?.id || !result.payload.document.view_revision) throw usageError('Save returned no record identity. Read back before retrying; the outcome may be unknown.');
82
+ return ctx.output.emitSuccess({ command: 'reports save', data: result.payload });
83
+ } finally { release.close(); }
84
+ },
85
+ },
86
+ ];
@@ -0,0 +1,75 @@
1
+ import { getJwtSubject } from '../runtime/profiles.js';
2
+ import { reconcileAllSkills } from '../runtime/sync-skills.js';
3
+ import { ensureFreshOAuthCredential } from '../runtime/oauth.js';
4
+ import { installSkillSyncService } from '../runtime/skill-sync-service.js';
5
+
6
+ async function loadSkillSyncEngine() {
7
+ return import('../../dist/skill-sync/index.js');
8
+ }
9
+
10
+ export async function syncSkillsHandler(ctx, {
11
+ refresh = ensureFreshOAuthCredential, loadEngine = loadSkillSyncEngine,
12
+ reconcile = reconcileAllSkills, install = installSkillSyncService,
13
+ } = {}) {
14
+ await refresh(ctx.runtime);
15
+ const userId = ctx.runtime.oauthUserId || getJwtSubject(ctx.runtime.jwt);
16
+ const { runSkillSync, fetchSyncSettings } = await loadEngine();
17
+ const settings = await fetchSyncSettings(ctx.runtime.apiBase, ctx.runtime.jwt);
18
+ const result = await reconcile({
19
+ serverUrl: ctx.runtime.apiBase,
20
+ jwt: ctx.runtime.jwt,
21
+ userId: settings.user_id || userId,
22
+ honorSyncEnabled: Boolean(ctx.options.electronRepeat),
23
+ runAccountSync: (serverUrl, jwt, dependencies, options) => runSkillSync(
24
+ serverUrl, jwt, { ...dependencies, fetchSyncSettings: async () => settings }, options,
25
+ ),
26
+ });
27
+ if (settings.sync_enabled && !ctx.options.electronRepeat) {
28
+ try {
29
+ result.automaticSync = await install(ctx.runtime);
30
+ } catch (error) {
31
+ result.automaticSync = { status: 'error', message: error.message };
32
+ }
33
+ }
34
+
35
+ const failures = [...(result.failedPushes || []), ...(result.failedLinks || [])];
36
+ return ctx.output.emitSuccess({
37
+ command: 'skills sync',
38
+ data: result,
39
+ warnings: result.automaticSync?.status === 'error'
40
+ ? [`Skills synced, but automatic refresh could not start: ${result.automaticSync.message}`] : [],
41
+ humanSummary: failures.length ? `Skill sync completed with ${failures.length} reported failures; inspect failedPushes and failedLinks.` : result.syncEnabled
42
+ ? `Synced account skills and kept ${result.baseSkills.length} base skills current.`
43
+ : `Automatic Desktop sync is off; kept ${result.baseSkills.length} base skills current.`,
44
+ renderHuman: () => failures.length ? `Skill sync needs attention: ${failures.map((failure) => `${failure.name}: ${failure.error}`).join("; ")}` : result.syncEnabled
45
+ ? `Skills synced. Base skills current: ${result.baseSkills.join(', ')}.`
46
+ : `Automatic Desktop sync is off. Base skills remain current: ${result.baseSkills.join(', ')}.`,
47
+ });
48
+ }
49
+
50
+ export const skillsCommandSpecs = [
51
+ {
52
+ command_path: ['skills', 'sync'],
53
+ summary: 'Synchronize account skills and keep the three Notis base skills current.',
54
+ when_to_use:
55
+ 'Run manually whenever local agent skills should be reconciled. Manual runs ignore the Desktop automatic-sync preference.',
56
+ args_schema: {
57
+ arguments: [],
58
+ options: [
59
+ {
60
+ flags: '--electron-repeat',
61
+ description: 'Honor the automatic Desktop sync preference (used by Notis Desktop).',
62
+ },
63
+ ],
64
+ },
65
+ examples: ['notis skills sync', 'notis skills sync --json'],
66
+ output_schema:
67
+ 'Returns account sync counts plus baseSkills, baseInstalled, baseLinked, and baseBackups.',
68
+ mutates: true,
69
+ idempotent: true,
70
+ require_auth: true,
71
+ related_commands: ['notis login', 'notis start', 'notis doctor'],
72
+ backend_call: { type: 'local', name: 'skill_sync' },
73
+ handler: syncSkillsHandler,
74
+ },
75
+ ];