@hasna/skills 0.2.0 → 0.3.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.
- package/README.md +100 -36
- package/bin/index.js +2151 -650
- package/bin/mcp.js +1787 -625
- package/bin/migrate.js +5 -0
- package/bin/server.js +365 -198
- package/bin/worker.js +346 -179
- package/dist/cli/cli.test-utils.d.ts +14 -0
- package/dist/cli/commands/publish.d.ts +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1100 -152
- package/dist/lib/api-url.d.ts +32 -23
- package/dist/lib/auth-store.d.ts +111 -42
- package/dist/lib/config.d.ts +19 -21
- package/dist/lib/feedback.d.ts +8 -3
- package/dist/lib/fleet-credentials.d.ts +246 -0
- package/dist/lib/portable-skills-files.d.ts +9 -0
- package/dist/lib/portable-skills.d.ts +2 -2
- package/dist/lib/remote-client.d.ts +20 -10
- package/dist/lib/remote-registry.d.ts +31 -11
- package/dist/lib/run-routing.d.ts +6 -4
- package/dist/lib/vendor-host-policy.d.ts +31 -0
- package/dist/sdk/index.d.ts +6 -0
- package/dist/sdk/index.js +1738 -572
- package/dist/server/artifact-storage.d.ts +11 -0
- package/dist/server/skills-api.d.ts +13 -2
- package/dist/storage.js +9 -14
- package/package.json +3 -1
package/dist/lib/api-url.d.ts
CHANGED
|
@@ -1,34 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Where the Skills API lives, for this process, right now.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* This module is a thin reading of `fleet-credentials.ts`, which is itself a
|
|
5
|
+
* thin reading of `@hasna/contracts/client`. It exists only so the two failure
|
|
6
|
+
* MODES stay named:
|
|
6
7
|
*
|
|
7
|
-
* - Read paths fail closed: `resolveApiUrl()` returns `undefined`
|
|
8
|
-
* caller keeps working against the bundled local
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* - Read paths fail closed: `resolveApiUrl()` returns `undefined` when nothing
|
|
9
|
+
* is configured, and the caller keeps working against the bundled local
|
|
10
|
+
* registry.
|
|
11
|
+
* - Auth and write paths fail loudly: `requireApiUrl()` throws an error naming
|
|
12
|
+
* the missing configuration.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* Boundary rule (R1), unchanged in substance: an install with NO credential
|
|
15
|
+
* produces no URL at all. The fleet gateway default in the shared seam applies
|
|
16
|
+
* only once a credential has resolved, so an unconfigured OSS install still
|
|
17
|
+
* names no host — see `resolveSkillsFleet`.
|
|
18
|
+
*
|
|
19
|
+
* A URL that IS configured while no credential resolves does not fall back to
|
|
20
|
+
* local: it throws (SkillsFleetCredentialError). Reading local data because
|
|
21
|
+
* authentication is unconfigured is the false green this ladder exists to
|
|
22
|
+
* remove.
|
|
15
23
|
*/
|
|
16
|
-
import { type
|
|
17
|
-
|
|
18
|
-
export declare const
|
|
24
|
+
import { type SkillsFleetOptions } from "./fleet-credentials.js";
|
|
25
|
+
/** The canonical env name. The unprefixed `SKILLS_API_URL` alias is still read. */
|
|
26
|
+
export declare const API_URL_ENV_VAR: string;
|
|
19
27
|
/** Hint shown wherever a Skills API URL is required but absent. */
|
|
20
28
|
export declare const MISSING_API_URL_HINT: string;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
29
|
+
/** Kept under its old name: callers and tests match on this identity. */
|
|
30
|
+
export { MissingSkillsFleetError as MissingApiUrlError } from "./fleet-credentials.js";
|
|
31
|
+
type Env = Record<string, string | undefined>;
|
|
25
32
|
/**
|
|
26
|
-
* Resolve the
|
|
27
|
-
*
|
|
33
|
+
* Resolve the Skills origin in effect, or `undefined` when this install has
|
|
34
|
+
* neither a credential nor an authority — i.e. runs on this machine.
|
|
35
|
+
*
|
|
36
|
+
* Throws when an authority IS configured and no credential resolves.
|
|
28
37
|
*/
|
|
29
|
-
export declare function resolveApiUrl(
|
|
38
|
+
export declare function resolveApiUrl(env?: Env, options?: SkillsFleetOptions): string | undefined;
|
|
30
39
|
/**
|
|
31
|
-
* Resolve the
|
|
32
|
-
*
|
|
40
|
+
* Resolve the Skills origin, or throw naming what is missing. Use this on every
|
|
41
|
+
* auth and write path.
|
|
33
42
|
*/
|
|
34
|
-
export declare function requireApiUrl(action?: string,
|
|
43
|
+
export declare function requireApiUrl(action?: string, env?: Env, options?: SkillsFleetOptions): string;
|
package/dist/lib/auth-store.d.ts
CHANGED
|
@@ -1,66 +1,135 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The credential the CLI signs in with, and the identity it displays.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* and
|
|
9
|
-
*
|
|
4
|
+
* READING is not done here. Every read goes through `fleet-credentials.ts` →
|
|
5
|
+
* `@hasna/contracts/client`, so the argument, the env pointer, the macOS
|
|
6
|
+
* Keychain, `~/.hasna/skills/config/credentials` and `HASNA_SKILLS_API_KEY` are
|
|
7
|
+
* consulted in the fleet's one order, on every call. There is no cache: a
|
|
8
|
+
* credential is mutable state, and a value captured at process start is the
|
|
9
|
+
* defect the ladder exists to remove (a shell that outlives a key rotation).
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* WRITING lands in exactly one file — `~/.hasna/skills/config/credentials`,
|
|
12
|
+
* mode 0600, the shared seam's disk tier — so `skills auth login` on this
|
|
13
|
+
* machine and a station wrapper reading the same file cannot disagree.
|
|
14
|
+
* `HASNA_HOME` / `HASNA_CONFIG_HOME` relocate it; `$HASNA_SKILLS_DIR` does not,
|
|
15
|
+
* because that variable relocates this app's DATA (corpus, database, config),
|
|
16
|
+
* and the fleet credential is not app data — it is the machine's, shared with
|
|
17
|
+
* every other Hasna CLI.
|
|
18
|
+
*
|
|
19
|
+
* The display identity (`email`, org, user ids) is NOT a credential and lives
|
|
20
|
+
* beside it in `identity.json`. It is only ever what the server's `whoami`
|
|
21
|
+
* returned; nothing here invents a field.
|
|
22
|
+
*
|
|
23
|
+
* `~/.skills/auth.json` and `~/.hasna/skills/auth.json` are retired locations and
|
|
24
|
+
* are not read. `skills auth login` writes the credentials file; an operator
|
|
25
|
+
* still holding an old auth.json is told to sign in again.
|
|
18
26
|
*/
|
|
19
|
-
|
|
27
|
+
import { type SkillsFleetOptions } from "./fleet-credentials.js";
|
|
28
|
+
export { normalizeSkillsApiOrigin } from "./fleet-credentials.js";
|
|
29
|
+
type Env = Record<string, string | undefined>;
|
|
30
|
+
/** The credentials file this package writes and the shared seam reads. */
|
|
31
|
+
export declare function getAuthFilePath(env?: Env): string;
|
|
20
32
|
/**
|
|
21
|
-
* Write-free
|
|
33
|
+
* Write-free path resolution for read-only paths (e.g. `sync --dry-run`).
|
|
22
34
|
*
|
|
23
|
-
* getAuthFilePath()
|
|
24
|
-
*
|
|
25
|
-
*
|
|
35
|
+
* Identical to getAuthFilePath(): nothing in the credential path writes as a
|
|
36
|
+
* side effect of resolving any more. Kept as a separate name so the read-only
|
|
37
|
+
* callers keep reading as read-only.
|
|
26
38
|
*/
|
|
27
|
-
export declare function getAuthFilePathReadOnly(): string;
|
|
39
|
+
export declare function getAuthFilePathReadOnly(env?: Env): string;
|
|
40
|
+
/** The display identity file beside the credential. Never holds a secret. */
|
|
41
|
+
export declare function getIdentityFilePath(env?: Env): string;
|
|
28
42
|
/**
|
|
29
43
|
* Stored credentials for a Skills API instance.
|
|
30
44
|
*
|
|
31
|
-
* `apiKey` is the
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
45
|
+
* `apiKey` is the credential the ladder resolved — not necessarily one this CLI
|
|
46
|
+
* wrote. The identity fields are display metadata echoed back from the
|
|
47
|
+
* instance's `whoami`, so they are optional: an instance that does not return
|
|
48
|
+
* them leaves them unset. They are never invented locally — a placeholder
|
|
49
|
+
* written here is indistinguishable from a value the server actually returned.
|
|
36
50
|
*/
|
|
37
51
|
export interface AuthConfig {
|
|
38
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The credential the ladder resolved, or null when it is a vault POINTER that
|
|
54
|
+
* only the async path can complete (see {@link getApiKeyAsync}). Callers that
|
|
55
|
+
* need to SEND it must resolve it there; the display surfaces below only need
|
|
56
|
+
* to know that one is configured.
|
|
57
|
+
*/
|
|
58
|
+
apiKey: string | null;
|
|
39
59
|
email?: string;
|
|
40
60
|
orgId?: string;
|
|
41
61
|
orgSlug?: string;
|
|
42
62
|
userId?: string;
|
|
43
63
|
}
|
|
44
|
-
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
/** The identity half, on its own: what `whoami` said, with no credential. */
|
|
65
|
+
export type AuthIdentity = Omit<AuthConfig, "apiKey">;
|
|
66
|
+
/**
|
|
67
|
+
* What `saveAuthConfig` is handed: a key this CLI actually holds.
|
|
68
|
+
*
|
|
69
|
+
* Distinct from {@link AuthConfig}, whose `apiKey` may be null for a vault
|
|
70
|
+
* pointer — there is nothing to write to disk in that case, and writing an
|
|
71
|
+
* empty line would masquerade as a stored credential.
|
|
72
|
+
*/
|
|
73
|
+
export type StoredAuthConfig = AuthIdentity & {
|
|
74
|
+
apiKey: string;
|
|
75
|
+
};
|
|
76
|
+
export declare function getAuthIdentity(env?: Env): AuthIdentity;
|
|
77
|
+
/**
|
|
78
|
+
* The credential in effect plus whatever identity was recorded for it, or null
|
|
79
|
+
* when no credential resolves anywhere on the ladder.
|
|
80
|
+
*/
|
|
81
|
+
export declare function getAuthConfig(env?: Env, options?: SkillsFleetOptions): AuthConfig | null;
|
|
82
|
+
/** Alias kept for the read-only callers; resolution never writes. */
|
|
83
|
+
export declare function getAuthConfigReadOnly(env?: Env, options?: SkillsFleetOptions): AuthConfig | null;
|
|
48
84
|
/**
|
|
49
|
-
*
|
|
85
|
+
* Persist the credential (and any identity the server returned) for this user.
|
|
50
86
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
87
|
+
* Returns the file it wrote, so the CLI can name the real path rather than a
|
|
88
|
+
* path it assumed.
|
|
89
|
+
*/
|
|
90
|
+
export declare function saveAuthConfig(config: StoredAuthConfig, env?: Env): string;
|
|
91
|
+
/** Store (or clear, with null) the API URL beside the credential. */
|
|
92
|
+
export declare function saveApiUrl(apiUrl: string | null, env?: Env): string;
|
|
93
|
+
/** The API URL recorded in the credentials file, or null. */
|
|
94
|
+
export declare function readStoredApiUrl(env?: Env): string | null;
|
|
95
|
+
/**
|
|
96
|
+
* Remove the credential this CLI wrote.
|
|
97
|
+
*
|
|
98
|
+
* Only the file is cleared: a key injected from the environment or held in the
|
|
99
|
+
* Keychain belongs to the machine, not to this command, and silently appearing
|
|
100
|
+
* to remove it would be a lie. The caller is told whether one still resolves.
|
|
101
|
+
*/
|
|
102
|
+
export declare function clearAuthConfig(env?: Env): {
|
|
103
|
+
stillResolves: boolean;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* The credential in effect, resolved fresh through the shared ladder.
|
|
107
|
+
*
|
|
108
|
+
* SYNCHRONOUS, so it cannot complete a vault pointer
|
|
109
|
+
* (`HASNA_SKILLS_API_KEY_REF`): for that tier it returns null, because the
|
|
110
|
+
* pointer's own value is the empty string and handing THAT back as a key is how
|
|
111
|
+
* `Authorization: Bearer ` reached the wire. Any path that is about to SEND the
|
|
112
|
+
* key must use {@link getApiKeyAsync} (or `resolveSkillsApiKey`), which fetches
|
|
113
|
+
* the vault item and refuses loudly when it cannot.
|
|
114
|
+
*/
|
|
115
|
+
export declare function getApiKey(env?: Env, options?: SkillsFleetOptions): string | null;
|
|
116
|
+
/**
|
|
117
|
+
* The credential in effect, completing a vault pointer through the secrets
|
|
118
|
+
* vault. Null only in local mode; throws when a configured credential cannot
|
|
119
|
+
* be produced. Use this wherever the key is about to be sent.
|
|
55
120
|
*/
|
|
56
|
-
export declare function
|
|
57
|
-
|
|
58
|
-
export declare function
|
|
121
|
+
export declare function getApiKeyAsync(env?: Env, options?: SkillsFleetOptions): Promise<string | null>;
|
|
122
|
+
/** Identical to getApiKey(): resolution has no write side effects. */
|
|
123
|
+
export declare function getApiKeyReadOnly(env?: Env, options?: SkillsFleetOptions): string | null;
|
|
59
124
|
/**
|
|
60
125
|
* Origin every credential-bearing request is sent to.
|
|
61
126
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
127
|
+
* The AUTHORITY, not the whole hosted resolution: `skills auth login` runs
|
|
128
|
+
* before there is a credential, and requiring one here would make signing in
|
|
129
|
+
* impossible. Throws when nothing names a service: an install that named none
|
|
130
|
+
* must not decide on the user's behalf where their email address, login code,
|
|
131
|
+
* or API key goes.
|
|
65
132
|
*/
|
|
66
|
-
export declare function getApiUrl(action?: string): string;
|
|
133
|
+
export declare function getApiUrl(action?: string, env?: Env, options?: SkillsFleetOptions): string;
|
|
134
|
+
/** Permission bits of the credentials file, for `skills auth status`-style output. */
|
|
135
|
+
export declare function credentialFileMode(env?: Env): number | null;
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -16,29 +16,29 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { DATA_DIR_ENV } from "./app-home.js";
|
|
18
18
|
/**
|
|
19
|
-
* There is no deployment "mode" key.
|
|
19
|
+
* There is no deployment "mode" key, and no service address here either.
|
|
20
20
|
*
|
|
21
21
|
* Skills has one deployment story: you run it. Whether this CLI talks to a
|
|
22
|
-
* server is not a product variant, it is one fact — whether
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* it claims to describe.
|
|
22
|
+
* server is not a product variant, it is one fact — whether a fleet credential
|
|
23
|
+
* resolves. Nothing may be derived from a declared label, because a label can
|
|
24
|
+
* disagree with the configuration it claims to describe.
|
|
26
25
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* the
|
|
31
|
-
*
|
|
26
|
+
* `apiUrl` used to live here as a sixth URL tier of this package's own. It is
|
|
27
|
+
* retired (owner ruling 2026-09-04, hasna/apps#1720): the authority ladder is
|
|
28
|
+
* `HASNA_SKILLS_API_URL` → the Keychain `api-url` item →
|
|
29
|
+
* `~/.hasna/skills/config/credentials` → the fleet gateway, and it belongs to
|
|
30
|
+
* @hasna/contracts so every Hasna CLI resolves it identically.
|
|
31
|
+
* `skills setup --api-url <origin>` still writes it — into the credentials file.
|
|
32
32
|
*
|
|
33
|
-
* Configs written by older versions may still carry a "mode" key on
|
|
34
|
-
* refused rather than ignored
|
|
35
|
-
* the worse of the two failures
|
|
33
|
+
* Configs written by older versions may still carry a "mode" or "apiUrl" key on
|
|
34
|
+
* disk. Those are refused rather than ignored — see lib/retired-settings.ts for
|
|
35
|
+
* why silence is the worse of the two failures — and `skills config unset <key>`
|
|
36
|
+
* removes them.
|
|
36
37
|
*/
|
|
37
38
|
export interface SkillsConfig {
|
|
38
39
|
defaultAgent?: "claude" | "codex" | "gemini" | "pi" | "opencode" | "all";
|
|
39
40
|
defaultScope?: "global" | "project";
|
|
40
41
|
format?: "compact" | "json" | "csv";
|
|
41
|
-
apiUrl?: string;
|
|
42
42
|
extensionsDir?: string;
|
|
43
43
|
}
|
|
44
44
|
export type ConfigScope = "global" | "project";
|
|
@@ -109,9 +109,8 @@ export declare function getConfigPathReadOnly(scope: ConfigScope): string;
|
|
|
109
109
|
* This mirrors that FILE-LEVEL precedence, never field-level merging: canonical
|
|
110
110
|
* config.json, when present, is the whole global config; legacy ~/.skillsrc is read
|
|
111
111
|
* only in the exact situation the write path would copy it (no canonical file, no
|
|
112
|
-
* override). Field-level merging would inherit
|
|
113
|
-
* canonical config that omits
|
|
114
|
-
* whatever origin resolves, so divergence from the write path is credential-bearing.
|
|
112
|
+
* override). Field-level merging would inherit stale legacy values beneath a
|
|
113
|
+
* canonical config that omits them, so the two paths must agree exactly.
|
|
115
114
|
*/
|
|
116
115
|
export declare function loadConfigReadOnly(): SkillsConfig;
|
|
117
116
|
/**
|
|
@@ -129,10 +128,9 @@ export declare function saveConfig(key: string, value: string, scope?: ConfigSco
|
|
|
129
128
|
/**
|
|
130
129
|
* Remove a single config key from the specified scope.
|
|
131
130
|
*
|
|
132
|
-
* This is
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* to set mode=local, and that key is gone.
|
|
131
|
+
* This is also how a retired key written by an older version is removed: a file
|
|
132
|
+
* carrying one is refused by every read, so refusing to unset it would leave an
|
|
133
|
+
* operator with a config every command rejects and no supported repair.
|
|
136
134
|
*
|
|
137
135
|
* Returns whether the key was actually present, so callers can distinguish
|
|
138
136
|
* "removed" from "there was nothing to remove" instead of guessing.
|
package/dist/lib/feedback.d.ts
CHANGED
|
@@ -21,8 +21,13 @@ export interface FeedbackResult {
|
|
|
21
21
|
export declare function getFeedbackDbPath(): string;
|
|
22
22
|
export declare function saveFeedback(input: FeedbackInput): FeedbackResult;
|
|
23
23
|
/**
|
|
24
|
-
* True when this install
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* True when this install talks to a Skills instance — i.e. a credential resolves
|
|
25
|
+
* on the shared fleet ladder (lib/fleet-credentials.ts).
|
|
26
|
+
*
|
|
27
|
+
* The check never throws: `skills feedback` must not crash because the ladder is
|
|
28
|
+
* half-configured. A configured authority with no credential is treated as
|
|
29
|
+
* api mode here on purpose — the operator meant to be hosted, so feedback goes
|
|
30
|
+
* to the forwardable JSONL file rather than opening a local database that the
|
|
31
|
+
* hosted install has no business creating.
|
|
27
32
|
*/
|
|
28
33
|
export declare function isApiMode(env?: Record<string, string | undefined>): boolean;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place this package decides WHICH Skills service it talks to and WITH
|
|
3
|
+
* WHICH credential.
|
|
4
|
+
*
|
|
5
|
+
* Everything here delegates to the shared client seam in `@hasna/contracts/client`
|
|
6
|
+
* (owner rulings 2026-09-04 — hasna/apps#1720, #1668, #1690, #1613, #1599). This
|
|
7
|
+
* package owns no second copy of the ladder, and no alias env name of its own.
|
|
8
|
+
*
|
|
9
|
+
* CREDENTIAL, resolved fresh on every call (contracts `resolveCredential`):
|
|
10
|
+
*
|
|
11
|
+
* 1. an explicit argument — `--api-key`, `--profile`
|
|
12
|
+
* 2. a deliberate env pointer — `HASNA_SKILLS_API_KEY_OVERRIDE`,
|
|
13
|
+
* `HASNA_PROFILE`, `HASNA_SKILLS_API_KEY_REF`
|
|
14
|
+
*
|
|
15
|
+
* `HASNA_SKILLS_API_KEY_REF` names a VAULT ITEM rather than a key, so it
|
|
16
|
+
* resolves in two steps: the synchronous chain yields the pointer, and
|
|
17
|
+
* `resolveSkillsApiKey()` (async) fetches the value through the secrets
|
|
18
|
+
* SDK on each call. Never treat the pointer's own `apiKey` — the empty
|
|
19
|
+
* string — as a credential; `resolveSkillsFleet` reports null for it.
|
|
20
|
+
* 3. the macOS Keychain — generic-password `hasna.credentials.skills.api-key`,
|
|
21
|
+
* account `HASNA_STATION`, else `hostname -s`, else `USER`
|
|
22
|
+
* 4. disk, read at call time — `~/.hasna/skills/config/credentials`
|
|
23
|
+
* (`HASNA_HOME` / `HASNA_CONFIG_HOME` relocate it; XDG never)
|
|
24
|
+
* 5. `HASNA_SKILLS_API_KEY` in the environment — a legitimate tier, below disk,
|
|
25
|
+
* and carrying no deprecation notice
|
|
26
|
+
*
|
|
27
|
+
* URL: `HASNA_SKILLS_API_URL` → the Keychain `api-url` item → the credentials
|
|
28
|
+
* file → the fleet gateway `https://api.hasna.com/skills`. The gateway default
|
|
29
|
+
* applies ONLY once a credential has resolved, so an install with no credential
|
|
30
|
+
* still names no host at all (the R1 boundary in vendor-host-policy.ts).
|
|
31
|
+
*
|
|
32
|
+
* The unprefixed `SKILLS_API_URL` / `SKILLS_API_KEY` spellings are still
|
|
33
|
+
* accepted, silently, because the shared seam accepts `<APP>_API_URL` /
|
|
34
|
+
* `<APP>_API_KEY` as documented aliases one rung below the canonical
|
|
35
|
+
* `HASNA_SKILLS_*` names. They are documented in the README for one release and
|
|
36
|
+
* are not read anywhere else in this package. `SKILL_API_KEY` (singular) is
|
|
37
|
+
* gone: it shadowed nothing canonical and was never documented.
|
|
38
|
+
*
|
|
39
|
+
* THREE OUTCOMES, and no fourth:
|
|
40
|
+
*
|
|
41
|
+
* - a credential resolves → HOSTED. The authority is the configured
|
|
42
|
+
* URL, else the fleet gateway. A credential
|
|
43
|
+
* that cannot produce a usable key — a
|
|
44
|
+
* deliberate selection contracts refuses, a
|
|
45
|
+
* pointer whose vault item is missing, any
|
|
46
|
+
* tier that yields a blank value — is a LOUD
|
|
47
|
+
* failure, never a demotion to local.
|
|
48
|
+
* - no credential, but a URL is configured
|
|
49
|
+
* → LOUD failure. The caller exits non-zero
|
|
50
|
+
* with one line naming what is missing.
|
|
51
|
+
* There is no local fallback here: serving
|
|
52
|
+
* local results while authentication is
|
|
53
|
+
* 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.
|
|
58
|
+
*/
|
|
59
|
+
import { type CredentialChainOptions, type CredentialTier, type KeychainTierOptions, type ResolvedCredential } from "@hasna/contracts/client";
|
|
60
|
+
/** The app slug: the Keychain service, the `~/.hasna/<app>` folder, the gateway path. */
|
|
61
|
+
export declare const SKILLS_APP = "skills";
|
|
62
|
+
type Env = Record<string, string | undefined>;
|
|
63
|
+
/** `HASNA_SKILLS_API_URL`, then the accepted `SKILLS_API_URL` alias. */
|
|
64
|
+
export declare const SKILLS_API_URL_ENV_KEYS: readonly string[];
|
|
65
|
+
/** `HASNA_SKILLS_API_KEY`, then the accepted `SKILLS_API_KEY` alias. */
|
|
66
|
+
export declare const SKILLS_API_KEY_ENV_KEYS: readonly string[];
|
|
67
|
+
/** The canonical spellings, for messages that have to name exactly one. */
|
|
68
|
+
export declare const SKILLS_API_URL_ENV: string;
|
|
69
|
+
export declare const SKILLS_API_KEY_ENV: string;
|
|
70
|
+
export interface SkillsFleetOptions {
|
|
71
|
+
/** Tier-1 credential inputs and the Keychain-tier controls (a fake runner in tests). */
|
|
72
|
+
credentials?: CredentialChainOptions;
|
|
73
|
+
}
|
|
74
|
+
/** A hosted resolution: an authority to call and a credential to call it with. */
|
|
75
|
+
export interface HostedSkillsFleet {
|
|
76
|
+
mode: "hosted";
|
|
77
|
+
/** Origin the CLI/SDK sends requests to. Never carries a trailing slash. */
|
|
78
|
+
apiOrigin: string;
|
|
79
|
+
/**
|
|
80
|
+
* The resolved key, or null when the credential is a vault POINTER
|
|
81
|
+
* (`HASNA_SKILLS_API_KEY_REF` / a `credential_ref` line) that only the async
|
|
82
|
+
* path can complete — see {@link apiKeyPointer} and {@link resolveSkillsApiKey}.
|
|
83
|
+
*
|
|
84
|
+
* It is NEVER the empty string: a blank key would produce
|
|
85
|
+
* `Authorization: Bearer ` on the wire, and — read as falsy by a caller
|
|
86
|
+
* looking for "is there a token" — a silent drop back to local data. Both
|
|
87
|
+
* are refused at resolution time instead.
|
|
88
|
+
*
|
|
89
|
+
* Never logged, never written anywhere but the header.
|
|
90
|
+
*/
|
|
91
|
+
apiKey: string | null;
|
|
92
|
+
/**
|
|
93
|
+
* The unresolved vault pointer, when tier === "pointer"; null otherwise.
|
|
94
|
+
*
|
|
95
|
+
* `resolveCredential` returns a TRUTHY credential for the pointer tier whose
|
|
96
|
+
* `apiKey` is empty and whose `pointerVaultKey` names the vault item;
|
|
97
|
+
* fetching that item is a separate async step. Carrying the pointer (rather
|
|
98
|
+
* than its empty key) is what keeps the sending paths honest.
|
|
99
|
+
*/
|
|
100
|
+
apiKeyPointer: ResolvedCredential | null;
|
|
101
|
+
/** WHERE the URL came from: an env key NAME, a Keychain reference, a path, or "default". */
|
|
102
|
+
apiUrlSource: string;
|
|
103
|
+
/** WHERE the key came from: an env key NAME, a Keychain reference, or a path. Never a value. */
|
|
104
|
+
apiKeySource: string;
|
|
105
|
+
apiKeyTier: CredentialTier;
|
|
106
|
+
/** Advisory from the shared seam (never secret), or null. */
|
|
107
|
+
warning: string | null;
|
|
108
|
+
}
|
|
109
|
+
/** Nothing is configured: this install runs on this machine. */
|
|
110
|
+
export interface LocalSkillsFleet {
|
|
111
|
+
mode: "local";
|
|
112
|
+
apiOrigin: null;
|
|
113
|
+
apiKey: null;
|
|
114
|
+
}
|
|
115
|
+
export type SkillsFleet = HostedSkillsFleet | LocalSkillsFleet;
|
|
116
|
+
/** Machine-readable reasons a hosted resolution was refused. */
|
|
117
|
+
export type SkillsFleetErrorCode = "MISSING_API_CREDENTIAL" | "INVALID_API_URL";
|
|
118
|
+
/**
|
|
119
|
+
* A configured install could not produce a usable hosted client.
|
|
120
|
+
*
|
|
121
|
+
* `code` is stable so JSON callers can branch on it, and distinguishes the two
|
|
122
|
+
* refusals that are NOT the same fault: an authority with no credential
|
|
123
|
+
* (MISSING_API_CREDENTIAL) versus an authority that is declared but unusable
|
|
124
|
+
* (INVALID_API_URL). MISSING_API_URL stays the code for "nothing configured at
|
|
125
|
+
* all" (see MissingSkillsFleetError), which is a third, non-error state for the
|
|
126
|
+
* commands that may run locally.
|
|
127
|
+
*/
|
|
128
|
+
export declare class SkillsFleetCredentialError extends Error {
|
|
129
|
+
readonly code: SkillsFleetErrorCode;
|
|
130
|
+
constructor(message: string, code?: SkillsFleetErrorCode);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Normalize a configured Skills authority to the origin the client dials.
|
|
134
|
+
*
|
|
135
|
+
* The Skills server serves its API under `/api/v1`, so the client composes
|
|
136
|
+
* `<origin>/api/v1/...` itself. An operator who pasted the full API base — the
|
|
137
|
+
* URL printed by every error message — must not end up with `/api/v1/api/v1`.
|
|
138
|
+
*/
|
|
139
|
+
export declare function normalizeSkillsApiOrigin(apiUrl: string): string;
|
|
140
|
+
/** One configured authority: its value and the source that decided it. */
|
|
141
|
+
export interface ConfiguredSkillsApiUrl {
|
|
142
|
+
value: string;
|
|
143
|
+
/** An env key NAME, a `keychain:<service>@<account>` reference, or an absolute path. */
|
|
144
|
+
source: string;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The authority an operator configured, in the shared seam's precedence order —
|
|
148
|
+
* environment, then the Keychain `api-url` item, then the credentials file.
|
|
149
|
+
*
|
|
150
|
+
* Returns null when nothing configures one, which is what lets the gateway
|
|
151
|
+
* default apply for a credentialled install and what keeps an install with no
|
|
152
|
+
* credential from naming a host at all.
|
|
153
|
+
*/
|
|
154
|
+
export declare function configuredSkillsApiUrl(env?: Env, keychain?: KeychainTierOptions): ConfiguredSkillsApiUrl | null;
|
|
155
|
+
/** The credential file paths consulted, for a message that has to name them. */
|
|
156
|
+
export declare function skillsCredentialFiles(env?: Env): string[];
|
|
157
|
+
/**
|
|
158
|
+
* Where a credential should be written, and the only file this package writes.
|
|
159
|
+
*
|
|
160
|
+
* Throws when neither HOME nor HASNA_HOME anchors a root, because there is then
|
|
161
|
+
* no correct place to put a secret and guessing one is worse than refusing.
|
|
162
|
+
*/
|
|
163
|
+
export declare function skillsCredentialFilePath(env?: Env): string;
|
|
164
|
+
/**
|
|
165
|
+
* Say — once per process, on stderr — that this install is running locally.
|
|
166
|
+
*
|
|
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.
|
|
171
|
+
*/
|
|
172
|
+
export declare function noticeLocalSkillsMode(write?: (line: string) => void): void;
|
|
173
|
+
/** Test seam: forget that the local-mode line was printed. */
|
|
174
|
+
export declare function resetLocalSkillsModeNotice(): void;
|
|
175
|
+
/**
|
|
176
|
+
* Resolve the service this process should use, fresh.
|
|
177
|
+
*
|
|
178
|
+
* Never returns a hosted resolution without a credential, and never degrades a
|
|
179
|
+
* configured authority to local mode.
|
|
180
|
+
*/
|
|
181
|
+
export declare function resolveSkillsFleet(env?: Env, options?: SkillsFleetOptions): SkillsFleet;
|
|
182
|
+
/**
|
|
183
|
+
* The usable API key for this process, completing a vault pointer if that is
|
|
184
|
+
* the tier that won.
|
|
185
|
+
*
|
|
186
|
+
* ASYNC because the pointer tier is: the value is fetched from the secrets
|
|
187
|
+
* vault at call time, so a rotated item is picked up without a restart. Every
|
|
188
|
+
* path that is about to SEND the key resolves it here; the synchronous
|
|
189
|
+
* `resolveSkillsFleet` is for reporting (which tier, which source, which
|
|
190
|
+
* origin), and its `apiKey` is deliberately null for a pointer.
|
|
191
|
+
*
|
|
192
|
+
* Returns null only in local mode. Throws {@link SkillsFleetCredentialError}
|
|
193
|
+
* when a credential is configured and cannot be produced — never a fallback.
|
|
194
|
+
*/
|
|
195
|
+
export declare function resolveSkillsApiKey(env?: Env, options?: SkillsFleetOptions): Promise<string | null>;
|
|
196
|
+
/** The usable API key, or throw naming what is missing. Use on every send path. */
|
|
197
|
+
export declare function requireSkillsApiKey(action?: string, env?: Env, options?: SkillsFleetOptions): Promise<string>;
|
|
198
|
+
/**
|
|
199
|
+
* The credential for a surface that reports refusals as data (an MCP tool, a
|
|
200
|
+
* `--json` command) rather than as an exception.
|
|
201
|
+
*
|
|
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.
|
|
206
|
+
*/
|
|
207
|
+
export declare function skillsCredentialOrReason(env?: Env, options?: SkillsFleetOptions): Promise<{
|
|
208
|
+
apiKey: string;
|
|
209
|
+
reason: null;
|
|
210
|
+
} | {
|
|
211
|
+
apiKey: null;
|
|
212
|
+
reason: string | null;
|
|
213
|
+
}>;
|
|
214
|
+
/**
|
|
215
|
+
* The authority alone, for a flow that is ACQUIRING a credential.
|
|
216
|
+
*
|
|
217
|
+
* `skills auth login` cannot require a credential — obtaining one is the point —
|
|
218
|
+
* so it resolves the AUTHORITY on its own: the configured URL (environment,
|
|
219
|
+
* Keychain, credentials file), else the authority a resolved credential implies.
|
|
220
|
+
* With neither, this returns null and the caller fails loudly: R1 still holds,
|
|
221
|
+
* an install that named no service does not get to send an email address to one.
|
|
222
|
+
*/
|
|
223
|
+
export declare function resolveSkillsApiOrigin(env?: Env, options?: SkillsFleetOptions): {
|
|
224
|
+
origin: string;
|
|
225
|
+
source: string;
|
|
226
|
+
} | null;
|
|
227
|
+
/** The authority for an auth flow, or throw naming what is missing. */
|
|
228
|
+
export declare function requireSkillsApiOrigin(action?: string, env?: Env, options?: SkillsFleetOptions): string;
|
|
229
|
+
/**
|
|
230
|
+
* The hosted resolution, or throw. Use on every auth and write path.
|
|
231
|
+
*
|
|
232
|
+
* `action` names the caller so the message says what was refused.
|
|
233
|
+
*/
|
|
234
|
+
export declare function requireSkillsFleet(action?: string, env?: Env, options?: SkillsFleetOptions): HostedSkillsFleet;
|
|
235
|
+
/**
|
|
236
|
+
* Nothing at all is configured and the caller needed a service.
|
|
237
|
+
*
|
|
238
|
+
* The message names the environment variable, the Keychain item, the credential
|
|
239
|
+
* file and the command — and deliberately contains no URL, so an "error" can
|
|
240
|
+
* never hand a caller an endpoint it refused to resolve.
|
|
241
|
+
*/
|
|
242
|
+
export declare class MissingSkillsFleetError extends Error {
|
|
243
|
+
readonly code = "MISSING_API_URL";
|
|
244
|
+
constructor(action?: string);
|
|
245
|
+
}
|
|
246
|
+
export {};
|
|
@@ -11,6 +11,15 @@ export declare function defaultProvenance(sourceCommit?: string): PortableSkillP
|
|
|
11
11
|
export declare function normalizePortableSkillName(name: string): string;
|
|
12
12
|
export declare function readPortableSkillManifest(skillPath: string, fallbackName?: string): PortableSkillManifest;
|
|
13
13
|
export declare function parseSkillKind(value: string | undefined): SkillKind | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The version a skill EXPLICITLY declares, or undefined when none of the sources do:
|
|
16
|
+
* skill.json first, then the SKILL.md frontmatter, then package.json.
|
|
17
|
+
*
|
|
18
|
+
* Distinct from `readPortableSkillManifest().version`, which falls back to
|
|
19
|
+
* PORTABLE_SKILL_DEFAULT_VERSION: callers that must not invent a version (publishing,
|
|
20
|
+
* version pinning) use this and refuse when it is undefined.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readDeclaredSkillVersion(skillPath: string): string | undefined;
|
|
14
23
|
export declare function createInstructionManifest(name: string, options: {
|
|
15
24
|
description: string;
|
|
16
25
|
}): PortableSkillManifest;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { SkillKind, SkillMeta } from "./registry-types.js";
|
|
2
2
|
import { type SkillValidationResult } from "./skill-validation.js";
|
|
3
|
-
import { normalizePortableSkillName, readPortableSkillManifest } from "./portable-skills-files.js";
|
|
3
|
+
import { normalizePortableSkillName, readDeclaredSkillVersion, readPortableSkillManifest } from "./portable-skills-files.js";
|
|
4
4
|
import type { BulkPortPortableSkillOptions, BulkPortResult, PortableSkillOptions, PortableSkillRunOptions, PortableSkillRunResult, PortableSkillSummary, PortableSkillWriteResult, PortPortableSkillOptions, ScaffoldPortableSkillOptions } from "./portable-skills-types.js";
|
|
5
|
-
export { normalizePortableSkillName, readPortableSkillManifest, };
|
|
5
|
+
export { normalizePortableSkillName, readDeclaredSkillVersion, readPortableSkillManifest, };
|
|
6
6
|
export * from "./portable-skills-types.js";
|
|
7
7
|
/**
|
|
8
8
|
* Resolve the corpus: the directory holding one folder per installed skill.
|