@hasna/skills 0.3.0 → 0.5.0

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 (55) hide show
  1. package/README.md +269 -14
  2. package/bin/index.js +7835 -5440
  3. package/bin/mcp.js +2040 -589
  4. package/bin/migrate.js +148 -40
  5. package/bin/server.js +66 -87
  6. package/bin/worker.js +42 -75
  7. package/dist/admin-contract.d.ts +37 -19
  8. package/dist/admin-contract.js +1 -1
  9. package/dist/cli/cli.test-utils.d.ts +10 -8
  10. package/dist/cli/commands/customer-profile.d.ts +2 -0
  11. package/dist/cli/commands/customer-verification.d.ts +5 -0
  12. package/dist/cli/commands/remote-account.d.ts +7 -0
  13. package/dist/cli/commands/tool-primitives.d.ts +1 -1
  14. package/dist/cli/commands/workspace-member-mutations.d.ts +2 -0
  15. package/dist/cli/commands/workspace-members.d.ts +2 -0
  16. package/dist/cli/env-assignment.d.ts +9 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.js +1369 -349
  19. package/dist/lib/agent-sync.d.ts +13 -8
  20. package/dist/lib/api-url.d.ts +4 -3
  21. package/dist/lib/app-home.d.ts +0 -1
  22. package/dist/lib/auth-store.d.ts +1 -1
  23. package/dist/lib/client-types.d.ts +75 -0
  24. package/dist/lib/credential-state.d.ts +12 -0
  25. package/dist/lib/fleet-credentials.d.ts +49 -17
  26. package/dist/lib/home-adoption.d.ts +2 -0
  27. package/dist/lib/home-census.d.ts +3 -1
  28. package/dist/lib/instance-credentials.d.ts +13 -0
  29. package/dist/lib/local-opt-in.d.ts +24 -0
  30. package/dist/lib/mcp-contracts.d.ts +4 -0
  31. package/dist/lib/portable-skills-files.d.ts +10 -2
  32. package/dist/lib/portable-skills-types.d.ts +2 -0
  33. package/dist/lib/read-access.d.ts +83 -0
  34. package/dist/lib/remote-account.d.ts +42 -0
  35. package/dist/lib/remote-auth.d.ts +46 -0
  36. package/dist/lib/remote-client.d.ts +90 -6
  37. package/dist/lib/remote-customer-operations.d.ts +106 -0
  38. package/dist/lib/remote-files.d.ts +21 -0
  39. package/dist/lib/remote-profile.d.ts +26 -0
  40. package/dist/lib/remote-registry.d.ts +7 -3
  41. package/dist/lib/remote-workspace.d.ts +76 -0
  42. package/dist/lib/run-routing.d.ts +1 -0
  43. package/dist/lib/run-state.d.ts +3 -0
  44. package/dist/lib/skillinfo.d.ts +1 -1
  45. package/dist/mcp/helpers.d.ts +22 -0
  46. package/dist/mcp/index.d.ts +16 -0
  47. package/dist/mcp/remote-customer-tools.d.ts +2 -0
  48. package/dist/sdk/governance-store.d.ts +1 -0
  49. package/dist/sdk/index.d.ts +8 -1
  50. package/dist/sdk/index.js +1994 -416
  51. package/dist/sdk/outputs.d.ts +0 -11
  52. package/dist/sdk/runs.d.ts +5 -5
  53. package/dist/storage.js +6 -40
  54. package/docs/skill-standard.md +30 -2
  55. package/package.json +7 -6
@@ -29,9 +29,9 @@ export declare const SYNC_AGENTS: readonly SyncAgent[];
29
29
  */
30
30
  export declare const SKILLS_SOURCE_ENV = "SKILLS_SOURCE";
31
31
  /**
32
- * Ownership marker written beside every SKILL.md this tool syncs. Its presence is how a
33
- * re-sync tells "a skill I wrote, safe to update" from "a skill the user hand-authored,
34
- * do not touch". A hidden sidecar rather than a frontmatter field so the SKILL.md the
32
+ * Ownership marker written beside every SKILL.md this tool syncs. Its exact managedBy
33
+ * value tells a re-sync which directories this tool owns; a foreign or malformed
34
+ * marker grants no authority. A hidden sidecar rather than a frontmatter field so the SKILL.md the
35
35
  * agent loads stays exactly the adapted document and nothing else.
36
36
  */
37
37
  export declare const SYNC_MARKER_FILE = ".hasna-skills.json";
@@ -42,6 +42,10 @@ export interface SyncMarker {
42
42
  source: string;
43
43
  syncedAt: string;
44
44
  }
45
+ /** Internal shared ownership predicate; not exported by the public package entrypoint. */
46
+ export declare function isSkillsOwnershipMarker(marker: unknown): marker is Record<string, unknown>;
47
+ /** Read only a regular ownership sidecar. */
48
+ export declare function hasSkillsOwnershipMarker(dir: string): boolean;
45
49
  export declare function isSyncAgent(value: string): value is SyncAgent;
46
50
  export declare function resolveSyncAgents(arg?: string): SyncAgent[];
47
51
  /** The global skills directory for an agent, honouring a test-supplied home. */
@@ -142,12 +146,12 @@ export interface WriteManagedAgentSkillParams {
142
146
  /**
143
147
  * Write one skill into one agent's global folder, non-clobbering.
144
148
  *
145
- * A directory this tool has written before carries the marker file and is replaced with
149
+ * A directory this tool has written before carries our exact ownership marker and is replaced with
146
150
  * an exact mirror — except that a managed home holding full content is never silently
147
151
  * replaced with an executable pointer stub (that would be data loss; it is refused
148
- * unless `force` is passed). A directory with a SKILL.md but no marker is the user's
149
- * own skill and is skipped unless `force` explicitly adopts it. Any other pre-existing
150
- * unmarked directory is always left untouched. A fresh directory is created.
152
+ * unless `force` is passed). A directory with a SKILL.md but no valid Skills marker is
153
+ * skipped unless `force` explicitly adopts it. Any other pre-existing unmanaged
154
+ * directory is always left untouched. A fresh directory is created.
151
155
  */
152
156
  export declare function writeManagedAgentSkill(params: WriteManagedAgentSkillParams): AgentSyncAction;
153
157
  export interface ManagedDirWriteResult {
@@ -173,6 +177,7 @@ export interface ManagedDirWriteOptions {
173
177
  export declare function writeManagedSkillDir(dir: string, skillMd: string, options: ManagedDirWriteOptions): ManagedDirWriteResult;
174
178
  /**
175
179
  * Remove a skill this tool synced from an agent folder. Refuses to delete a directory it
176
- * did not write (no marker), so it can never remove a user's hand-authored skill.
180
+ * did not write (no valid Skills ownership marker). Foreign or malformed markers
181
+ * grant no deletion authority, and removal has no force override.
177
182
  */
178
183
  export declare function removeManagedAgentSkill(skill: string, agent: SyncAgent, homeDir?: string): boolean;
@@ -5,9 +5,10 @@
5
5
  * thin reading of `@hasna/contracts/client`. It exists only so the two failure
6
6
  * MODES stay named:
7
7
  *
8
- * - Read paths fail closed: `resolveApiUrl()` returns `undefined` when nothing
9
- * is configured, and the caller keeps working against the bundled local
10
- * registry.
8
+ * - Read paths fail closed: `resolveApiUrl()` returns `undefined` only under
9
+ * the explicit local opt-in (`HASNA_SKILLS_LOCAL=1`, alias `SKILLS_LOCAL=1`),
10
+ * and the caller keeps working against the bundled local registry. Without
11
+ * the opt-in, an install with nothing configured is a refusal, not a URL.
11
12
  * - Auth and write paths fail loudly: `requireApiUrl()` throws an error naming
12
13
  * the missing configuration.
13
14
  *
@@ -17,7 +17,6 @@
17
17
  * Nothing moves on disk in this phase — the package just resolves the new
18
18
  * paths.
19
19
  */
20
- export type PathKind = "config" | "data" | "state" | "cache";
21
20
  export interface PathsResolverOptions {
22
21
  app: string;
23
22
  internal?: boolean;
@@ -87,7 +87,7 @@ export declare function getAuthConfigReadOnly(env?: Env, options?: SkillsFleetOp
87
87
  * Returns the file it wrote, so the CLI can name the real path rather than a
88
88
  * path it assumed.
89
89
  */
90
- export declare function saveAuthConfig(config: StoredAuthConfig, env?: Env): string;
90
+ export declare function saveAuthConfig(config: StoredAuthConfig, env?: Env, authenticatedOrigin?: string): string;
91
91
  /** Store (or clear, with null) the API URL beside the credential. */
92
92
  export declare function saveApiUrl(apiUrl: string | null, env?: Env): string;
93
93
  /** The API URL recorded in the credentials file, or null. */
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The @hasna/contracts client types that cross this package's published
3
+ * boundary, spelled locally.
4
+ *
5
+ * WHY THIS FILE EXISTS. `bun build --target bun` inlines @hasna/contracts into
6
+ * every shipped bundle, so the runtime never needs it installed alongside
7
+ * @hasna/skills and it is correctly a build-time devDependency. `tsc
8
+ * --emitDeclarationOnly` inlines nothing: the moment a PUBLIC declaration names
9
+ * a type from `@hasna/contracts/client`, the published `.d.ts` imports it, and
10
+ * a consumer who installed only this package's declared runtime dependencies
11
+ * fails type-checking (7 x TS2307 before #1782 fixed the same shape in
12
+ * @hasna/secrets).
13
+ *
14
+ * This file is NOT the vendored resolver returning: it has no imports, no
15
+ * runtime statement, no tier, no Keychain read, no URL ladder — only the
16
+ * shapes, copied from the @hasna/contracts/@1.0.2 declarations. Its sibling
17
+ * `client-types.test.ts` asserts each one is mutually assignable with the real
18
+ * declaration, in the direction it actually crosses, so a drifted shape fails
19
+ * `tsc` in the same build step that emits the declarations it protects.
20
+ *
21
+ * `src/lib/fleet-credentials.ts` keeps importing the resolver and its VALUE
22
+ * exports from @hasna/contracts/client (inlined at runtime); only the types it
23
+ * re-exports or names in its own public signatures come from here.
24
+ *
25
+ * Do not add an import. Do not add a value. This file must stay a leaf.
26
+ */
27
+ /** Which link of the chain supplied the credential. */
28
+ export type CredentialTier = "argument" | "override" | "pointer" | "profile" | "keychain" | "disk" | "env";
29
+ export interface ResolvedCredential {
30
+ /** The secret. Property access and destructuring work; it is never enumerated or serialized. */
31
+ readonly apiKey: string;
32
+ readonly tier: CredentialTier;
33
+ /** Where it came from: an env key NAME, an absolute file path, or a Keychain item reference. Never a value. */
34
+ readonly source: string;
35
+ /** True for tiers an operator sets on purpose. These never fall through. */
36
+ readonly deliberate: boolean;
37
+ /** When tier === "pointer", the vault ITEM KEY to resolve at request time. Never a credential value. */
38
+ readonly pointerVaultKey?: string;
39
+ /** The disk paths consulted before this credential was chosen. */
40
+ readonly diskCandidates: readonly string[];
41
+ /** Human-readable advisory. Never contains key material. */
42
+ readonly warning: string | null;
43
+ }
44
+ /** The captured outcome of one `security` invocation. `stdout` IS the secret; it is never logged. */
45
+ export interface KeychainCommandResult {
46
+ /** Exit status; null when the tool could not be started or was killed. */
47
+ status: number | null;
48
+ stdout: string;
49
+ stderr: string;
50
+ }
51
+ /** Runs `/usr/bin/security` with the given argv — no shell. Injected by tests. */
52
+ export type KeychainCommandRunner = (argv: readonly string[]) => KeychainCommandResult;
53
+ /** Tier 3 controls. Every field is optional; production callers pass nothing. */
54
+ export interface KeychainTierOptions {
55
+ /**
56
+ * Whether the Keychain is consulted for a caller-built env object. The tier
57
+ * is AMBIENT: by default it runs only for the live `process.env`. Injecting
58
+ * `run` implies `true`.
59
+ */
60
+ enabled?: boolean;
61
+ /** Defaults to `process.platform`; the tier exists only on `"darwin"`. */
62
+ platform?: string;
63
+ /** The machine's host name, used as the account when `HASNA_STATION` is unset. */
64
+ hostname?: () => string;
65
+ /** The `security` runner. Defaults to spawning `/usr/bin/security` by argv. */
66
+ run?: KeychainCommandRunner;
67
+ }
68
+ export interface CredentialChainOptions {
69
+ /** Tier 1: an explicit key, e.g. from `--api-key`. */
70
+ apiKey?: string;
71
+ /** Tier 1: an explicit profile name, e.g. from `--profile`. Beats `HASNA_PROFILE`. */
72
+ profile?: string;
73
+ /** Tier 3: Keychain controls — a fake `security` runner in tests, an opt-out on CI. */
74
+ keychain?: KeychainTierOptions;
75
+ }
@@ -0,0 +1,12 @@
1
+ export interface CredentialState {
2
+ /** `misconfigured` is the ladder's refusal carried as data; `error` says why. */
3
+ mode: "hosted" | "local" | "misconfigured";
4
+ apiUrl: string | null;
5
+ apiUrlSource: string | null;
6
+ apiKeySource: string | null;
7
+ apiKeyTier: string | null;
8
+ credentialsFile: string | null;
9
+ credentialsFileMode: string | null;
10
+ error: string | null;
11
+ }
12
+ export declare function describeCredentialState(): CredentialState;
@@ -36,8 +36,14 @@
36
36
  * are not read anywhere else in this package. `SKILL_API_KEY` (singular) is
37
37
  * gone: it shadowed nothing canonical and was never documented.
38
38
  *
39
- * THREE OUTCOMES, and no fourth:
39
+ * THIRD: the local run is opt-in only (owner ruling 2026-09-04, hasna/apps#1720;
40
+ * class-patch order 2026-09-06). `HASNA_SKILLS_LOCAL=1` (alias `SKILLS_LOCAL=1`)
41
+ * selects the on-machine run when the ENVIRONMENT configures no authority; it is
42
+ * answered before the resolver runs, so opting in never reads the Keychain or
43
+ * the credentials file. A configured environment always outranks it. The
44
+ * outcomes, and no fourth:
40
45
  *
46
+ * - the local opt-in selected → LOCAL, announced once on stderr.
41
47
  * - a credential resolves → HOSTED. The authority is the configured
42
48
  * URL, else the fleet gateway. A credential
43
49
  * that cannot produce a usable key — a
@@ -51,14 +57,18 @@
51
57
  * There is no local fallback here: serving
52
58
  * local results while authentication is
53
59
  * unconfigured is a false green.
54
- * - neither a credential nor a URL LOCAL. Skills is an OSS tool with a
55
- * bundled corpus, so running on this
56
- * machine is a real mode — and it says so,
57
- * once, on stderr.
60
+ * - neither a credential nor a URL, and no opt-in
61
+ * LOUD failure, exit non-zero, no SQLite,
62
+ * no *-local-fallback event. Running on
63
+ * this machine is a deliberate choice now,
64
+ * not the silence that follows a missing
65
+ * credential.
58
66
  */
59
- import { type CredentialChainOptions, type CredentialTier, type KeychainTierOptions, type ResolvedCredential } from "@hasna/contracts/client";
67
+ import type { CredentialChainOptions, CredentialTier, KeychainTierOptions, ResolvedCredential } from "./client-types.js";
60
68
  /** The app slug: the Keychain service, the `~/.hasna/<app>` folder, the gateway path. */
61
69
  export declare const SKILLS_APP = "skills";
70
+ /** The deliberate unhosted opt-in env names. Re-exported for the surfaces that have to name them. */
71
+ export { SKILLS_LOCAL_OPT_IN_ENV_KEYS, isSkillsLocalOptIn, selectsSkillsLocalMode } from "./local-opt-in.js";
62
72
  type Env = Record<string, string | undefined>;
63
73
  /** `HASNA_SKILLS_API_URL`, then the accepted `SKILLS_API_URL` alias. */
64
74
  export declare const SKILLS_API_URL_ENV_KEYS: readonly string[];
@@ -114,7 +124,7 @@ export interface LocalSkillsFleet {
114
124
  }
115
125
  export type SkillsFleet = HostedSkillsFleet | LocalSkillsFleet;
116
126
  /** Machine-readable reasons a hosted resolution was refused. */
117
- export type SkillsFleetErrorCode = "MISSING_API_CREDENTIAL" | "INVALID_API_URL";
127
+ export type SkillsFleetErrorCode = "MISSING_API_CREDENTIAL" | "INVALID_API_URL" | "INSTANCE_CREDENTIAL_MISMATCH";
118
128
  /**
119
129
  * A configured install could not produce a usable hosted client.
120
130
  *
@@ -129,6 +139,14 @@ export declare class SkillsFleetCredentialError extends Error {
129
139
  readonly code: SkillsFleetErrorCode;
130
140
  constructor(message: string, code?: SkillsFleetErrorCode);
131
141
  }
142
+ /**
143
+ * True for this package's own refusal, across bundle boundaries.
144
+ *
145
+ * Exported for the surfaces that turn the refusal into data (an MCP tool's
146
+ * `AUTH_REQUIRED` result, a CLI handler's one-line stderr exit) so they match
147
+ * on the error's NAME rather than on a class identity a bundle may not share.
148
+ */
149
+ export declare function isSkillsFleetCredentialError(error: unknown): error is SkillsFleetCredentialError;
132
150
  /**
133
151
  * Normalize a configured Skills authority to the origin the client dials.
134
152
  *
@@ -151,7 +169,7 @@ export interface ConfiguredSkillsApiUrl {
151
169
  * default apply for a credentialled install and what keeps an install with no
152
170
  * credential from naming a host at all.
153
171
  */
154
- export declare function configuredSkillsApiUrl(env?: Env, keychain?: KeychainTierOptions): ConfiguredSkillsApiUrl | null;
172
+ export declare function configuredSkillsApiUrl(env?: Env, keychain?: KeychainTierOptions, profile?: string): ConfiguredSkillsApiUrl | null;
155
173
  /** The credential file paths consulted, for a message that has to name them. */
156
174
  export declare function skillsCredentialFiles(env?: Env): string[];
157
175
  /**
@@ -164,10 +182,12 @@ export declare function skillsCredentialFilePath(env?: Env): string;
164
182
  /**
165
183
  * Say — once per process, on stderr — that this install is running locally.
166
184
  *
167
- * Local mode is legitimate for Skills: the corpus ships in the package. It is
168
- * still announced, because "no credential resolved" and "deliberately offline"
169
- * look identical in the output otherwise, and the first one is usually a
170
- * misconfiguration the operator wants to hear about.
185
+ * Local mode is legitimate for Skills: the corpus ships in the package. It is a
186
+ * deliberate choice now, though: it is reachable only through the explicit
187
+ * opt-in (`HASNA_SKILLS_LOCAL=1`), and it is still announced, because "no
188
+ * credential resolved" and "deliberately offline" look identical in the output
189
+ * otherwise, and the first one is usually a misconfiguration the operator wants
190
+ * to hear about.
171
191
  */
172
192
  export declare function noticeLocalSkillsMode(write?: (line: string) => void): void;
173
193
  /** Test seam: forget that the local-mode line was printed. */
@@ -193,22 +213,29 @@ export declare function resolveSkillsFleet(env?: Env, options?: SkillsFleetOptio
193
213
  * when a credential is configured and cannot be produced — never a fallback.
194
214
  */
195
215
  export declare function resolveSkillsApiKey(env?: Env, options?: SkillsFleetOptions): Promise<string | null>;
216
+ /** Resolve the URL and credential once, including any asynchronous vault lookup. */
217
+ export declare function resolveSkillsConnection(env?: Env, options?: SkillsFleetOptions): Promise<(HostedSkillsFleet & {
218
+ apiKey: string;
219
+ }) | null>;
196
220
  /** The usable API key, or throw naming what is missing. Use on every send path. */
197
221
  export declare function requireSkillsApiKey(action?: string, env?: Env, options?: SkillsFleetOptions): Promise<string>;
198
222
  /**
199
223
  * The credential for a surface that reports refusals as data (an MCP tool, a
200
224
  * `--json` command) rather than as an exception.
201
225
  *
202
- * `reason` is the ladder's own message when an authority is configured and no
203
- * key resolved a refusal carried as a value, NOT a fallback: the caller must
204
- * still refuse. It is null only when nothing at all is configured, which is the
205
- * ordinary "not signed in" case.
226
+ * `reason` is the ladder's own message when a hosted resolution is refused an
227
+ * authority with no key, or an unconfigured install without the local opt-in
228
+ * (fail-closed ruling) a refusal carried as a value, NOT a fallback: the
229
+ * caller must still refuse. It is null only when the explicit local opt-in
230
+ * selected the on-machine run, the ordinary "not signed in" case.
206
231
  */
207
232
  export declare function skillsCredentialOrReason(env?: Env, options?: SkillsFleetOptions): Promise<{
208
233
  apiKey: string;
234
+ apiOrigin: string;
209
235
  reason: null;
210
236
  } | {
211
237
  apiKey: null;
238
+ apiOrigin: null;
212
239
  reason: string | null;
213
240
  }>;
214
241
  /**
@@ -219,6 +246,12 @@ export declare function skillsCredentialOrReason(env?: Env, options?: SkillsFlee
219
246
  * Keychain, credentials file), else the authority a resolved credential implies.
220
247
  * With neither, this returns null and the caller fails loudly: R1 still holds,
221
248
  * an install that named no service does not get to send an email address to one.
249
+ *
250
+ * The fail-closed refusal an unconfigured install now raises on every DATA
251
+ * surface is swallowed here and reported as plain "no authority": for a flow
252
+ * whose purpose is to acquire a credential, "nothing is configured" and "local
253
+ * mode is opted in" have the same two-step way out (configure an origin, then
254
+ * sign in).
222
255
  */
223
256
  export declare function resolveSkillsApiOrigin(env?: Env, options?: SkillsFleetOptions): {
224
257
  origin: string;
@@ -243,4 +276,3 @@ export declare class MissingSkillsFleetError extends Error {
243
276
  readonly code = "MISSING_API_URL";
244
277
  constructor(action?: string);
245
278
  }
246
- export {};
@@ -30,6 +30,8 @@ export interface AdoptionScan {
30
30
  }
31
31
  export interface AdoptionOptions extends PortableSkillOptions {
32
32
  agents?: SyncAgent[];
33
+ /** Optional selected home/corpus names; omitted means all names. */
34
+ names?: string[];
33
35
  /** Write markers and the conflicts ledger. Without it, scan only. */
34
36
  apply?: boolean;
35
37
  }
@@ -23,4 +23,6 @@ export interface DriftCensus {
23
23
  managed: number;
24
24
  clean: boolean;
25
25
  }
26
- export declare function censusHomeDrift(options?: AdoptionOptions): DriftCensus;
26
+ export declare function censusHomeDrift(options?: AdoptionOptions & {
27
+ names?: string[];
28
+ }): DriftCensus;
@@ -0,0 +1,13 @@
1
+ export declare const SKILLS_BOUND_API_URL = "HASNA_SKILLS_BOUND_API_URL";
2
+ type Env = Record<string, string | undefined>;
3
+ /** Match the shared resolver's profile syntax before using its path helper. */
4
+ export declare function selectedSkillsProfile(env: Env, explicit?: string): string | null;
5
+ export declare function skillsProfileCredentialFiles(env: Env, explicit?: string): string[];
6
+ /** Fence the released credential reader and this package's routing reads together. */
7
+ export declare function captureSkillsCredentialFiles(files: string[]): () => void;
8
+ /** Read only known non-secret routing fields, never export an API key or an arbitrary field. */
9
+ export declare function readSkillsInstanceMetadata(file: string): {
10
+ apiUrl?: string;
11
+ binding?: string;
12
+ };
13
+ export {};
@@ -0,0 +1,24 @@
1
+ /** The deliberate unhosted opt-in, canonical name first. */
2
+ export declare const SKILLS_LOCAL_OPT_IN_ENV_KEYS: readonly ["HASNA_SKILLS_LOCAL", "SKILLS_LOCAL"];
3
+ export type SkillsLocalOptInEnv = Record<string, string | undefined>;
4
+ /** True when the operator deliberately asked for the unhosted on-machine run. */
5
+ export declare function isSkillsLocalOptIn(env?: SkillsLocalOptInEnv): boolean;
6
+ /** Every env name that can configure a Skills authority or credential, resolver-derived. */
7
+ export declare function skillsAuthorityEnvKeys(): string[];
8
+ /**
9
+ * Does the ENVIRONMENT itself configure a Skills authority or credential?
10
+ *
11
+ * Deliberately narrower than "does a credential resolve": answering it must not
12
+ * touch the Keychain or the filesystem, because doing so would defeat the
13
+ * isolation the opt-in short-circuit exists to provide. It reads the env
14
+ * dictionary and nothing else.
15
+ *
16
+ * A DECLARED-BUT-BLANK variable counts as absent HERE — a blank has always been
17
+ * this package's spelling for "not configured", and helpers in the wild blank
18
+ * rather than delete. It is NOT absent once we do go hosted: the resolver
19
+ * refuses a blank loudly rather than falling through to another identity, which
20
+ * is the behaviour that matters at that point.
21
+ */
22
+ export declare function hasSkillsEnvAuthorityIntent(env?: SkillsLocalOptInEnv): boolean;
23
+ /** True when this environment should be served by the on-box local run. */
24
+ export declare function selectsSkillsLocalMode(env?: SkillsLocalOptInEnv): boolean;
@@ -9,6 +9,10 @@ export interface JsonSchemaObject {
9
9
  default?: unknown;
10
10
  format?: string;
11
11
  minimum?: number;
12
+ maximum?: number;
13
+ pattern?: string;
14
+ maxItems?: number;
15
+ maxLength?: number;
12
16
  items?: JsonSchemaObject;
13
17
  properties?: Record<string, JsonSchemaObject>;
14
18
  required?: string[];
@@ -9,7 +9,11 @@ export declare function defaultRuntimeContract(entrypoint?: string): PortableSki
9
9
  /** Provenance defaults; content_hash is filled at write time over the bundle. */
10
10
  export declare function defaultProvenance(sourceCommit?: string): PortableSkillProvenance;
11
11
  export declare function normalizePortableSkillName(name: string): string;
12
+ /** New local identities use hyphens; legacy lookups and command names keep their grammar. */
13
+ export declare function normalizeNewPortableSkillName(name: string): string;
12
14
  export declare function readPortableSkillManifest(skillPath: string, fallbackName?: string): PortableSkillManifest;
15
+ /** Import needs the original spelling before the legacy reader discards case boundaries. */
16
+ export declare function readPortableSkillManifestForImport(skillPath: string): PortableSkillManifest;
13
17
  export declare function parseSkillKind(value: string | undefined): SkillKind | undefined;
14
18
  /**
15
19
  * The version a skill EXPLICITLY declares, or undefined when none of the sources do:
@@ -22,10 +26,14 @@ export declare function parseSkillKind(value: string | undefined): SkillKind | u
22
26
  export declare function readDeclaredSkillVersion(skillPath: string): string | undefined;
23
27
  export declare function createInstructionManifest(name: string, options: {
24
28
  description: string;
29
+ category?: string;
30
+ tags?: string[];
25
31
  }): PortableSkillManifest;
26
32
  export declare function writeInstructionSkillTemplate(skillPath: string, manifest: PortableSkillManifest): void;
27
33
  export declare function createPortableManifest(name: string, options: {
28
34
  description: string;
35
+ category?: string;
36
+ tags?: string[];
29
37
  }): PortableSkillManifest;
30
38
  export declare function writePortableSkillTemplate(skillPath: string, manifest: PortableSkillManifest): void;
31
39
  /**
@@ -42,8 +50,8 @@ export declare function ensurePortableSkillFiles(skillPath: string, manifest: Po
42
50
  /**
43
51
  * Instruction (prose) skills are consumed by agent renderers/MCP docs, not run
44
52
  * locally, so `port` must never fabricate executable stubs (package.json, bin,
45
- * src/index.ts, tsconfig.json, AGENTS.md). It keeps the copied SKILL.md verbatim
46
- * and writes a minimal skill.json declaring `kind: "instruction"`.
53
+ * src/index.ts, tsconfig.json, AGENTS.md). Apart from aligning declared names
54
+ * after an import rename, it preserves the copied prose and metadata.
47
55
  */
48
56
  export declare function ensureInstructionSkillFiles(skillPath: string, manifest: PortableSkillManifest): PortableSkillManifest;
49
57
  export declare function copySkillDirectory(source: string, destination: string): void;
@@ -98,6 +98,8 @@ export interface PortableSkillOptions {
98
98
  }
99
99
  export interface ScaffoldPortableSkillOptions extends PortableSkillOptions {
100
100
  description?: string;
101
+ category?: string;
102
+ tags?: string[];
101
103
  overwrite?: boolean;
102
104
  kind?: SkillKind;
103
105
  }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The gate every DATA surface runs before it answers from the bundled corpus,
3
+ * and the one registry those surfaces show.
4
+ *
5
+ * `skills list` has resolved through the fleet ladder since the fail-closed
6
+ * ruling (owner directive 2026-09-04, hasna/apps#1720): with no credential, no
7
+ * authority and no `HASNA_SKILLS_LOCAL=1` opt-in it exits 1 naming the refusal,
8
+ * opens nothing and serves nothing. `skills info` / `docs` / `requires`, the
9
+ * bare non-TTY listing and the MCP discovery tools (`list_skills`,
10
+ * `search_skills`, `get_skill_info`, `get_skill_docs`, `list_categories`,
11
+ * `list_tags`, `get_requirements`) did NOT — they read `loadRegistry()` and
12
+ * answered from the bundled catalog plus `~/.hasna/skills/installed` at exit 0
13
+ * with no notice, on the very machine `skills list` had just refused (#1720
14
+ * validation, round 1). Two surfaces that disagree about whether an install is
15
+ * configured is the false green the ruling removed, so they now share this one
16
+ * gate with the browsing commands.
17
+ *
18
+ * - refused (nothing configured, no opt-in; an authority with no key; a
19
+ * deliberate selection that cannot be honoured) → `SkillsFleetCredentialError`
20
+ * from the shared ladder, carried to the caller unchanged: the CLI prints
21
+ * its one line and exits 1, an MCP tool answers `AUTH_REQUIRED`.
22
+ * - the explicit local opt-in → `{ mode: "local" }`; the ladder has announced
23
+ * local mode once on stderr, and the caller serves the on-machine answer.
24
+ * - a credential resolves → `{ mode: "hosted", apiOrigin }`. The key is
25
+ * deliberately NOT returned here: a surface that only reads local data has
26
+ * no business holding it, and the surfaces that send it resolve it again
27
+ * at request time (`remoteRequestHeaders`, per call, completing a vault
28
+ * pointer if that is the tier).
29
+ *
30
+ * Resolved fresh on every call — the resolver contract — so a credential that
31
+ * appears, rotates or disappears mid-process is honoured by the next call.
32
+ */
33
+ import { type SkillsFleetOptions } from "./fleet-credentials.js";
34
+ import { type SkillMeta } from "./registry.js";
35
+ type Env = Record<string, string | undefined>;
36
+ /** Where a read surface stands, once the ladder has let it through. Never carries a key. */
37
+ export type SkillsReadAccess = {
38
+ mode: "hosted";
39
+ apiOrigin: string;
40
+ } | {
41
+ mode: "local";
42
+ };
43
+ /**
44
+ * Run the fail-closed routing preamble for a read surface.
45
+ *
46
+ * Throws {@link SkillsFleetCredentialError} when the ladder refuses; the
47
+ * caller must let that refusal out (as a non-zero exit or a structured error),
48
+ * never swallow it into a local answer.
49
+ */
50
+ export declare function requireSkillsReadAccess(env?: Env, options?: SkillsFleetOptions): Promise<SkillsReadAccess>;
51
+ /**
52
+ * The registry a browsing surface should show — the CLI's `list` / `search` /
53
+ * `categories` / `tags` and the MCP discovery tools, from ONE implementation.
54
+ *
55
+ * The default read path is folder UNION cloud: whenever the install is pointed
56
+ * at a hosted instance (a resolved credential, and HASNA_SKILLS_API_URL for your
57
+ * own instance) the authenticated remote registry joins the local listing even
58
+ * without `--remote`. The explicit local opt-in keeps today's exact local
59
+ * output; an unconfigured or auth-missing install is a refusal, thrown from the
60
+ * shared ladder (fail-closed R1 — see mergeRemoteRegistry()).
61
+ *
62
+ * `--remote` used to REPLACE the local registry: `skills list --remote` returned exactly
63
+ * what the instance served and nothing else, so the bundled corpus and every skill the
64
+ * operator had written locally disappeared from the listing the moment they pointed the
65
+ * CLI at their own server. It now MERGES, under the precedence documented in
66
+ * src/lib/registry-merge.ts: custom > extension > local > remote > official, "whichever
67
+ * copy this machine would actually use wins the listing".
68
+ *
69
+ * The profile (`all` vs the curated basic set) applies to the local half only. The
70
+ * instance's skills are never filtered by it: the basic profile is a hand-written list of
71
+ * ten bundled names, so applying it to remote entries would drop every published skill
72
+ * from `skills list --remote` - the same disappearance this change exists to fix.
73
+ *
74
+ * A remote failure is still fatal. An explicit `--remote` request (and a configured,
75
+ * authenticated default read) that fails surfaces a clear error, and silently returning
76
+ * the local half of a merge the user asked to include the remote half in would report
77
+ * success for a listing that is missing entries.
78
+ */
79
+ export declare function getBrowseRegistry(options?: {
80
+ all?: boolean;
81
+ remote?: boolean;
82
+ }): Promise<SkillMeta[]>;
83
+ export {};
@@ -0,0 +1,42 @@
1
+ import type { RemoteInputFileDescriptor } from "./remote-files.js";
2
+ /** Optional account APIs supplied by a configured Skills server. No local prices or provider policy. */
3
+ export interface RemoteRunQuote {
4
+ skill: string;
5
+ pricing: {
6
+ costCents: number;
7
+ formattedCost: string;
8
+ [key: string]: unknown;
9
+ };
10
+ [key: string]: unknown;
11
+ }
12
+ export interface RemoteRunApproval {
13
+ /** Preferred public spelling: maximum integer credits approved by the caller. */
14
+ maxCredits?: number;
15
+ /** Maximum integer credits approved by the caller (legacy wire spelling). */
16
+ maxCostCents?: number;
17
+ idempotencyKey?: string;
18
+ inputFiles?: RemoteInputFileDescriptor[];
19
+ }
20
+ export interface RemoteCreditPack {
21
+ id: string;
22
+ credits: number;
23
+ expiresInDays?: number;
24
+ }
25
+ export declare class RemoteCreditApprovalError extends Error {
26
+ readonly requiredCredits: number;
27
+ readonly maximumCredits: number;
28
+ readonly code = "CREDIT_APPROVAL_REQUIRED";
29
+ constructor(requiredCredits: number, maximumCredits: number);
30
+ }
31
+ export declare function creditCount(value: unknown): number;
32
+ export declare function parseRemoteRunQuote(value: unknown): RemoteRunQuote;
33
+ export declare function parseRemoteCreditPacks(value: unknown): RemoteCreditPack[];
34
+ export declare function parseRemoteBillingStatus(value: unknown): {
35
+ hasPaymentMethod?: boolean | undefined;
36
+ plan?: string | undefined;
37
+ creditBalance: number;
38
+ formattedCreditBalance: string;
39
+ };
40
+ export declare function parseRemoteCheckout(value: unknown): {
41
+ url: string;
42
+ };
@@ -0,0 +1,46 @@
1
+ import { type RemoteWorkspaceMembersOptions } from "./remote-workspace.js";
2
+ import { type SetRemoteWorkspaceMemberRole, type RemoveRemoteWorkspaceMember } from "./remote-workspace.js";
3
+ import { type UpdateRemoteProfile, type UpdateRemoteWorkspace } from "./remote-profile.js";
4
+ export declare class HostedApiError extends Error {
5
+ readonly status?: number;
6
+ readonly code?: string;
7
+ readonly detail?: string;
8
+ readonly endpoint?: string;
9
+ readonly apiUrl?: string;
10
+ constructor(message: string, options?: {
11
+ status?: number;
12
+ code?: string;
13
+ detail?: string;
14
+ endpoint?: string;
15
+ apiUrl?: string;
16
+ });
17
+ }
18
+ /** Passwordless auth transport for an explicitly selected instance. It never writes credentials. */
19
+ export declare class RemoteSkillsAuthClient {
20
+ readonly apiOrigin: string;
21
+ constructor(apiUrl: string);
22
+ requestCode(email: string): Promise<any>;
23
+ verifyCode(email: string, code: string): Promise<any>;
24
+ startDevice(): Promise<any>;
25
+ pollDevice(deviceCode: string): Promise<any>;
26
+ private sessionClient;
27
+ createApiKey(email: string, code: string, name: string, scopes?: string[]): Promise<{
28
+ [field: string]: unknown;
29
+ key: string;
30
+ }>;
31
+ listApiKeys(email: string, code: string): Promise<Record<string, unknown>[]>;
32
+ revokeApiKey(email: string, code: string, keyId: string): Promise<Record<string, unknown>>;
33
+ /** Reauthentication is ephemeral: it never replaces a saved key or profile. */
34
+ updateProfile(email: string, code: string, input: UpdateRemoteProfile): Promise<{
35
+ user: import("./remote-profile.js").RemoteCustomerProfile;
36
+ }>;
37
+ updateCurrentWorkspace(email: string, code: string, input: UpdateRemoteWorkspace): Promise<{
38
+ organization: import("./remote-profile.js").RemoteCurrentWorkspace;
39
+ }>;
40
+ /** Fresh owner/admin session; no saved credential or profile is replaced. */
41
+ listWorkspaceMembers(email: string, code: string, options?: RemoteWorkspaceMembersOptions): Promise<import("./remote-workspace.js").RemoteWorkspaceMembersPage>;
42
+ setWorkspaceMemberRole(email: string, code: string, membershipId: string, input: SetRemoteWorkspaceMemberRole): Promise<import("./remote-workspace.js").RemoteWorkspaceMemberRoleResult>;
43
+ removeWorkspaceMember(email: string, code: string, membershipId: string, input: RemoveRemoteWorkspaceMember): Promise<import("./remote-workspace.js").RemoteWorkspaceMemberRemovalResult>;
44
+ /** Common auth transport used by CLI login, preserving the selected instance through awaits. */
45
+ request(path: string, options?: RequestInit): Promise<any>;
46
+ }