@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.
- package/README.md +4 -2
- package/bin/index.js +24657 -24681
- package/bin/mcp.js +372 -243
- package/bin/migrate.js +202 -41
- package/bin/server.js +3502 -2898
- package/bin/worker.js +2187 -1806
- package/dist/cli/commands/install.d.ts +16 -0
- package/dist/cli/commands/publish.d.ts +35 -0
- package/dist/cli/commands/registry.d.ts +5 -0
- package/dist/index.js +705 -363
- package/dist/lib/app-home.d.ts +111 -0
- package/dist/lib/config.d.ts +9 -10
- package/dist/lib/feedback.d.ts +6 -0
- package/dist/lib/installer.d.ts +2 -0
- package/dist/lib/pull.d.ts +18 -1
- package/dist/lib/remote-client.d.ts +15 -1
- package/dist/lib/skill-version.d.ts +11 -0
- package/dist/lib/station-hydrate.d.ts +2 -0
- package/dist/lib/station-snapshot.d.ts +1 -1
- package/dist/sdk/index.js +5723 -5340
- package/dist/server/app.d.ts +3 -0
- package/dist/server/artifact-storage.d.ts +27 -0
- package/dist/server/config.d.ts +2 -0
- package/dist/server/rows.d.ts +2 -1
- package/dist/server/seed-bundled.d.ts +20 -0
- package/dist/server/skills-api.d.ts +18 -1
- package/dist/server/sqlite-store.d.ts +3 -1
- package/dist/server/store.d.ts +6 -1
- package/dist/server/types.d.ts +43 -0
- package/dist/storage.js +156 -43
- package/migrations/postgres/0006_skill_versions.sql +30 -0
- package/migrations/sqlite/0006_skill_versions.sql +17 -0
- package/package.json +2 -2
package/dist/server/app.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type GovernanceStore } from "../sdk/governance-store.js";
|
|
2
|
+
import { ArtifactStorage } from "./artifact-storage.js";
|
|
2
3
|
import { type SkillsServerConfig } from "./config.js";
|
|
3
4
|
import { type MemorySkillsStore } from "./store.js";
|
|
4
5
|
import { type SkillsProductStore } from "./types.js";
|
|
5
6
|
export interface SkillsServerOptions {
|
|
7
|
+
/** Overrides the artifact storage (tests inject an in-memory S3 stand-in). */
|
|
8
|
+
artifactStorage?: ArtifactStorage;
|
|
6
9
|
config?: Partial<SkillsServerConfig>;
|
|
7
10
|
store?: SkillsProductStore;
|
|
8
11
|
/** Lifecycle ledger and ceiling reads for governance surfaces (cancellation). Defaults to the store's database. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
1
2
|
import { type OwnedBytes } from "../lib/skill-bundle.js";
|
|
2
3
|
import type { BlobStorageKind, ServerArtifact, ServerRunRecord, ServerSkillBundle } from "./types.js";
|
|
3
4
|
export interface ArtifactBody {
|
|
@@ -5,10 +6,18 @@ export interface ArtifactBody {
|
|
|
5
6
|
bodyText: string;
|
|
6
7
|
contentType: string;
|
|
7
8
|
}
|
|
9
|
+
/** The slice of the AWS client the storage uses; injectable so tests can stand in an in-memory bucket. */
|
|
10
|
+
export interface S3ClientLike {
|
|
11
|
+
send(command: PutObjectCommand | GetObjectCommand | DeleteObjectCommand): Promise<{
|
|
12
|
+
Body?: unknown;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
8
15
|
export interface ArtifactStorageOptions {
|
|
9
16
|
bucket?: string;
|
|
10
17
|
prefix?: string;
|
|
11
18
|
region?: string;
|
|
19
|
+
/** Overrides the real S3 client (tests). Ignored when no bucket is configured. */
|
|
20
|
+
client?: S3ClientLike;
|
|
12
21
|
}
|
|
13
22
|
export declare class ArtifactStorage {
|
|
14
23
|
private bucket?;
|
|
@@ -98,5 +107,23 @@ export declare class ArtifactStorage {
|
|
|
98
107
|
*/
|
|
99
108
|
moveToQuarantine(artifact: ServerArtifact): Promise<string | null>;
|
|
100
109
|
private keyFor;
|
|
110
|
+
/**
|
|
111
|
+
* Version-addressed copy of a published bundle plus its manifest (hasna/apps#1630):
|
|
112
|
+
* <prefix>/skills/<org>/<slug>/<version>/bundle.tar.gz
|
|
113
|
+
* <prefix>/skills/<org>/<slug>/<version>/manifest.json
|
|
114
|
+
* The content-addressed object under bundles/ stays the read path (dedupe); these keys
|
|
115
|
+
* are the durable, browsable history and are never deleted by orphan collection.
|
|
116
|
+
* Returns the placement recorded on the version row; in db mode nothing is written.
|
|
117
|
+
*/
|
|
118
|
+
putVersionObjects(orgId: string, slug: string, version: string, bytes: OwnedBytes, manifest: Record<string, unknown>, contentType?: string): Promise<{
|
|
119
|
+
storageKind: BlobStorageKind;
|
|
120
|
+
storageKey?: string;
|
|
121
|
+
}>;
|
|
122
|
+
/** Where putVersionObjects WOULD place a version, without writing: recorded on the row before the objects exist. */
|
|
123
|
+
versionPlacement(orgId: string, slug: string, version: string): {
|
|
124
|
+
storageKind: BlobStorageKind;
|
|
125
|
+
storageKey?: string;
|
|
126
|
+
};
|
|
127
|
+
versionKeyFor(orgId: string, slug: string, version: string, file: string): string;
|
|
101
128
|
private bundleKeyFor;
|
|
102
129
|
}
|
package/dist/server/config.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface SkillsServerConfig {
|
|
|
3
3
|
port: number;
|
|
4
4
|
databaseUrl?: string;
|
|
5
5
|
bootstrapApiKey?: string;
|
|
6
|
+
/** Publish the bundled corpus as versioned skills on boot (default on; set HASNA_SKILLS_SEED_BUNDLED_CORPUS=0 to skip). */
|
|
7
|
+
seedBundledCorpus: boolean;
|
|
6
8
|
artifactBucket?: string;
|
|
7
9
|
artifactPrefix: string;
|
|
8
10
|
inlineWorker: boolean;
|
package/dist/server/rows.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord } from "./types.js";
|
|
1
|
+
import type { ServerSkillVersion, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord } from "./types.js";
|
|
2
2
|
export declare function nowIso(): string;
|
|
3
3
|
/**
|
|
4
4
|
* Coerce a caller-supplied row limit to a non-negative integer.
|
|
@@ -22,3 +22,4 @@ export declare function rowToSkillBundle(row: Record<string, unknown>): ServerSk
|
|
|
22
22
|
export declare function parseJsonObject(value: unknown): Record<string, unknown>;
|
|
23
23
|
export declare function parseJsonArray(value: unknown): string[];
|
|
24
24
|
export declare function dateString(value: unknown): string;
|
|
25
|
+
export declare function rowToSkillVersion(row: Record<string, unknown>): ServerSkillVersion;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ArtifactStorage } from "./artifact-storage.js";
|
|
2
|
+
import type { ApiPrincipal, SkillsProductStore } from "./types.js";
|
|
3
|
+
export interface SeedBundledCorpusOptions {
|
|
4
|
+
store: SkillsProductStore;
|
|
5
|
+
artifactStorage: ArtifactStorage;
|
|
6
|
+
principal: ApiPrincipal;
|
|
7
|
+
/** Version recorded for every seeded skill. Defaults to the package version. */
|
|
8
|
+
version?: string;
|
|
9
|
+
log?: (line: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface SeedBundledCorpusResult {
|
|
12
|
+
version: string;
|
|
13
|
+
seeded: string[];
|
|
14
|
+
skipped: string[];
|
|
15
|
+
failed: Array<{
|
|
16
|
+
slug: string;
|
|
17
|
+
error: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export declare function seedBundledCorpus(options: SeedBundledCorpusOptions): Promise<SeedBundledCorpusResult>;
|
|
@@ -2,7 +2,7 @@ import { type OwnedBytes } from "../lib/skill-bundle.js";
|
|
|
2
2
|
import type { SkillMeta } from "../lib/registry-types.js";
|
|
3
3
|
import type { ArtifactStorage } from "./artifact-storage.js";
|
|
4
4
|
import type { SkillsServerConfig } from "./config.js";
|
|
5
|
-
import { type ApiPrincipal, type PublishSkillInput, type ServerPin, type ServerSkillRecord, type SkillsProductStore } from "./types.js";
|
|
5
|
+
import { type ApiPrincipal, type PublishSkillInput, type ServerPin, type ServerSkillRecord, type ServerSkillVersion, type SkillsProductStore } from "./types.js";
|
|
6
6
|
/** Wire shape of a pin: the client-facing facts, without the storage columns. */
|
|
7
7
|
export declare function pinPayload(pin: ServerPin): Record<string, unknown>;
|
|
8
8
|
/**
|
|
@@ -167,4 +167,21 @@ export declare function readPublishedBundle(store: SkillsProductStore, artifactS
|
|
|
167
167
|
}>;
|
|
168
168
|
export declare function assertPublishableSlug(slug: string): void;
|
|
169
169
|
export declare function assertSha256(value: string): void;
|
|
170
|
+
/**
|
|
171
|
+
* Versions surface (hasna/apps#1630).
|
|
172
|
+
*
|
|
173
|
+
* GET /api/v1/skills/:slug/versions -> { slug, current, versions: [...] }
|
|
174
|
+
* GET /api/v1/skills/:slug/versions/:version -> one version's manifest
|
|
175
|
+
* GET /api/v1/skills/:slug/versions/:version/bundle -> the exact bytes of that version
|
|
176
|
+
*
|
|
177
|
+
* The registry row is the "current" pointer; a version's bytes come from the
|
|
178
|
+
* content-addressed bundle store so db-mode instances serve history exactly like S3 ones.
|
|
179
|
+
*/
|
|
180
|
+
export declare function skillVersionPayload(version: ServerSkillVersion, currentSha?: string): Record<string, unknown>;
|
|
181
|
+
export declare function listSkillVersionsPayload(store: SkillsProductStore, principal: ApiPrincipal, slug: string): Promise<Record<string, unknown>>;
|
|
182
|
+
export declare function readSkillVersion(store: SkillsProductStore, principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion>;
|
|
183
|
+
export declare function readSkillVersionBundle(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, slug: string, version: string): Promise<{
|
|
184
|
+
version: ServerSkillVersion;
|
|
185
|
+
bytes: OwnedBytes;
|
|
186
|
+
}>;
|
|
170
187
|
export {};
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* requirement; changing Postgres behaviour is not this module's job.
|
|
14
14
|
*/
|
|
15
15
|
import { Database } from "bun:sqlite";
|
|
16
|
-
import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
|
|
16
|
+
import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillVersion, ServerSkillRecord, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
|
|
17
17
|
export interface SqliteStoreOptions {
|
|
18
18
|
/** Apply pending migrations on open. Default true - it is what makes zero-config work. */
|
|
19
19
|
migrate?: boolean;
|
|
@@ -102,6 +102,8 @@ export declare class SqliteSkillsStore implements SkillsProductStore {
|
|
|
102
102
|
deleteSkill(principal: ApiPrincipal, slug: string, tombstoneWindowMs: number): Promise<ServerSkillRecord | null>;
|
|
103
103
|
purgeExpiredTombstones(principal: ApiPrincipal): Promise<ServerSkillRecord[]>;
|
|
104
104
|
getSkillBundle(principal: ApiPrincipal, sha256: string): Promise<ServerSkillBundle | null>;
|
|
105
|
+
listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
|
|
106
|
+
getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
|
|
105
107
|
pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
|
|
106
108
|
unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
|
|
107
109
|
listPins(principal: ApiPrincipal): Promise<ServerPin[]>;
|
package/dist/server/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
|
|
1
|
+
import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, ServerSkillVersion, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
|
|
2
2
|
import { type SqliteStoreOptions } from "./sqlite-store.js";
|
|
3
3
|
export declare function createArtifactId(): string;
|
|
4
4
|
export interface StoreOptions {
|
|
@@ -31,6 +31,7 @@ export declare class MemorySkillsStore implements SkillsProductStore {
|
|
|
31
31
|
private idempotency;
|
|
32
32
|
private skills;
|
|
33
33
|
private bundles;
|
|
34
|
+
private versions;
|
|
34
35
|
private pins;
|
|
35
36
|
constructor(apiKeys?: Array<{
|
|
36
37
|
token: string;
|
|
@@ -67,6 +68,8 @@ export declare class MemorySkillsStore implements SkillsProductStore {
|
|
|
67
68
|
listArtifacts(principal: ApiPrincipal, runId: string): Promise<ServerArtifact[]>;
|
|
68
69
|
getArtifact(principal: ApiPrincipal, runId: string, id: string): Promise<ServerArtifact | null>;
|
|
69
70
|
publishSkill(input: PublishSkillInput): Promise<ServerSkillRecord>;
|
|
71
|
+
listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
|
|
72
|
+
getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
|
|
70
73
|
listSkills(principal: ApiPrincipal): Promise<ServerSkillRecord[]>;
|
|
71
74
|
getSkill(principal: ApiPrincipal, slug: string): Promise<ServerSkillRecord | null>;
|
|
72
75
|
updateSkill(principal: ApiPrincipal, slug: string, patch: UpdateSkillPatch, expectedRevisionId?: string): Promise<ServerSkillRecord | null>;
|
|
@@ -158,6 +161,8 @@ export declare class PostgresSkillsStore implements SkillsProductStore {
|
|
|
158
161
|
deleteSkill(principal: ApiPrincipal, slug: string, tombstoneWindowMs: number): Promise<ServerSkillRecord | null>;
|
|
159
162
|
purgeExpiredTombstones(principal: ApiPrincipal): Promise<ServerSkillRecord[]>;
|
|
160
163
|
getSkillBundle(principal: ApiPrincipal, sha256: string): Promise<ServerSkillBundle | null>;
|
|
164
|
+
listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
|
|
165
|
+
getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
|
|
161
166
|
pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
|
|
162
167
|
unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
|
|
163
168
|
listPins(principal: ApiPrincipal): Promise<ServerPin[]>;
|
package/dist/server/types.d.ts
CHANGED
|
@@ -31,6 +31,18 @@ export declare class SkillRevisionConflictError extends Error {
|
|
|
31
31
|
readonly currentRevisionId: string | null;
|
|
32
32
|
constructor(slug: string, expectedRevisionId: string | null | undefined, currentRevisionId: string | null);
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* A publish was refused because (org, slug, version) already exists with a DIFFERENT bundle
|
|
36
|
+
* digest. Versions are immutable (hasna/apps#1630): the same digest is idempotent, a new
|
|
37
|
+
* digest needs a new version. Carries the stored digest so the client can say which.
|
|
38
|
+
*/
|
|
39
|
+
export declare class SkillVersionExistsError extends Error {
|
|
40
|
+
readonly slug: string;
|
|
41
|
+
readonly version: string;
|
|
42
|
+
readonly existingSha256: string;
|
|
43
|
+
readonly attemptedSha256: string;
|
|
44
|
+
constructor(slug: string, version: string, existingSha256: string, attemptedSha256: string);
|
|
45
|
+
}
|
|
34
46
|
export interface ApiPrincipal {
|
|
35
47
|
apiKeyId: string;
|
|
36
48
|
orgId: string;
|
|
@@ -182,6 +194,24 @@ export interface ServerSkillBundle {
|
|
|
182
194
|
bytes?: OwnedBytes;
|
|
183
195
|
createdAt: string;
|
|
184
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* One immutable published version of a skill (hasna/apps#1630). The registry row is the
|
|
199
|
+
* mutable "current" pointer; these rows are the history: each (org, slug, version) exactly once,
|
|
200
|
+
* pointing at a content-addressed bundle that orphan collection must keep alive.
|
|
201
|
+
*/
|
|
202
|
+
export interface ServerSkillVersion {
|
|
203
|
+
orgId: string;
|
|
204
|
+
slug: string;
|
|
205
|
+
version: string;
|
|
206
|
+
bundleSha256: string;
|
|
207
|
+
bundleByteSize: number;
|
|
208
|
+
storageKind: BlobStorageKind;
|
|
209
|
+
storageKey?: string;
|
|
210
|
+
/** Files (path -> sha256), byte counts and provenance the publisher sent; never secret. */
|
|
211
|
+
manifest: Record<string, unknown>;
|
|
212
|
+
publishedByUserId?: string;
|
|
213
|
+
createdAt: string;
|
|
214
|
+
}
|
|
185
215
|
export interface PublishSkillInput {
|
|
186
216
|
principal: ApiPrincipal;
|
|
187
217
|
slug: string;
|
|
@@ -195,6 +225,16 @@ export interface PublishSkillInput {
|
|
|
195
225
|
skillMd?: string;
|
|
196
226
|
/** Omitted for a metadata-only publish or update. */
|
|
197
227
|
bundle?: Omit<ServerSkillBundle, "orgId" | "createdAt">;
|
|
228
|
+
/**
|
|
229
|
+
* Publisher-supplied version manifest (file digests, provenance). Stored verbatim on the
|
|
230
|
+
* version row when `version` is set; ignored otherwise.
|
|
231
|
+
*/
|
|
232
|
+
versionManifest?: Record<string, unknown>;
|
|
233
|
+
/** Version-addressed object key the API wrote (S3 mode); recorded on the version row. */
|
|
234
|
+
versionStorage?: {
|
|
235
|
+
storageKind: BlobStorageKind;
|
|
236
|
+
storageKey?: string;
|
|
237
|
+
};
|
|
198
238
|
/**
|
|
199
239
|
* Optimistic-concurrency guard (todos d061fcda): the revision_id (ETag) the writer
|
|
200
240
|
* read. A publish against an existing, LIVE row requires the guard to name that row's
|
|
@@ -320,6 +360,9 @@ export interface SkillsProductStore {
|
|
|
320
360
|
*/
|
|
321
361
|
purgeExpiredTombstones(principal: ApiPrincipal): Promise<ServerSkillRecord[]>;
|
|
322
362
|
getSkillBundle(principal: ApiPrincipal, sha256: string): Promise<ServerSkillBundle | null>;
|
|
363
|
+
/** Every published version of a slug, newest first (hasna/apps#1630). */
|
|
364
|
+
listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
|
|
365
|
+
getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
|
|
323
366
|
pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
|
|
324
367
|
/** False when this principal has no pin by that slug. */
|
|
325
368
|
unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
|
package/dist/storage.js
CHANGED
|
@@ -49,19 +49,18 @@ var __require = import.meta.require;
|
|
|
49
49
|
// src/lib/native-storage.ts
|
|
50
50
|
import { createHash, createHmac } from "crypto";
|
|
51
51
|
import {
|
|
52
|
-
existsSync as
|
|
52
|
+
existsSync as existsSync4,
|
|
53
53
|
mkdirSync as mkdirSync3,
|
|
54
54
|
readFileSync as readFileSync3,
|
|
55
55
|
readdirSync as readdirSync2,
|
|
56
56
|
statSync as statSync2,
|
|
57
57
|
writeFileSync as writeFileSync3
|
|
58
58
|
} from "fs";
|
|
59
|
-
import { dirname as dirname2, join as
|
|
59
|
+
import { dirname as dirname2, join as join4, normalize, relative, sep } from "path";
|
|
60
60
|
|
|
61
61
|
// src/lib/config.ts
|
|
62
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
63
|
-
import { join, dirname } from "path";
|
|
64
|
-
import { homedir } from "os";
|
|
62
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
63
|
+
import { join as join2, dirname } from "path";
|
|
65
64
|
|
|
66
65
|
// src/lib/retired-settings.ts
|
|
67
66
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
@@ -109,6 +108,124 @@ function assertNoRetiredConfigKeys(config, source) {
|
|
|
109
108
|
}
|
|
110
109
|
}
|
|
111
110
|
|
|
111
|
+
// src/lib/app-home.ts
|
|
112
|
+
import { existsSync } from "fs";
|
|
113
|
+
import { homedir } from "os";
|
|
114
|
+
import { join, resolve } from "path";
|
|
115
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
116
|
+
import { join as pathsResolverJoin } from "path";
|
|
117
|
+
var PATHS_RESOLVER_KIND_ENV = {
|
|
118
|
+
config: "HASNA_CONFIG_HOME",
|
|
119
|
+
data: "HASNA_DATA_HOME",
|
|
120
|
+
state: "HASNA_STATE_HOME",
|
|
121
|
+
cache: "HASNA_CACHE_HOME"
|
|
122
|
+
};
|
|
123
|
+
var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
124
|
+
function pathsResolverAssertApp(app) {
|
|
125
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
126
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
127
|
+
}
|
|
128
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
129
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function pathsResolverAssertKind(kind) {
|
|
133
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
134
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function pathsResolverBaseDir(kind, options) {
|
|
138
|
+
pathsResolverAssertKind(kind);
|
|
139
|
+
const env = options.env ?? process.env;
|
|
140
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
141
|
+
if (typeof override === "string" && override.length > 0)
|
|
142
|
+
return override;
|
|
143
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
144
|
+
const platform = options.platform ?? process.platform;
|
|
145
|
+
if (platform === "darwin") {
|
|
146
|
+
switch (kind) {
|
|
147
|
+
case "config":
|
|
148
|
+
case "data":
|
|
149
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
150
|
+
case "cache":
|
|
151
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
152
|
+
case "state":
|
|
153
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
switch (kind) {
|
|
157
|
+
case "config":
|
|
158
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
159
|
+
case "data":
|
|
160
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
161
|
+
case "state":
|
|
162
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
163
|
+
case "cache":
|
|
164
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function pathsResolverResolve(kind, options) {
|
|
168
|
+
pathsResolverAssertApp(options.app);
|
|
169
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
170
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
171
|
+
}
|
|
172
|
+
function dataDir(options) {
|
|
173
|
+
return pathsResolverResolve("data", options);
|
|
174
|
+
}
|
|
175
|
+
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
176
|
+
var HASNA_SKILLS_HOME_ENV = "HASNA_SKILLS_HOME";
|
|
177
|
+
var SKILLS_HOME_ENV = "SKILLS_HOME";
|
|
178
|
+
var DEFAULT_SQLITE_FILENAME = "server.db";
|
|
179
|
+
var GLOBAL_CONFIG_FILENAME = "config.json";
|
|
180
|
+
function effectiveHome() {
|
|
181
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir() || "/tmp";
|
|
182
|
+
}
|
|
183
|
+
function legacyDataRoot() {
|
|
184
|
+
return join(effectiveHome(), ".hasna", "skills");
|
|
185
|
+
}
|
|
186
|
+
function resolverDataRoot(home = effectiveHome(), env) {
|
|
187
|
+
return dataDir({ app: "skills", home, env });
|
|
188
|
+
}
|
|
189
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
190
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
191
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
192
|
+
return true;
|
|
193
|
+
return existsSync(join(resolved, DEFAULT_SQLITE_FILENAME)) || existsSync(join(resolved, GLOBAL_CONFIG_FILENAME));
|
|
194
|
+
}
|
|
195
|
+
function exactDataRoot() {
|
|
196
|
+
for (const key of [DATA_DIR_ENV, HASNA_SKILLS_HOME_ENV, SKILLS_HOME_ENV]) {
|
|
197
|
+
const dir = process.env[key]?.trim();
|
|
198
|
+
if (dir)
|
|
199
|
+
return resolve(dir);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
function hasExactOverride(env = process.env) {
|
|
204
|
+
return Boolean(env[DATA_DIR_ENV]?.trim()) || Boolean(env[HASNA_SKILLS_HOME_ENV]?.trim()) || Boolean(env[SKILLS_HOME_ENV]?.trim());
|
|
205
|
+
}
|
|
206
|
+
function hasOperatorOverride(env = process.env) {
|
|
207
|
+
return hasExactOverride(env) || Boolean(env.HASNA_DATA_HOME?.trim());
|
|
208
|
+
}
|
|
209
|
+
function getDataRoot() {
|
|
210
|
+
const exact = exactDataRoot();
|
|
211
|
+
if (exact)
|
|
212
|
+
return exact;
|
|
213
|
+
const resolved = resolverDataRoot();
|
|
214
|
+
return adoptResolverDataRoot(resolved) ? resolve(resolved) : resolve(legacyDataRoot());
|
|
215
|
+
}
|
|
216
|
+
function skillsDataRootForHome(home) {
|
|
217
|
+
const isOwnHome = resolve(home) === resolve(effectiveHome()) || resolve(home) === resolve(homedir());
|
|
218
|
+
if (isOwnHome) {
|
|
219
|
+
const exact = exactDataRoot();
|
|
220
|
+
if (exact)
|
|
221
|
+
return exact;
|
|
222
|
+
const resolved2 = resolverDataRoot(home);
|
|
223
|
+
return adoptResolverDataRoot(resolved2) ? resolve(resolved2) : resolve(join(home, ".hasna", "skills"));
|
|
224
|
+
}
|
|
225
|
+
const resolved = resolverDataRoot(home, {});
|
|
226
|
+
return adoptResolverDataRoot(resolved, {}) ? resolve(resolved) : resolve(join(home, ".hasna", "skills"));
|
|
227
|
+
}
|
|
228
|
+
|
|
112
229
|
// src/lib/config.ts
|
|
113
230
|
var ENUM_KEYS = {
|
|
114
231
|
defaultAgent: ["claude", "codex", "gemini", "pi", "opencode", "all"],
|
|
@@ -123,19 +240,19 @@ function allowedValues(key) {
|
|
|
123
240
|
return ENUM_KEYS[key];
|
|
124
241
|
}
|
|
125
242
|
function mergeDirectoryContents(sourceDir, targetDir) {
|
|
126
|
-
if (!
|
|
243
|
+
if (!existsSync2(sourceDir))
|
|
127
244
|
return;
|
|
128
245
|
mkdirSync(targetDir, { recursive: true });
|
|
129
246
|
for (const entry of readdirSync(sourceDir)) {
|
|
130
|
-
const sourcePath =
|
|
131
|
-
const targetPath =
|
|
247
|
+
const sourcePath = join2(sourceDir, entry);
|
|
248
|
+
const targetPath = join2(targetDir, entry);
|
|
132
249
|
try {
|
|
133
250
|
const sourceStat = statSync(sourcePath);
|
|
134
251
|
if (sourceStat.isDirectory()) {
|
|
135
252
|
mergeDirectoryContents(sourcePath, targetPath);
|
|
136
253
|
continue;
|
|
137
254
|
}
|
|
138
|
-
if (!
|
|
255
|
+
if (!existsSync2(targetPath))
|
|
139
256
|
copyFileSync(sourcePath, targetPath);
|
|
140
257
|
} catch {}
|
|
141
258
|
}
|
|
@@ -160,44 +277,40 @@ function normalizeConfigValue(key, value) {
|
|
|
160
277
|
return value.trim() ? value : undefined;
|
|
161
278
|
return;
|
|
162
279
|
}
|
|
163
|
-
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
164
280
|
var INSTALLED_SKILLS_DIRNAME = "installed";
|
|
165
281
|
var SKILLS_CACHE_DIRNAME = "skills";
|
|
166
282
|
var LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
|
|
167
283
|
function isOwnerLayoutMigrated(appDir) {
|
|
168
|
-
return
|
|
284
|
+
return existsSync2(join2(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
|
|
169
285
|
}
|
|
170
286
|
function getDataDir() {
|
|
171
|
-
const
|
|
172
|
-
if (override) {
|
|
173
|
-
try {
|
|
174
|
-
mkdirSync(override, { recursive: true });
|
|
175
|
-
} catch {}
|
|
176
|
-
return override;
|
|
177
|
-
}
|
|
178
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
179
|
-
const newDir = join(home, ".hasna", "skills");
|
|
180
|
-
const oldDir = join(home, ".skills");
|
|
181
|
-
const oldConfigFile = join(home, ".skillsrc");
|
|
182
|
-
mkdirSync(newDir, { recursive: true });
|
|
287
|
+
const root = getDataRoot();
|
|
183
288
|
try {
|
|
184
|
-
|
|
289
|
+
mkdirSync(root, { recursive: true });
|
|
185
290
|
} catch {}
|
|
186
|
-
if (
|
|
291
|
+
if (hasOperatorOverride())
|
|
292
|
+
return root;
|
|
293
|
+
const home = effectiveHome();
|
|
294
|
+
const oldDir = join2(home, ".skills");
|
|
295
|
+
const oldConfigFile = join2(home, ".skillsrc");
|
|
296
|
+
try {
|
|
297
|
+
mergeDirectoryContents(oldDir, root);
|
|
298
|
+
} catch {}
|
|
299
|
+
if (existsSync2(oldConfigFile) && !existsSync2(join2(root, "config.json"))) {
|
|
187
300
|
try {
|
|
188
|
-
copyFileSync(oldConfigFile,
|
|
301
|
+
copyFileSync(oldConfigFile, join2(root, "config.json"));
|
|
189
302
|
} catch {}
|
|
190
303
|
}
|
|
191
|
-
return
|
|
304
|
+
return root;
|
|
192
305
|
}
|
|
193
306
|
function getConfigPath(scope) {
|
|
194
307
|
if (scope === "global") {
|
|
195
|
-
return
|
|
308
|
+
return join2(getDataDir(), "config.json");
|
|
196
309
|
}
|
|
197
|
-
return
|
|
310
|
+
return join2(process.cwd(), "skills.config.json");
|
|
198
311
|
}
|
|
199
312
|
function readConfigFile(path) {
|
|
200
|
-
if (!
|
|
313
|
+
if (!existsSync2(path))
|
|
201
314
|
return {};
|
|
202
315
|
let parsed;
|
|
203
316
|
try {
|
|
@@ -233,7 +346,7 @@ function saveConfig(key, value, scope = "project") {
|
|
|
233
346
|
}
|
|
234
347
|
const filePath = getConfigPath(scope);
|
|
235
348
|
let existing = {};
|
|
236
|
-
if (
|
|
349
|
+
if (existsSync2(filePath)) {
|
|
237
350
|
try {
|
|
238
351
|
existing = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
239
352
|
if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
|
|
@@ -244,7 +357,7 @@ function saveConfig(key, value, scope = "project") {
|
|
|
244
357
|
}
|
|
245
358
|
} else {
|
|
246
359
|
const dir = dirname(filePath);
|
|
247
|
-
if (!
|
|
360
|
+
if (!existsSync2(dir)) {
|
|
248
361
|
mkdirSync(dir, { recursive: true });
|
|
249
362
|
}
|
|
250
363
|
}
|
|
@@ -258,7 +371,7 @@ function unsetConfig(key, scope = "project") {
|
|
|
258
371
|
throw new Error(`Unknown config key: ${key}. Valid keys: ${validKeys().join(", ")}`);
|
|
259
372
|
}
|
|
260
373
|
const filePath = getConfigPath(scope);
|
|
261
|
-
if (!
|
|
374
|
+
if (!existsSync2(filePath))
|
|
262
375
|
return false;
|
|
263
376
|
let existing;
|
|
264
377
|
try {
|
|
@@ -278,8 +391,8 @@ function unsetConfig(key, scope = "project") {
|
|
|
278
391
|
}
|
|
279
392
|
|
|
280
393
|
// src/lib/project-state.ts
|
|
281
|
-
import { existsSync as
|
|
282
|
-
import { join as
|
|
394
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
395
|
+
import { join as join3 } from "path";
|
|
283
396
|
|
|
284
397
|
// src/lib/utils.ts
|
|
285
398
|
function normalizeSkillName(name) {
|
|
@@ -301,14 +414,14 @@ var SKILLS_PROJECT_DIR = ".skills";
|
|
|
301
414
|
var PROJECT_CONFIG_FILE = "project.json";
|
|
302
415
|
var DEFAULT_EXPORT_DIR = ".skills/exports";
|
|
303
416
|
function getProjectStateDir(targetDir = process.cwd()) {
|
|
304
|
-
return
|
|
417
|
+
return join3(targetDir, SKILLS_PROJECT_DIR);
|
|
305
418
|
}
|
|
306
419
|
function getProjectConfigPath(targetDir = process.cwd()) {
|
|
307
|
-
return
|
|
420
|
+
return join3(getProjectStateDir(targetDir), PROJECT_CONFIG_FILE);
|
|
308
421
|
}
|
|
309
422
|
function loadProjectConfig(targetDir = process.cwd()) {
|
|
310
423
|
const path = getProjectConfigPath(targetDir);
|
|
311
|
-
if (!
|
|
424
|
+
if (!existsSync3(path))
|
|
312
425
|
return null;
|
|
313
426
|
try {
|
|
314
427
|
return normalizeProjectConfig(JSON.parse(readFileSync2(path, "utf-8")));
|
|
@@ -561,7 +674,7 @@ function getSkillsNativeStorageStatus(options = {}) {
|
|
|
561
674
|
local: {
|
|
562
675
|
dataDir: getDataDir(),
|
|
563
676
|
projectStateDir: getProjectStateDir(targetDir),
|
|
564
|
-
feedbackDbPath:
|
|
677
|
+
feedbackDbPath: join4(getDataDir(), "skills.db")
|
|
565
678
|
},
|
|
566
679
|
remote: {
|
|
567
680
|
databaseConfigured: Boolean(config.databaseUrl),
|
|
@@ -584,7 +697,7 @@ function getStorageStatus(options = {}) {
|
|
|
584
697
|
function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
|
|
585
698
|
const projectStateDir = getProjectStateDir(targetDir);
|
|
586
699
|
const files = [];
|
|
587
|
-
if (
|
|
700
|
+
if (existsSync4(projectStateDir)) {
|
|
588
701
|
for (const filePath of walkFiles(projectStateDir)) {
|
|
589
702
|
const bytes = readFileSync3(filePath);
|
|
590
703
|
const relativePath = toPosix(relative(targetDir, filePath));
|
|
@@ -611,7 +724,7 @@ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options
|
|
|
611
724
|
continue;
|
|
612
725
|
}
|
|
613
726
|
const absolutePath = resolveSnapshotPath(targetDir, file.path);
|
|
614
|
-
if (
|
|
727
|
+
if (existsSync4(absolutePath) && !options.overwrite) {
|
|
615
728
|
skipped += 1;
|
|
616
729
|
continue;
|
|
617
730
|
}
|
|
@@ -918,7 +1031,7 @@ function parsePositiveInteger(value) {
|
|
|
918
1031
|
function walkFiles(dir) {
|
|
919
1032
|
const files = [];
|
|
920
1033
|
for (const entry of readdirSync2(dir)) {
|
|
921
|
-
const full =
|
|
1034
|
+
const full = join4(dir, entry);
|
|
922
1035
|
const stats = statSync2(full);
|
|
923
1036
|
if (stats.isDirectory())
|
|
924
1037
|
files.push(...walkFiles(full));
|
|
@@ -935,7 +1048,7 @@ function resolveSnapshotPath(targetDir, snapshotPath) {
|
|
|
935
1048
|
if (!toPosix(normalizedPath).startsWith(".skills/")) {
|
|
936
1049
|
throw new Error(`Snapshot path must stay inside .skills: ${snapshotPath}`);
|
|
937
1050
|
}
|
|
938
|
-
return
|
|
1051
|
+
return join4(targetDir, normalizedPath);
|
|
939
1052
|
}
|
|
940
1053
|
function normalizeS3Prefix(prefix) {
|
|
941
1054
|
return (prefix ?? "").trim().replace(/^\/+|\/+$/g, "");
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Immutable skill versions (hasna/apps#1630).
|
|
2
|
+
--
|
|
3
|
+
-- skills_registry keeps ONE row per (org, slug): the current revision. Every re-publish
|
|
4
|
+
-- replaced it and purged the previous bundle, so there was no history. This table records
|
|
5
|
+
-- each published name@version once: the content-addressed bundle it points at, the file
|
|
6
|
+
-- manifest and provenance the client sent, and where the version-addressed copy lives.
|
|
7
|
+
--
|
|
8
|
+
-- Rows are never updated. A publish of an existing (org, slug, version) with a different
|
|
9
|
+
-- bundle digest is refused (409 SKILL_VERSION_EXISTS); the same digest is idempotent.
|
|
10
|
+
-- bundle_sha256 references skills_bundles(org_id, sha256) softly, like skills_registry
|
|
11
|
+
-- does; orphan collection must consult this table so a version's bundle outlives the
|
|
12
|
+
-- registry row that first published it.
|
|
13
|
+
CREATE TABLE IF NOT EXISTS skills_versions (
|
|
14
|
+
org_id text NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
15
|
+
slug text NOT NULL,
|
|
16
|
+
version text NOT NULL,
|
|
17
|
+
bundle_sha256 text NOT NULL,
|
|
18
|
+
bundle_byte_size integer NOT NULL,
|
|
19
|
+
-- 'db' when the bytes live in skills_bundles.body_blob, 's3' when a version-addressed
|
|
20
|
+
-- object (<prefix>/skills/<org>/<slug>/<version>/bundle.tar.gz) was written.
|
|
21
|
+
storage_kind text NOT NULL DEFAULT 'db',
|
|
22
|
+
storage_key text,
|
|
23
|
+
manifest_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
24
|
+
published_by_user_id text REFERENCES users(id) ON DELETE SET NULL,
|
|
25
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
26
|
+
PRIMARY KEY (org_id, slug, version)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS skills_versions_org_slug_created_idx ON skills_versions (org_id, slug, created_at DESC);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS skills_versions_bundle_idx ON skills_versions (org_id, bundle_sha256);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Immutable skill versions (hasna/apps#1630). SQLite twin of migrations/postgres/0006.
|
|
2
|
+
CREATE TABLE IF NOT EXISTS skills_versions (
|
|
3
|
+
org_id text NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
4
|
+
slug text NOT NULL,
|
|
5
|
+
version text NOT NULL,
|
|
6
|
+
bundle_sha256 text NOT NULL,
|
|
7
|
+
bundle_byte_size integer NOT NULL,
|
|
8
|
+
storage_kind text NOT NULL DEFAULT 'db',
|
|
9
|
+
storage_key text,
|
|
10
|
+
manifest_json text NOT NULL DEFAULT '{}',
|
|
11
|
+
published_by_user_id text REFERENCES users(id) ON DELETE SET NULL,
|
|
12
|
+
created_at text NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
13
|
+
PRIMARY KEY (org_id, slug, version)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS skills_versions_org_slug_created_idx ON skills_versions (org_id, slug, created_at DESC);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS skills_versions_bundle_idx ON skills_versions (org_id, bundle_sha256);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Skills library for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"author": "Hasna",
|
|
81
81
|
"license": "Apache-2.0",
|
|
82
82
|
"devDependencies": {
|
|
83
|
-
"@types/bun": "
|
|
83
|
+
"@types/bun": "1.3.14",
|
|
84
84
|
"@types/node": "25.2.3",
|
|
85
85
|
"@types/react": "^18.2.0",
|
|
86
86
|
"bun-types": "1.3.14",
|