@hasna/skills 0.1.70 → 0.1.72
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/bin/index.js +1376 -852
- package/bin/mcp.js +342 -232
- package/bin/migrate.js +149 -40
- package/bin/server.js +212 -100
- package/bin/worker.js +151 -45
- package/dist/cli/commands/hydrate.d.ts +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1058 -304
- package/dist/lib/app-home.d.ts +85 -0
- package/dist/lib/config.d.ts +9 -10
- package/dist/lib/portable-snapshot-filter.d.ts +48 -0
- package/dist/lib/station-hydrate.d.ts +105 -0
- package/dist/lib/station-snapshot.d.ts +96 -0
- package/dist/sdk/index.js +518 -630
- package/dist/storage.js +155 -43
- package/package.json +2 -1
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
/** Env var that relocates the skills data directory (the shipped override). */
|
|
21
|
+
export declare const DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
22
|
+
/** Wave-convention exact-app home aliases, honoured after HASNA_SKILLS_DIR. */
|
|
23
|
+
export declare const HASNA_SKILLS_HOME_ENV = "HASNA_SKILLS_HOME";
|
|
24
|
+
export declare const SKILLS_HOME_ENV = "SKILLS_HOME";
|
|
25
|
+
/** Filename of the default SQLite database inside the skills data directory. */
|
|
26
|
+
export declare const DEFAULT_SQLITE_FILENAME = "server.db";
|
|
27
|
+
/** Filename of the global config file inside the skills data directory. */
|
|
28
|
+
export declare const GLOBAL_CONFIG_FILENAME = "config.json";
|
|
29
|
+
/**
|
|
30
|
+
* The effective user home, mirroring the pre-existing skills resolution
|
|
31
|
+
* (`HOME` || `USERPROFILE` || `os.homedir()`). Read at call time so a runtime
|
|
32
|
+
* `HOME` reassignment (e.g. a test temp home) is honoured — `os.homedir()`
|
|
33
|
+
* snapshots `HOME` at process start and, under Bun, ignores later changes.
|
|
34
|
+
*/
|
|
35
|
+
export declare function effectiveHome(): string;
|
|
36
|
+
/** The legacy (pre-XDG) data root: `~/.hasna/skills`. */
|
|
37
|
+
export declare function legacyDataRoot(): string;
|
|
38
|
+
/**
|
|
39
|
+
* The @hasna/paths-resolved (XDG / macOS home layout) data root for skills:
|
|
40
|
+
* `~/.local/share/hasna/skills` on Linux, `~/Library/Application
|
|
41
|
+
* Support/Hasna/skills` on macOS. The home override mirrors the pre-existing
|
|
42
|
+
* `$HOME`-first resolution so the resolver follows the same home the legacy
|
|
43
|
+
* path does.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolverDataRoot(home?: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the resolver (XDG) data root should be adopted as the effective
|
|
48
|
+
* data root. The resolver root is adopted only when the operator has set
|
|
49
|
+
* `HASNA_DATA_HOME` (the data-kind override — a deliberate opt-in to the XDG
|
|
50
|
+
* layout) or the store has already been physically migrated there (`server.db`
|
|
51
|
+
* — the default SQLite store — or `config.json` exists). A machine that only
|
|
52
|
+
* redirects another kind (e.g. cache to tmpfs) must NOT have its data home
|
|
53
|
+
* moved, and a live store at the legacy home must never become invisible on
|
|
54
|
+
* upgrade.
|
|
55
|
+
*/
|
|
56
|
+
export declare function adoptResolverDataRoot(resolved: string, env?: NodeJS.ProcessEnv): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* The exact-app override root, when set: the shipped `HASNA_SKILLS_DIR` wins,
|
|
59
|
+
* then the wave-convention aliases `HASNA_SKILLS_HOME` and `SKILLS_HOME`.
|
|
60
|
+
* First non-blank override wins; a blank or whitespace-only primary must not
|
|
61
|
+
* shadow a valid secondary (nullish `??` does not fall through on `""`).
|
|
62
|
+
*/
|
|
63
|
+
export declare function exactDataRoot(): string | undefined;
|
|
64
|
+
/** Whether an exact-app override root is set (used to skip legacy migration). */
|
|
65
|
+
export declare function hasExactOverride(env?: NodeJS.ProcessEnv): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Whether the operator named a data root at all — an exact-app override or the
|
|
68
|
+
* data-kind `HASNA_DATA_HOME`. Used to decide when the legacy `~/.skills` /
|
|
69
|
+
* `~/.skillsrc` migration must be skipped: copying a stray legacy tree into an
|
|
70
|
+
* operator-chosen directory would be a surprising write.
|
|
71
|
+
*/
|
|
72
|
+
export declare function hasOperatorOverride(env?: NodeJS.ProcessEnv): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The effective data root: an exact-app override (`HASNA_SKILLS_DIR`, then
|
|
75
|
+
* `HASNA_SKILLS_HOME` / `SKILLS_HOME`) wins unconditionally; otherwise the
|
|
76
|
+
* resolver (XDG) data root once adopted; otherwise the legacy `~/.hasna/skills`
|
|
77
|
+
* default. Write-free: callers that need the directory to exist create it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function getDataRoot(): string;
|
|
80
|
+
/**
|
|
81
|
+
* The skills app data root for an explicit home root, mirroring getDataRoot()
|
|
82
|
+
* with the home injected. Used by the sync-home snapshot mapping to enumerate
|
|
83
|
+
* the skills corpus under a staged home mirror (`homesRoot`) or the real home.
|
|
84
|
+
*/
|
|
85
|
+
export declare function skillsDataRootForHome(home: string): string;
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -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 —
|
|
96
|
-
*
|
|
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
|
/**
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SyncAgent } from "./agent-sync.js";
|
|
2
|
+
/** A snapshot source: one directory of installed skills. */
|
|
3
|
+
export interface SyncHomeDefinition {
|
|
4
|
+
/** Directory name used in the snapshot destination and reporting. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Snapshot category: the standalone stores, or one of the agent homes. */
|
|
7
|
+
subClass: "skills" | "custom" | "agent-homes";
|
|
8
|
+
/** The coding agent whose home this is, when subClass is agent-homes. */
|
|
9
|
+
agent: SyncAgent | null;
|
|
10
|
+
}
|
|
11
|
+
/** The seven installed skill homes the per-station snapshot covers. */
|
|
12
|
+
export declare const SYNC_HOMES: readonly SyncHomeDefinition[];
|
|
13
|
+
/**
|
|
14
|
+
* Scanner-flagged portable files the fleet conformance gate refuses. They are
|
|
15
|
+
* absent from the snapshots by construction; the check stays as defense in
|
|
16
|
+
* depth. See the header comment for the provenance and retirement path.
|
|
17
|
+
*/
|
|
18
|
+
export declare const REFUSED_SCANNER_FLAGGED: Set<string>;
|
|
19
|
+
export declare function isExcludedSkillFileName(fileName: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The portable filter: only files at `<ident>/SKILL.md`, `<ident>/skill.json`,
|
|
22
|
+
* and `<ident>/scripts|assets|references/*` may reach a snapshot or the cache.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isPortableWithinSkill(relativeParts: string[]): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The on-disk location of one of the seven snapshot homes. `homesRoot` stages
|
|
27
|
+
* a copy of the homes (e.g. an rsync'd mirror of a remote station); when
|
|
28
|
+
* absent, this machine's real `$HOME` is the root. The staged layout mirrors
|
|
29
|
+
* the home mapping: `<dir>/skills`, `<dir>/<agent>/skills`,
|
|
30
|
+
* `<dir>/opencode/skills`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function homePathFor(definition: SyncHomeDefinition, homesRoot?: string): string;
|
|
33
|
+
/** Snapshot destination inside the repo, relative to the repo root. */
|
|
34
|
+
export declare function destinationFor(definition: SyncHomeDefinition, stationId: string, relativePath: string): string;
|
|
35
|
+
export interface WalkEntry {
|
|
36
|
+
kind: "file" | "symlink";
|
|
37
|
+
relativePath: string;
|
|
38
|
+
fullPath: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Recursive walk that records symlinks instead of following them, and skips
|
|
42
|
+
* the excluded directory names/patterns. A missing root reads as empty — the
|
|
43
|
+
* same contract as the source script, where an absent home is a zero-file
|
|
44
|
+
* home, not an error.
|
|
45
|
+
*/
|
|
46
|
+
export declare function walkEntries(absoluteRoot: string): WalkEntry[];
|
|
47
|
+
/** True when a path exists and is a regular file. */
|
|
48
|
+
export declare function isRegularFile(filePath: string): boolean;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type SyncAgent } from "./agent-sync.js";
|
|
2
|
+
/**
|
|
3
|
+
* The wire schema of the per-station hydration manifest; keeps the
|
|
4
|
+
* fleet-resources spelling for the same wire-compat reason as the
|
|
5
|
+
* sync-manifest.
|
|
6
|
+
*/
|
|
7
|
+
export declare const STATION_HYDRATION_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-hydration-manifest/v1";
|
|
8
|
+
/** Package identity recorded in the manifest's producer field. */
|
|
9
|
+
export declare const STATION_HYDRATION_PRODUCER: {
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
};
|
|
13
|
+
export interface HydrationCandidate {
|
|
14
|
+
ident: string;
|
|
15
|
+
agent: SyncAgent;
|
|
16
|
+
/** Ident-relative portable path, e.g. `scripts/run.sh`. */
|
|
17
|
+
withinIdent: string;
|
|
18
|
+
fullPath: string;
|
|
19
|
+
size: number;
|
|
20
|
+
mtimeMs: number;
|
|
21
|
+
/** The manifest's recorded sha256 for (agent, home-relative path), if any. */
|
|
22
|
+
manifestHash: string | null;
|
|
23
|
+
/** True when the candidate's bytes hash to the manifest record (verified match). */
|
|
24
|
+
verified: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface HydrationWinnerFile {
|
|
27
|
+
withinIdent: string;
|
|
28
|
+
winner: HydrationCandidate;
|
|
29
|
+
/** The losing copies' agents, for reporting. */
|
|
30
|
+
alternates: SyncAgent[];
|
|
31
|
+
}
|
|
32
|
+
export interface HydrationWinnerSkill {
|
|
33
|
+
ident: string;
|
|
34
|
+
files: HydrationWinnerFile[];
|
|
35
|
+
}
|
|
36
|
+
export interface StationHydrationOptions {
|
|
37
|
+
stationId: string;
|
|
38
|
+
/** Repo root holding resources/<stationId>/skills and its sync-manifest. */
|
|
39
|
+
repoRoot?: string;
|
|
40
|
+
/** Destination corpus cache; defaults to the canonical corpus root. */
|
|
41
|
+
cacheRoot?: string;
|
|
42
|
+
/** Report without writing anything (the default, as in the source script). */
|
|
43
|
+
dryRun?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface StationHydrationResult {
|
|
46
|
+
stationId: string;
|
|
47
|
+
mode: "dry-run" | "apply";
|
|
48
|
+
cacheRoot: string;
|
|
49
|
+
snapshotRoot: string;
|
|
50
|
+
sourceSnapshotSha: string;
|
|
51
|
+
stats: {
|
|
52
|
+
idents: number;
|
|
53
|
+
files: number;
|
|
54
|
+
bytes: number;
|
|
55
|
+
written?: number;
|
|
56
|
+
unchanged?: number;
|
|
57
|
+
};
|
|
58
|
+
/** The full winner plan, for reporting (never written to the manifest). */
|
|
59
|
+
winners: HydrationWinnerSkill[];
|
|
60
|
+
/** The manifest payload shape: per-skill files plus the skill's sha256. */
|
|
61
|
+
skills: Array<{
|
|
62
|
+
ident: string;
|
|
63
|
+
files: Array<{
|
|
64
|
+
relativePath: string;
|
|
65
|
+
sourceAgent: SyncAgent;
|
|
66
|
+
sourceMtimeMs: number;
|
|
67
|
+
size: number;
|
|
68
|
+
}>;
|
|
69
|
+
sha256: string | null;
|
|
70
|
+
}>;
|
|
71
|
+
manifestPath?: string;
|
|
72
|
+
}
|
|
73
|
+
interface SnapshotManifestFile {
|
|
74
|
+
relativePath: string;
|
|
75
|
+
agent: string | null;
|
|
76
|
+
sha256: string;
|
|
77
|
+
}
|
|
78
|
+
interface SnapshotManifest {
|
|
79
|
+
files?: SnapshotManifestFile[];
|
|
80
|
+
}
|
|
81
|
+
interface HydrationPlan {
|
|
82
|
+
manifest: SnapshotManifest;
|
|
83
|
+
sourceSnapshotSha: string;
|
|
84
|
+
winners: HydrationWinnerSkill[];
|
|
85
|
+
skippedByRule: Array<{
|
|
86
|
+
ident: string;
|
|
87
|
+
agent: SyncAgent;
|
|
88
|
+
relativePath: string;
|
|
89
|
+
reason: string;
|
|
90
|
+
}>;
|
|
91
|
+
totalFiles: number;
|
|
92
|
+
totalBytes: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Build the dedup winner plan from the snapshot. Fails closed on symlinks and
|
|
96
|
+
* on a missing or unreadable sync-manifest.
|
|
97
|
+
*/
|
|
98
|
+
export declare function planStationHydration(stationId: string, repoRoot: string): HydrationPlan;
|
|
99
|
+
/**
|
|
100
|
+
* Run the hydration. Dry-run reports and writes nothing; apply detects
|
|
101
|
+
* conflicts across the whole plan first and only then writes, so the terminal
|
|
102
|
+
* refusal genuinely writes nothing.
|
|
103
|
+
*/
|
|
104
|
+
export declare function writeStationHydration(options: StationHydrationOptions): StationHydrationResult;
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type SyncHomeDefinition } from "./portable-snapshot-filter.js";
|
|
2
|
+
/**
|
|
3
|
+
* The wire schema of the per-station sync-manifest. The name keeps the
|
|
4
|
+
* fleet-resources spelling deliberately: the fleet hydrator (and the snapshot
|
|
5
|
+
* repo's consumers) record and compare this string, so the package-owned
|
|
6
|
+
* producer writes the same value the retired script wrote.
|
|
7
|
+
*/
|
|
8
|
+
export declare const STATION_SYNC_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-sync-manifest/v1";
|
|
9
|
+
/** Package identity recorded in the manifest's producer field. */
|
|
10
|
+
export declare const STATION_SNAPSHOT_PRODUCER: {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
};
|
|
14
|
+
export type StationSnapshotErrorCode = "INVALID_STATION" | "SYMLINKS_REFUSED" | "CONFLICT" | "DESTINATION_ESCAPE" | "MANIFEST_UNREADABLE" | "MANIFEST_HASH_MISMATCH";
|
|
15
|
+
export declare class StationSnapshotError extends Error {
|
|
16
|
+
readonly code: StationSnapshotErrorCode;
|
|
17
|
+
/** Per-item lines for the CONFLICT class (printed before the summary). */
|
|
18
|
+
readonly detail: string[];
|
|
19
|
+
constructor(code: StationSnapshotErrorCode, message: string, detail?: string[]);
|
|
20
|
+
}
|
|
21
|
+
/** Validate a station id the same way the source script does. */
|
|
22
|
+
export declare function validateStationId(stationId: string): void;
|
|
23
|
+
export interface PortableSnapshotFile {
|
|
24
|
+
relativePath: string;
|
|
25
|
+
fullPath: string;
|
|
26
|
+
size: number;
|
|
27
|
+
mtimeMs: number;
|
|
28
|
+
mtimeIso: string;
|
|
29
|
+
}
|
|
30
|
+
export interface SnapshotSkip {
|
|
31
|
+
relativePath: string;
|
|
32
|
+
reason: "symlink" | "not-portable" | "excluded" | "refused-scanner-flagged" | "not-regular-file";
|
|
33
|
+
}
|
|
34
|
+
export interface ScannedHome {
|
|
35
|
+
definition: SyncHomeDefinition;
|
|
36
|
+
homePath: string;
|
|
37
|
+
portable: PortableSnapshotFile[];
|
|
38
|
+
skipped: SnapshotSkip[];
|
|
39
|
+
}
|
|
40
|
+
export interface SnapshotPlan {
|
|
41
|
+
definition: SyncHomeDefinition;
|
|
42
|
+
source: PortableSnapshotFile;
|
|
43
|
+
destination: string;
|
|
44
|
+
digest: string;
|
|
45
|
+
}
|
|
46
|
+
export interface StationSnapshotOptions {
|
|
47
|
+
stationId: string;
|
|
48
|
+
/** Target repo root; the snapshot lands in <repoRoot>/resources/<stationId>/skills. */
|
|
49
|
+
repoRoot?: string;
|
|
50
|
+
/** Stage the homes from a mirror instead of this machine's real $HOME. */
|
|
51
|
+
homesRoot?: string;
|
|
52
|
+
/** Report without writing anything (the default, as in the source script). */
|
|
53
|
+
dryRun?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface StationSnapshotManifestFile {
|
|
56
|
+
relativePath: string;
|
|
57
|
+
destination: string;
|
|
58
|
+
subClass: "skills" | "custom" | "agent-homes";
|
|
59
|
+
agent: string | null;
|
|
60
|
+
sha256: string;
|
|
61
|
+
sourceMtimeMs: number;
|
|
62
|
+
sourceMtimeIso: string;
|
|
63
|
+
size: number;
|
|
64
|
+
}
|
|
65
|
+
export interface StationSnapshotResult {
|
|
66
|
+
stationId: string;
|
|
67
|
+
mode: "dry-run" | "populate";
|
|
68
|
+
repoRoot: string;
|
|
69
|
+
stats: {
|
|
70
|
+
files: number;
|
|
71
|
+
bytes: number;
|
|
72
|
+
written?: number;
|
|
73
|
+
unchanged?: number;
|
|
74
|
+
};
|
|
75
|
+
homes: Array<{
|
|
76
|
+
name: string;
|
|
77
|
+
homePath: string;
|
|
78
|
+
files: number;
|
|
79
|
+
skipped: number;
|
|
80
|
+
}>;
|
|
81
|
+
manifestPath?: string;
|
|
82
|
+
files: StationSnapshotManifestFile[];
|
|
83
|
+
}
|
|
84
|
+
export declare function sha256File(filePath: string): string;
|
|
85
|
+
/** Scan every home and build the write plan; fails closed on symlinks. */
|
|
86
|
+
export declare function planStationSnapshot(options: StationSnapshotOptions): {
|
|
87
|
+
scanned: ScannedHome[];
|
|
88
|
+
plans: SnapshotPlan[];
|
|
89
|
+
totalBytes: number;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Run the per-station snapshot. Dry-run reports and writes nothing; populate
|
|
93
|
+
* detects conflicts across the whole plan first and only then writes, so the
|
|
94
|
+
* terminal refusal genuinely writes nothing.
|
|
95
|
+
*/
|
|
96
|
+
export declare function writeStationSnapshot(options: StationSnapshotOptions): StationSnapshotResult;
|