@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.196.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 +822 -135
- package/bin/check-runtime.js +15 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_boundary_rules.json +50 -0
- package/config/notis_app_design_rules.json +135 -0
- package/config/portal-route-roots.json +48 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +45587 -0
- package/dist/base-skills/notis-apps/SKILL.md +93 -0
- package/dist/base-skills/notis-apps/references/architecture.md +114 -0
- package/dist/base-skills/notis-apps/references/context.md +81 -0
- package/dist/base-skills/notis-apps/references/design.md +97 -0
- package/dist/base-skills/notis-apps/references/memory.md +58 -0
- package/dist/base-skills/notis-apps/references/reading.md +67 -0
- package/dist/base-skills/notis-apps/references/release.md +107 -0
- package/dist/base-skills/notis-apps/references/sdk.md +116 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +24 -0
- package/dist/base-skills/notis-apps/references/views.md +142 -0
- package/dist/base-skills/notis-cli/SKILL.md +154 -0
- package/dist/base-skills/notis-cli/references/app-delivery.md +19 -0
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +21 -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 +41 -0
- package/dist/base-skills/notis-query/references/database-discovery.md +60 -0
- package/dist/base-skills/notis-query/references/documents.md +63 -0
- package/dist/base-skills/notis-query/references/query.md +76 -0
- package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
- package/dist/sdk/src/agentContext.ts +36 -0
- package/dist/sdk/src/bundleModule.ts +79 -0
- package/dist/sdk/src/components/Dialog.tsx +46 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +106 -0
- package/dist/sdk/src/components/DocumentPage.tsx +141 -0
- package/dist/sdk/src/components/Markdown.tsx +60 -0
- package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
- package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
- package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
- package/dist/sdk/src/components/RecordComponents.tsx +33 -0
- package/dist/sdk/src/components/RenderChartData.tsx +19 -0
- package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
- package/dist/sdk/src/components/Skeleton.tsx +24 -0
- package/dist/sdk/src/config.ts +239 -0
- package/dist/sdk/src/documents.ts +258 -0
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
- package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
- package/dist/sdk/src/hooks/useCloudComputer.ts +95 -0
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
- package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
- package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/sdk/src/hooks/useDocument.ts +43 -0
- package/dist/sdk/src/hooks/useDocuments.ts +84 -0
- package/dist/sdk/src/hooks/useHandover.ts +78 -0
- package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
- package/dist/sdk/src/hooks/useNotis.ts +42 -0
- package/dist/sdk/src/hooks/useNotisNavigation.ts +67 -0
- package/dist/sdk/src/hooks/useQuery.ts +74 -0
- package/dist/sdk/src/hooks/useTool.ts +65 -0
- package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
- package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
- package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/sdk/src/hooks/useViewParams.ts +121 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/dist/sdk/src/index.ts +200 -0
- package/dist/sdk/src/interactions/actions.ts +59 -0
- package/dist/sdk/src/interactions/shortcuts.tsx +740 -0
- package/dist/sdk/src/interactions/visibility.ts +13 -0
- package/dist/sdk/src/interactions.ts +45 -0
- package/dist/sdk/src/nativeData.ts +39 -0
- package/dist/sdk/src/presentation.tsx +82 -0
- package/dist/sdk/src/provider.tsx +49 -0
- package/dist/sdk/src/queryCache.ts +172 -0
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +603 -0
- package/dist/sdk/src/space.ts +425 -0
- package/dist/sdk/src/styles.css +290 -0
- package/dist/sdk/src/tailwind.ts +66 -0
- package/dist/sdk/src/vite.ts +83 -0
- package/dist/skill-sync/index.js +2143 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/dist/skill-sync-worker.mjs +3487 -0
- package/dist/space-harness/host.js +66 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +1042 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +180 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +173 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/pool.js +76 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +201 -0
- package/package.json +25 -8
- package/skills/notis-apps/cli.md +406 -0
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +133 -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 +1476 -203
- package/src/command-specs/auth.js +114 -137
- package/src/command-specs/diagnostics.js +730 -0
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/helpers.js +86 -82
- package/src/command-specs/index.js +31 -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/skills.js +304 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/spaces.js +572 -0
- package/src/command-specs/tools.js +455 -139
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +700 -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 +407 -0
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +2752 -237
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/app-test-server.js +293 -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/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -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/native-record-files.js +47 -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/retired-sdk-files.js +20 -0
- package/src/runtime/screenshot-framing.js +85 -0
- package/src/runtime/skill-sync/cloud-client.ts +121 -0
- package/src/runtime/skill-sync/index.ts +905 -0
- package/src/runtime/skill-sync/local-scanner.ts +1259 -0
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +59 -0
- package/src/runtime/skill-sync/symlink-manager.ts +433 -0
- package/src/runtime/skill-sync/sync-plan.ts +54 -0
- package/src/runtime/skill-sync/types.ts +157 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +66 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +50 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +105 -0
- package/src/runtime/space-source.js +281 -0
- package/src/runtime/space-test-server.js +327 -0
- package/src/runtime/space-verification.js +89 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +254 -0
- package/src/runtime/store-screenshot.js +146 -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 +331 -45
- package/src/runtime/view-render.js +49 -0
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +50 -0
- package/src/space-harness-host.ts +103 -0
- package/template/.harness/index.html.tmpl +435 -0
- package/template/CHANGELOG.md +5 -0
- package/template/app/globals.css +29 -3
- package/template/app/layout.tsx +6 -3
- 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/button.tsx +1 -1
- 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 +3642 -0
- package/template/package.json +19 -16
- package/template/postcss.config.mjs +1 -1
- package/template/tailwind.config.ts +1 -6
- package/template/tsconfig.json +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/useNotis.ts +0 -31
- package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
- 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/src/vite.ts +0 -54
- package/template/packages/notis-sdk/tsconfig.json +0 -15
- /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
- /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
- /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +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,304 @@
|
|
|
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
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { httpRequest } from '../runtime/transport.js';
|
|
8
|
+
import { usageError } from '../runtime/errors.js';
|
|
9
|
+
import { parseJson } from './helpers.js';
|
|
10
|
+
|
|
11
|
+
async function nativeSkillAuthoring(ctx, body, path = '/portal_skills/native-authoring') {
|
|
12
|
+
// A Space target, destination or link change touches Spaces: declare the protocol.
|
|
13
|
+
const spaceTarget = Boolean(body.target?.space_id || body.destination?.space_id || body.links);
|
|
14
|
+
const result = await httpRequest({ runtime: ctx.runtime, method: 'POST', path, body,
|
|
15
|
+
...(spaceTarget ? { spacesProtocol: 1 } : {}) });
|
|
16
|
+
return ctx.output.emitSuccess({ command: ctx.spec.command_path.join(' '), data: result.payload,
|
|
17
|
+
humanSummary: JSON.stringify(result.payload, null, 2),
|
|
18
|
+
meta: { mutating: body.operation !== 'read' && !body.dry_run, request_id: result.requestId } });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function exactKeys(value, keys) {
|
|
22
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
23
|
+
&& Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function skillTarget(value, updating) {
|
|
27
|
+
const direct = updating ? ['skill_id', 'access_revision'] : ['skill_id'];
|
|
28
|
+
const space = updating ? ['space_id', 'binding_id', 'binding_revision', 'expected_skill_id', 'space_revision'] : ['space_id', 'binding_id'];
|
|
29
|
+
if (!exactKeys(value, direct) && !exactKeys(value, space)) {
|
|
30
|
+
throw usageError('Use one exact direct or Space target from the Skill inventory or skills read.');
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requestKey(ctx) {
|
|
36
|
+
const request = ctx.options.requestId || ctx.globalOptions.idempotencyKey;
|
|
37
|
+
if (typeof request !== 'string' || !request || request.length > 200) throw usageError('Pass --request-id <stable-key>.');
|
|
38
|
+
return request;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function linkLabel(link) {
|
|
42
|
+
return `${link.space_name || link.space_id}/${link.alias}${link.available === false ? ' (unavailable)' : ''}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One line per Skill: name, ID, kind, editability and its Space links. */
|
|
46
|
+
export function skillInventoryLines(page) {
|
|
47
|
+
const entries = Array.isArray(page?.skills) ? page.skills : [];
|
|
48
|
+
if (!entries.length) return 'No Skills match.';
|
|
49
|
+
const lines = entries.map(entry => {
|
|
50
|
+
const state = entry.editable ? 'editable' : `read-only${entry.read_only_reason ? ` (${entry.read_only_reason})` : ''}`;
|
|
51
|
+
const links = (entry.links || []).map(linkLabel);
|
|
52
|
+
return `${entry.name || entry.skill_id} ${entry.skill_id} ${entry.kind} ${state} links: ${links.length ? links.join(', ') : 'none'}`;
|
|
53
|
+
});
|
|
54
|
+
if (page.has_more && page.next) lines.push(`More Skills: rerun with --after ${page.next}`);
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What a link change did and where the resource is listed now. */
|
|
59
|
+
export function linkChangeSummary(result) {
|
|
60
|
+
const parts = [];
|
|
61
|
+
const added = (result?.added || []).map(link => `${link.space_id}/${link.alias}`);
|
|
62
|
+
const removed = (result?.removed || []).map(link => `${link.space_id}/${link.alias}`);
|
|
63
|
+
if (added.length) parts.push(`added: ${added.join(', ')}`);
|
|
64
|
+
if (removed.length) parts.push(`removed: ${removed.join(', ')}`);
|
|
65
|
+
if (result?.became_standalone) parts.push('now your standalone Skill');
|
|
66
|
+
if (result?.replayed) parts.push('confirmed retry');
|
|
67
|
+
const current = (result?.links || []).map(linkLabel);
|
|
68
|
+
return `${parts.join('; ') || 'No change'}. Listed in: ${current.length ? current.join(', ') : 'none'}.`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function linkEntries(value, option, required, optional = []) {
|
|
72
|
+
const parsed = parseJson(value, option);
|
|
73
|
+
const valid = entry => entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
74
|
+
&& required.every(key => key in entry) && Object.keys(entry).every(key => required.includes(key) || optional.includes(key));
|
|
75
|
+
if (!Array.isArray(parsed) || !parsed.length || !parsed.every(valid)) {
|
|
76
|
+
throw usageError(`${option} must be a JSON array of {${[...required, ...optional.map(key => `${key}?`)].join(', ')}} entries.`);
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function loadSkillSyncEngine() {
|
|
82
|
+
return import('../../dist/skill-sync/index.js');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function syncSkillsHandler(ctx, {
|
|
86
|
+
refresh = ensureFreshOAuthCredential, loadEngine = loadSkillSyncEngine,
|
|
87
|
+
reconcile = reconcileAllSkills, install = installSkillSyncService,
|
|
88
|
+
} = {}) {
|
|
89
|
+
await refresh(ctx.runtime);
|
|
90
|
+
const userId = ctx.runtime.oauthUserId || getJwtSubject(ctx.runtime.jwt);
|
|
91
|
+
const { runSkillSync, fetchSyncSettings } = await loadEngine();
|
|
92
|
+
const settings = await fetchSyncSettings(ctx.runtime.apiBase, ctx.runtime.jwt);
|
|
93
|
+
const result = await reconcile({
|
|
94
|
+
serverUrl: ctx.runtime.apiBase,
|
|
95
|
+
jwt: ctx.runtime.jwt,
|
|
96
|
+
userId: settings.user_id || userId,
|
|
97
|
+
honorSyncEnabled: Boolean(ctx.options.electronRepeat),
|
|
98
|
+
runAccountSync: (serverUrl, jwt, dependencies, options) => runSkillSync(
|
|
99
|
+
serverUrl, jwt, { ...dependencies, fetchSyncSettings: async () => settings }, options,
|
|
100
|
+
),
|
|
101
|
+
});
|
|
102
|
+
if (settings.sync_enabled && !ctx.options.electronRepeat) {
|
|
103
|
+
try {
|
|
104
|
+
result.automaticSync = await install(ctx.runtime);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
result.automaticSync = { status: 'error', message: error.message };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const failures = [...(result.failedPushes || []), ...(result.failedLinks || [])];
|
|
111
|
+
return ctx.output.emitSuccess({
|
|
112
|
+
command: 'skills sync',
|
|
113
|
+
data: result,
|
|
114
|
+
warnings: result.automaticSync?.status === 'error'
|
|
115
|
+
? [`Skills synced, but automatic refresh could not start: ${result.automaticSync.message}`] : [],
|
|
116
|
+
humanSummary: failures.length ? `Skill sync completed with ${failures.length} reported failures; inspect failedPushes and failedLinks.` : result.syncEnabled
|
|
117
|
+
? `Synced account skills and kept ${result.baseSkills.length} base skills current.`
|
|
118
|
+
: `Automatic Desktop sync is off; kept ${result.baseSkills.length} base skills current.`,
|
|
119
|
+
renderHuman: () => failures.length ? `Skill sync needs attention: ${failures.map((failure) => `${failure.name}: ${failure.error}`).join("; ")}` : result.syncEnabled
|
|
120
|
+
? `Skills synced. Base skills current: ${result.baseSkills.join(', ')}.`
|
|
121
|
+
: `Automatic Desktop sync is off. Base skills remain current: ${result.baseSkills.join(', ')}.`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const skillsCommandSpecs = [
|
|
126
|
+
{
|
|
127
|
+
command_path: ['skills', 'list'],
|
|
128
|
+
summary: 'List every Skill you can see, with its kind, editability and Space links.',
|
|
129
|
+
when_to_use: 'Find a Skill and its exact edit target before reading or editing it, or see what a Space and its sub-Spaces list. Links to Spaces you cannot open are omitted; an unavailable link gives no access.',
|
|
130
|
+
args_schema: { arguments: [], options: [
|
|
131
|
+
{ flags: '--space <id>', description: 'Only Skills listed by this Space and, by default, its sub-Spaces.' },
|
|
132
|
+
{ flags: '--no-child-spaces', description: 'With --space: direct links of that Space only.' },
|
|
133
|
+
{ flags: '--include-disabled', description: 'Include Skills you disabled.' },
|
|
134
|
+
{ flags: '--after <cursor>', description: 'Continue from the next cursor of the previous page.' },
|
|
135
|
+
{ flags: '--limit <n>', description: 'Page size between 1 and 100 (default 50).' },
|
|
136
|
+
] },
|
|
137
|
+
examples: ['notis skills list', 'notis skills list --space <space-id> --no-child-spaces --json'],
|
|
138
|
+
mutates: false, idempotent: true, require_auth: true,
|
|
139
|
+
backend_call: { type: 'http', path: '/portal_skills/inventory' },
|
|
140
|
+
async handler(ctx) {
|
|
141
|
+
const body = {};
|
|
142
|
+
if (ctx.options.space !== undefined) body.space_id = ctx.options.space;
|
|
143
|
+
if (ctx.options.childSpaces === false) {
|
|
144
|
+
if (body.space_id === undefined) throw usageError('--no-child-spaces needs --space <id>.');
|
|
145
|
+
body.include_child_spaces = false;
|
|
146
|
+
}
|
|
147
|
+
if (ctx.options.includeDisabled) body.include_disabled = true;
|
|
148
|
+
if (ctx.options.after !== undefined) body.after = ctx.options.after;
|
|
149
|
+
if (ctx.options.limit !== undefined) {
|
|
150
|
+
const limit = Number(ctx.options.limit);
|
|
151
|
+
if (!/^\d+$/.test(String(ctx.options.limit)) || limit < 1 || limit > 100) throw usageError('Pass --limit between 1 and 100.');
|
|
152
|
+
body.limit = limit;
|
|
153
|
+
}
|
|
154
|
+
const result = await httpRequest({ runtime: ctx.runtime, method: 'POST', path: '/portal_skills/inventory', body });
|
|
155
|
+
return ctx.output.emitSuccess({ command: 'skills list', data: result.payload,
|
|
156
|
+
humanSummary: skillInventoryLines(result.payload), meta: { mutating: false, request_id: result.requestId } });
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
command_path: ['skills', 'create'],
|
|
161
|
+
summary: 'Create a native Skill, standalone or linked to an editable Space.',
|
|
162
|
+
when_to_use: 'Create complete instructions or files without provider installation. Choose an optional Space destination, dry-run, and keep the exact request key on retry.',
|
|
163
|
+
args_schema: { arguments: [{ token: '<file>', key: 'file', description: 'JSON containing name, optional description, and skill_md or base64 files.' }],
|
|
164
|
+
options: [{ flags: '--request-id <key>', description: 'Stable creation intent key; reuse after an uncertain reply.' },
|
|
165
|
+
{ flags: '--dry-run', description: 'Validate without creating an identity or uploading files.' }] },
|
|
166
|
+
examples: ['notis skills create ./skill.json --request-id create-1 --dry-run'],
|
|
167
|
+
mutates: true, idempotent: true, require_auth: true,
|
|
168
|
+
backend_call: { type: 'http', path: '/portal_skills/native-create' },
|
|
169
|
+
handler(ctx) {
|
|
170
|
+
const payload = parseJson(readFileSync(resolve(ctx.args.file), 'utf8'), 'Skill creation file');
|
|
171
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)
|
|
172
|
+
|| Object.keys(payload).some(key => !['destination', 'name', 'description', 'skill_md', 'files'].includes(key))) {
|
|
173
|
+
throw usageError('Provide a name, complete instructions or files, and an optional current destination; never override ownership.');
|
|
174
|
+
}
|
|
175
|
+
const destination = payload.destination ?? { kind: 'standalone' };
|
|
176
|
+
if (!(exactKeys(destination, ['kind']) && destination.kind === 'standalone')
|
|
177
|
+
&& !exactKeys(destination, ['space_id', 'alias', 'revision'])) throw usageError('Choose a standalone destination or exact Space, alias and revision.');
|
|
178
|
+
const request = ctx.options.requestId || ctx.globalOptions.idempotencyKey;
|
|
179
|
+
if (typeof request !== 'string' || !request || request.length > 200) throw usageError('Pass --request-id <stable-key>.');
|
|
180
|
+
return nativeSkillAuthoring(ctx, { ...payload, operation: 'create', destination,
|
|
181
|
+
request_id: request, dry_run: Boolean(ctx.options.dryRun) }, '/portal_skills/native-create');
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
command_path: ['skills', 'settings', 'read'],
|
|
186
|
+
summary: 'Read a native Skill personal enablement, agent targets and settings revision.',
|
|
187
|
+
when_to_use: 'Inspect direct-owner preferences without changing shared instructions or Space access.',
|
|
188
|
+
args_schema: { arguments: [{ token: '<skill-id>', key: 'skillId', description: 'Exact native Skill ID.' }], options: [] },
|
|
189
|
+
mutates: false, idempotent: true, require_auth: true,
|
|
190
|
+
backend_call: { type: 'http', path: '/portal_skills/native-settings' },
|
|
191
|
+
handler(ctx) { return nativeSkillAuthoring(ctx, { operation: 'read', target: { skill_id: ctx.args.skillId } }, '/portal_skills/native-settings'); },
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
command_path: ['skills', 'settings', 'update'],
|
|
195
|
+
summary: 'Update personal native Skill settings with a current revision and stable retry key.',
|
|
196
|
+
when_to_use: 'Enable or disable your agents without changing a shared Skill definition or another Space.',
|
|
197
|
+
args_schema: { arguments: [{ token: '<file>', key: 'file', description: 'JSON with target, revision and patch (enabled or agent_targets).' }],
|
|
198
|
+
options: [{ flags: '--request-id <key>', description: 'Stable settings intent; reuse after a lost reply.' },
|
|
199
|
+
{ flags: '--dry-run', description: 'Validate current ownership and revision without changing settings.' }] },
|
|
200
|
+
mutates: true, idempotent: true, require_auth: true,
|
|
201
|
+
backend_call: { type: 'http', path: '/portal_skills/native-settings' },
|
|
202
|
+
handler(ctx) {
|
|
203
|
+
const payload = parseJson(readFileSync(resolve(ctx.args.file), 'utf8'), 'Skill settings file');
|
|
204
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)
|
|
205
|
+
|| Object.keys(payload).sort().join(',') !== 'patch,revision,target'
|
|
206
|
+
|| !payload.target || Object.keys(payload.target).sort().join(',') !== 'access_revision,skill_id') {
|
|
207
|
+
throw usageError('Use a current standalone target, settings revision and personal settings patch.');
|
|
208
|
+
}
|
|
209
|
+
const request = ctx.options.requestId || ctx.globalOptions.idempotencyKey;
|
|
210
|
+
if (typeof request !== 'string' || !request || request.length > 200) throw usageError('Pass --request-id <stable-key>.');
|
|
211
|
+
return nativeSkillAuthoring(ctx, { ...payload, operation: 'update', request_id: request, dry_run: Boolean(ctx.options.dryRun) }, '/portal_skills/native-settings');
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
command_path: ['skills', 'read'],
|
|
216
|
+
summary: 'Inspect a native Skill through direct ownership or an editable Space link.',
|
|
217
|
+
when_to_use: 'Read the exact target and content version before editing. A Space target uses its current Editor authority, never a personal fallback.',
|
|
218
|
+
args_schema: { arguments: [{ token: '[skill-id]', key: 'skillId', description: 'Exact directly owned Skill ID; omit when passing --target.' }],
|
|
219
|
+
options: [{ flags: '--files', description: 'Include every verified supporting file as base64.' },
|
|
220
|
+
{ flags: '--target <json>', description: 'Exact {skill_id} or {space_id,binding_id} read target; do not combine with a Skill ID.' }] },
|
|
221
|
+
examples: ['notis skills read <skill-id> --files', 'notis skills read --target \'{"space_id":"<id>","binding_id":"<binding>"}\' --files'],
|
|
222
|
+
mutates: false, idempotent: true, require_auth: true,
|
|
223
|
+
backend_call: { type: 'http', path: '/portal_skills/native-authoring' },
|
|
224
|
+
handler(ctx) {
|
|
225
|
+
if (Boolean(ctx.args.skillId) === Boolean(ctx.options.target)) throw usageError('Pass either a Skill ID or --target <json>.');
|
|
226
|
+
const target = skillTarget(ctx.options.target ? parseJson(ctx.options.target, '--target') : { skill_id: ctx.args.skillId }, false);
|
|
227
|
+
return nativeSkillAuthoring(ctx, { operation: 'read', target,
|
|
228
|
+
include_files: Boolean(ctx.options.files) });
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
command_path: ['skills', 'update'],
|
|
233
|
+
summary: 'Edit a native Skill through its direct or Space target, preserving its ID and supporting files.',
|
|
234
|
+
when_to_use: 'Use the target/version from skills list or skills read, dry-run, then reuse the same request key and bytes to save or recover a lost reply. The same edit file works for a standalone or a Space target; an optional links key changes where the Skill is listed in the same transaction.',
|
|
235
|
+
args_schema: { arguments: [{ token: '<file>', key: 'file', description: 'JSON with direct or Space target, version, skill_md or base64 files, optional name/description and links {add,remove}.' }],
|
|
236
|
+
options: [{ flags: '--request-id <key>', description: 'Stable edit intent key; reuse after lost replies.' },
|
|
237
|
+
{ flags: '--dry-run', description: 'Check current access, content and revisions without uploading or saving.' }] },
|
|
238
|
+
examples: ['notis skills update ./skill-edit.json --request-id edit-1 --dry-run'],
|
|
239
|
+
mutates: true, idempotent: true, require_auth: true,
|
|
240
|
+
backend_call: { type: 'http', path: '/portal_skills/native-authoring' },
|
|
241
|
+
handler(ctx) {
|
|
242
|
+
const payload = parseJson(readFileSync(resolve(ctx.args.file), 'utf8'), 'Skill edit file');
|
|
243
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)
|
|
244
|
+
|| Object.keys(payload).some(key => !['target', 'version', 'skill_md', 'files', 'name', 'description', 'links'].includes(key))) {
|
|
245
|
+
throw usageError('Use an exact target, version, skill_md or files, and optional links; never override ownership.');
|
|
246
|
+
}
|
|
247
|
+
skillTarget(payload.target, true);
|
|
248
|
+
if ('links' in payload && (!payload.links || typeof payload.links !== 'object' || Array.isArray(payload.links)
|
|
249
|
+
|| !Object.keys(payload.links).length || Object.keys(payload.links).some(key => !['add', 'remove'].includes(key)))) {
|
|
250
|
+
throw usageError('links must be an object with add and/or remove arrays.');
|
|
251
|
+
}
|
|
252
|
+
return nativeSkillAuthoring(ctx, { ...payload, operation: 'update', request_id: requestKey(ctx),
|
|
253
|
+
dry_run: Boolean(ctx.options.dryRun) });
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
command_path: ['skills', 'links'],
|
|
258
|
+
summary: 'Add, remove or move a Skill between Spaces; the Skill keeps its ID.',
|
|
259
|
+
when_to_use: 'Change where a Skill is listed without editing its content. A move is one add plus one remove in one transaction. You must be an Editor of every Space you touch and able to edit the Skill; removing its last available link keeps it as your own standalone Skill.',
|
|
260
|
+
args_schema: { arguments: [{ token: '<skill-id>', key: 'skillId', description: 'Exact Skill ID from skills list.' }], options: [
|
|
261
|
+
{ flags: '--add <json>', description: 'JSON array of {space_id, space_revision, alias?} links to add; alias defaults to the Skill name.' },
|
|
262
|
+
{ flags: '--remove <json>', description: 'JSON array of {binding_id, binding_revision} links to remove, from skills list.' },
|
|
263
|
+
{ flags: '--request-id <key>', description: 'Stable intent key; reuse it unchanged when retrying this change.' },
|
|
264
|
+
] },
|
|
265
|
+
examples: ['notis skills links <skill-id> --add \'[{"space_id":"<space-id>","space_revision":3}]\' --request-id link-1',
|
|
266
|
+
'notis skills links <skill-id> --add \'[{"space_id":"<to-space-id>","space_revision":3}]\' --remove \'[{"binding_id":"<binding-id>","binding_revision":2}]\' --request-id move-1'],
|
|
267
|
+
mutates: true, idempotent: true, require_auth: true,
|
|
268
|
+
backend_call: { type: 'http', path: '/portal_spaces/resource-links' },
|
|
269
|
+
async handler(ctx) {
|
|
270
|
+
const links = {};
|
|
271
|
+
if (ctx.options.add !== undefined) links.add = linkEntries(ctx.options.add, '--add', ['space_id', 'space_revision'], ['alias']);
|
|
272
|
+
if (ctx.options.remove !== undefined) links.remove = linkEntries(ctx.options.remove, '--remove', ['binding_id', 'binding_revision']);
|
|
273
|
+
if (!Object.keys(links).length) throw usageError('Pass --add and/or --remove.');
|
|
274
|
+
const body = { kind: 'skill', resource_id: ctx.args.skillId, links, request_id: requestKey(ctx) };
|
|
275
|
+
const result = await httpRequest({ runtime: ctx.runtime, method: 'POST', path: '/portal_spaces/resource-links', body, spacesProtocol: 1 });
|
|
276
|
+
return ctx.output.emitSuccess({ command: 'skills links', data: result.payload,
|
|
277
|
+
humanSummary: linkChangeSummary(result.payload), meta: { mutating: true, request_id: result.requestId } });
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
command_path: ['skills', 'sync'],
|
|
282
|
+
summary: 'Synchronize account skills and keep the three Notis base skills current.',
|
|
283
|
+
when_to_use:
|
|
284
|
+
'Run manually whenever local agent skills should be reconciled. Manual runs ignore the Desktop automatic-sync preference.',
|
|
285
|
+
args_schema: {
|
|
286
|
+
arguments: [],
|
|
287
|
+
options: [
|
|
288
|
+
{
|
|
289
|
+
flags: '--electron-repeat',
|
|
290
|
+
description: 'Honor the automatic Desktop sync preference (used by Notis Desktop).',
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
examples: ['notis skills sync', 'notis skills sync --json'],
|
|
295
|
+
output_schema:
|
|
296
|
+
'Returns account sync counts plus baseSkills, baseInstalled, baseLinked, and baseBackups.',
|
|
297
|
+
mutates: true,
|
|
298
|
+
idempotent: true,
|
|
299
|
+
require_auth: true,
|
|
300
|
+
related_commands: ['notis login', 'notis start', 'notis doctor'],
|
|
301
|
+
backend_call: { type: 'local', name: 'skill_sync' },
|
|
302
|
+
handler: syncSkillsHandler,
|
|
303
|
+
},
|
|
304
|
+
];
|