@hasna/skills 0.1.71 → 0.2.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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Skills app-home resolution through the @hasna/paths resolver.
3
+ *
4
+ * Skills keeps its global config (`config.json`), auth (`auth.json`), the
5
+ * default SQLite database (`server.db`), the corpus cache (`skills/`), and the
6
+ * portable-skill tree (`installed/`, `custom/`) under one data root.
7
+ * Historically that root was `~/.hasna/skills`. This module resolves the root
8
+ * through `@hasna/paths` (XDG / macOS home layout) with a gated legacy
9
+ * adoption: the legacy `~/.hasna/skills` stays the effective data root until
10
+ * the store is physically migrated to the XDG data home (`server.db` or
11
+ * `config.json` present there) or the operator sets the data-kind override
12
+ * `HASNA_DATA_HOME`. An existing live store never becomes invisible on
13
+ * upgrade. The exact-app overrides win unconditionally, in this order:
14
+ * `HASNA_SKILLS_DIR` (the shipped override), then the wave-convention aliases
15
+ * `HASNA_SKILLS_HOME` and `SKILLS_HOME`.
16
+ *
17
+ * Nothing moves on disk in this phase — the package just resolves the new
18
+ * paths.
19
+ */
20
+ export type PathKind = "config" | "data" | "state" | "cache";
21
+ export interface PathsResolverOptions {
22
+ app: string;
23
+ internal?: boolean;
24
+ platform?: string;
25
+ home?: string;
26
+ env?: Record<string, string | undefined>;
27
+ }
28
+ export declare function dataDir(options: PathsResolverOptions): string;
29
+ /** Env var that relocates the skills data directory (the shipped override). */
30
+ export declare const DATA_DIR_ENV = "HASNA_SKILLS_DIR";
31
+ /** Wave-convention exact-app home aliases, honoured after HASNA_SKILLS_DIR. */
32
+ export declare const HASNA_SKILLS_HOME_ENV = "HASNA_SKILLS_HOME";
33
+ export declare const SKILLS_HOME_ENV = "SKILLS_HOME";
34
+ /** Filename of the default SQLite database inside the skills data directory. */
35
+ export declare const DEFAULT_SQLITE_FILENAME = "server.db";
36
+ /** Filename of the global config file inside the skills data directory. */
37
+ export declare const GLOBAL_CONFIG_FILENAME = "config.json";
38
+ /**
39
+ * The effective user home, mirroring the pre-existing skills resolution
40
+ * (`HOME` || `USERPROFILE` || `os.homedir()`). Read at call time so a runtime
41
+ * `HOME` reassignment (e.g. a test temp home) is honoured — `os.homedir()`
42
+ * snapshots `HOME` at process start and, under Bun, ignores later changes.
43
+ */
44
+ export declare function effectiveHome(): string;
45
+ /** The legacy (pre-XDG) data root: `~/.hasna/skills`. */
46
+ export declare function legacyDataRoot(): string;
47
+ /**
48
+ * The @hasna/paths-resolved (XDG / macOS home layout) data root for skills:
49
+ * `~/.local/share/hasna/skills` on Linux, `~/Library/Application
50
+ * Support/Hasna/skills` on macOS. The home override mirrors the pre-existing
51
+ * `$HOME`-first resolution so the resolver follows the same home the legacy
52
+ * path does.
53
+ *
54
+ * `env` is forwarded to the resolver: an injected env (e.g. `{}` for a
55
+ * staged home mirror) suppresses the process-level `HASNA_*_HOME` overrides,
56
+ * which describe THIS machine's live store and must not relocate a mirror.
57
+ * `@hasna/paths` applies env overrides before the injected home, so without
58
+ * this the resolver would ignore the mirror home whenever `HASNA_DATA_HOME`
59
+ * is set.
60
+ */
61
+ export declare function resolverDataRoot(home?: string, env?: Record<string, string | undefined>): string;
62
+ /**
63
+ * Whether the resolver (XDG) data root should be adopted as the effective
64
+ * data root. The resolver root is adopted only when the operator has set
65
+ * `HASNA_DATA_HOME` (the data-kind override — a deliberate opt-in to the XDG
66
+ * layout) or the store has already been physically migrated there (`server.db`
67
+ * — the default SQLite store — or `config.json` exists). A machine that only
68
+ * redirects another kind (e.g. cache to tmpfs) must NOT have its data home
69
+ * moved, and a live store at the legacy home must never become invisible on
70
+ * upgrade.
71
+ */
72
+ export declare function adoptResolverDataRoot(resolved: string, env?: NodeJS.ProcessEnv): boolean;
73
+ /**
74
+ * The exact-app override root, when set: the shipped `HASNA_SKILLS_DIR` wins,
75
+ * then the wave-convention aliases `HASNA_SKILLS_HOME` and `SKILLS_HOME`.
76
+ * First non-blank override wins; a blank or whitespace-only primary must not
77
+ * shadow a valid secondary (nullish `??` does not fall through on `""`).
78
+ */
79
+ export declare function exactDataRoot(): string | undefined;
80
+ /** Whether an exact-app override root is set (used to skip legacy migration). */
81
+ export declare function hasExactOverride(env?: NodeJS.ProcessEnv): boolean;
82
+ /**
83
+ * Whether the operator named a data root at all — an exact-app override or the
84
+ * data-kind `HASNA_DATA_HOME`. Used to decide when the legacy `~/.skills` /
85
+ * `~/.skillsrc` migration must be skipped: copying a stray legacy tree into an
86
+ * operator-chosen directory would be a surprising write.
87
+ */
88
+ export declare function hasOperatorOverride(env?: NodeJS.ProcessEnv): boolean;
89
+ /**
90
+ * The effective data root: an exact-app override (`HASNA_SKILLS_DIR`, then
91
+ * `HASNA_SKILLS_HOME` / `SKILLS_HOME`) wins unconditionally; otherwise the
92
+ * resolver (XDG) data root once adopted; otherwise the legacy `~/.hasna/skills`
93
+ * default. Write-free: callers that need the directory to exist create it.
94
+ */
95
+ export declare function getDataRoot(): string;
96
+ /**
97
+ * The skills app data root for an explicit home root, mirroring getDataRoot()
98
+ * with the home injected. Used by the sync-home snapshot mapping to enumerate
99
+ * the skills corpus under a staged home mirror (`homesRoot`) or the real home.
100
+ *
101
+ * Process-level overrides (`HASNA_SKILLS_DIR` / `HASNA_SKILLS_HOME` /
102
+ * `SKILLS_HOME`, and the data-kind `HASNA_DATA_HOME`) describe THIS machine's
103
+ * live store. They apply only when the requested home IS the process's own
104
+ * effective home; a staged mirror (a different home, e.g. an rsync'd
105
+ * remote-station `homesRoot`) must resolve its own layout — the mirror's XDG
106
+ * data root once the mirror itself carries a migrated store there, else the
107
+ * mirror's legacy `~/.hasna/skills` — never the local process's live data
108
+ * root. Snapshotting a staged home with the local `HASNA_DATA_HOME` set would
109
+ * otherwise read live local data instead of the supplied mirror.
110
+ */
111
+ export declare function skillsDataRootForHome(home: string): string;
@@ -8,6 +8,13 @@
8
8
  *
9
9
  * Values from the project config override global config.
10
10
  */
11
+ /**
12
+ * Environment variable that relocates the skills data directory.
13
+ *
14
+ * Owned by the @hasna/paths-based app-home resolver (app-home.ts), re-exported
15
+ * here so every existing reader keeps agreeing on the name.
16
+ */
17
+ export { DATA_DIR_ENV } from "./app-home.js";
11
18
  /**
12
19
  * There is no deployment "mode" key.
13
20
  *
@@ -35,14 +42,6 @@ export interface SkillsConfig {
35
42
  extensionsDir?: string;
36
43
  }
37
44
  export type ConfigScope = "global" | "project";
38
- /**
39
- * Environment variable that relocates the skills data directory.
40
- *
41
- * Exported so that every reader agrees on the name: previously the literal was
42
- * duplicated in portable-skills.ts and honoured there but *not* in getDataDir(),
43
- * which is what made the override only half work (see getDataDir below).
44
- */
45
- export declare const DATA_DIR_ENV = "HASNA_SKILLS_DIR";
46
45
  /**
47
46
  * Subfolder of the data directory holding the installed skill corpus.
48
47
  *
@@ -92,8 +91,8 @@ export declare function getDataDir(): string;
92
91
  *
93
92
  * getDataDir() itself writes: it mkdirs the app folder, merges legacy ~/.skills
94
93
  * content and copies the legacy config file. A dry run must resolve the SAME
95
- * directory a real run would use without performing any of that — mirror the path
96
- * logic only, reading $HASNA_SKILLS_DIR and $HOME exactly the way getDataDir does.
94
+ * directory a real run would use without performing any of that — the app-home
95
+ * resolver (getDataRoot) is already write-free, so this mirrors it directly.
97
96
  */
98
97
  export declare function getDataDirReadOnly(): string;
99
98
  /**
@@ -20,3 +20,9 @@ export interface FeedbackResult {
20
20
  */
21
21
  export declare function getFeedbackDbPath(): string;
22
22
  export declare function saveFeedback(input: FeedbackInput): FeedbackResult;
23
+ /**
24
+ * True when this install is pointed at a Skills instance: the env var, its HASNA_-prefixed
25
+ * alias, or the config file written by `skills setup --api-url` / `skills login`. The check
26
+ * never throws - a broken config file means local mode, not a crash in `skills feedback`.
27
+ */
28
+ export declare function isApiMode(env?: Record<string, string | undefined>): boolean;
@@ -16,6 +16,8 @@ export interface InstallResult {
16
16
  path?: string;
17
17
  mode?: InstallMode;
18
18
  source?: InstallSource;
19
+ /** Exact published version when the caller asked for name@version (hasna/apps#1630). */
20
+ version?: string;
19
21
  }
20
22
  export interface InstallOptions {
21
23
  targetDir?: string;
@@ -25,7 +25,15 @@ export interface SkillPullClient {
25
25
  * `Response` is the natural transport type here: verification reads the
26
26
  * X-Skill-Bundle-Sha256 / X-Skill-Bundle-Signature headers off it.
27
27
  */
28
- getBundle(slug: string): Promise<Response | null>;
28
+ getBundle(slug: string, version?: string): Promise<Response | null>;
29
+ /**
30
+ * The registry's record of one published version (hasna/apps#1630), or null. Optional:
31
+ * an exact-version pull proves the received bytes against this digest; a client that
32
+ * cannot answer it falls back to the bundle's own digest header.
33
+ */
34
+ getSkillVersion?(slug: string, version: string): Promise<{
35
+ bundleSha256?: string;
36
+ } | null>;
29
37
  }
30
38
  export interface PullSkillsOptions extends PortableSkillOptions {
31
39
  /** Explicit skill names to pull. Ignored when `all` is set. */
@@ -136,3 +144,12 @@ export declare function writePullMarker(dir: string, record: {
136
144
  revisionId?: string;
137
145
  source?: "pull" | "sync";
138
146
  }): void;
147
+ /**
148
+ * Split `name@version` into its parts. A name without `@` pulls the current revision; an
149
+ * empty version (`name@`) is an error the caller reports. Scoped-looking names are not a
150
+ * concern here: skill slugs never start with `@`.
151
+ */
152
+ export declare function splitNameVersion(raw: string): {
153
+ name: string;
154
+ version?: string;
155
+ };
@@ -22,6 +22,16 @@ export declare class RemoteRequestError extends Error {
22
22
  * (`{ slug, pinnedAt, metadata }`). `pinnedAt`/`metadata` are server-reported
23
23
  * and may be absent.
24
24
  */
25
+ export interface RemoteSkillVersion {
26
+ slug: string;
27
+ version: string;
28
+ bundleSha256: string;
29
+ bundleByteSize: number;
30
+ storageKind?: string;
31
+ manifest?: Record<string, unknown>;
32
+ createdAt: string;
33
+ current?: boolean;
34
+ }
25
35
  export interface RemotePin {
26
36
  slug: string;
27
37
  pinnedAt?: string;
@@ -109,8 +119,12 @@ export declare class RemoteSkillsClient {
109
119
  * read the X-Skill-Bundle-Sha256 / X-Skill-Bundle-Signature headers, or null when the
110
120
  * instance serves no bundle for this skill (the metadata-only fallback path).
111
121
  */
112
- getBundle(slug: string): Promise<Response | null>;
122
+ getBundle(slug: string, version?: string): Promise<Response | null>;
113
123
  /** List the pins the instance holds for this principal. */
124
+ /** Every published version of a slug, newest first (hasna/apps#1630). */
125
+ listSkillVersions(slug: string): Promise<RemoteSkillVersion[]>;
126
+ /** One version's manifest, or null when the slug@version was never published. */
127
+ getSkillVersion(slug: string, version: string): Promise<RemoteSkillVersion | null>;
114
128
  listPins(): Promise<RemotePin[]>;
115
129
  /**
116
130
  * Pin a skill on the instance (upsert — pinning again refreshes it). The
@@ -0,0 +1,11 @@
1
+ /**
2
+ * What a skill version string may look like (hasna/apps#1630).
3
+ *
4
+ * The version becomes a path segment in the version-addressed object key and in the
5
+ * `/versions/:version` routes, so it is a closed alphabet rather than "whatever the
6
+ * publisher sent": no separators, no dot-only names, bounded length. Semver fits; so do
7
+ * date stamps and package-style prereleases.
8
+ */
9
+ export declare const SKILL_VERSION_PATTERN: RegExp;
10
+ export declare function isValidSkillVersion(value: unknown): boolean;
11
+ export declare const SKILL_VERSION_RULE = "1-128 characters: letters, digits, '.', '_', '+', '-'; must start with a letter or digit; no '..'";
@@ -20,6 +20,8 @@ export interface HydrationCandidate {
20
20
  mtimeMs: number;
21
21
  /** The manifest's recorded sha256 for (agent, home-relative path), if any. */
22
22
  manifestHash: string | null;
23
+ /** True when the candidate's bytes hash to the manifest record (verified match). */
24
+ verified: boolean;
23
25
  }
24
26
  export interface HydrationWinnerFile {
25
27
  withinIdent: string;
@@ -11,7 +11,7 @@ export declare const STATION_SNAPSHOT_PRODUCER: {
11
11
  name: string;
12
12
  version: string;
13
13
  };
14
- export type StationSnapshotErrorCode = "INVALID_STATION" | "SYMLINKS_REFUSED" | "CONFLICT" | "DESTINATION_ESCAPE" | "MANIFEST_UNREADABLE";
14
+ export type StationSnapshotErrorCode = "INVALID_STATION" | "SYMLINKS_REFUSED" | "CONFLICT" | "DESTINATION_ESCAPE" | "MANIFEST_UNREADABLE" | "MANIFEST_HASH_MISMATCH";
15
15
  export declare class StationSnapshotError extends Error {
16
16
  readonly code: StationSnapshotErrorCode;
17
17
  /** Per-item lines for the CONFLICT class (printed before the summary). */