@hasna/skills 0.1.72 → 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 +24514 -24319
- package/bin/mcp.js +217 -198
- package/bin/migrate.js +114 -62
- package/bin/server.js +3384 -2892
- package/bin/worker.js +2095 -1820
- 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 +475 -263
- package/dist/lib/app-home.d.ts +27 -1
- 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/sdk/index.js +5470 -4975
- 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 +62 -61
- package/migrations/postgres/0006_skill_versions.sql +30 -0
- package/migrations/sqlite/0006_skill_versions.sql +17 -0
- package/package.json +2 -3
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
|
@@ -56,11 +56,11 @@ import {
|
|
|
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
62
|
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
63
|
-
import { join as
|
|
63
|
+
import { join as join2, dirname } from "path";
|
|
64
64
|
|
|
65
65
|
// src/lib/retired-settings.ts
|
|
66
66
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
@@ -110,94 +110,87 @@ function assertNoRetiredConfigKeys(config, source) {
|
|
|
110
110
|
|
|
111
111
|
// src/lib/app-home.ts
|
|
112
112
|
import { existsSync } from "fs";
|
|
113
|
-
import { homedir as homedir2 } from "os";
|
|
114
|
-
import { join as join2, resolve } from "path";
|
|
115
|
-
|
|
116
|
-
// ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
|
|
117
113
|
import { homedir } from "os";
|
|
118
|
-
import { join } from "path";
|
|
119
|
-
|
|
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 = {
|
|
120
118
|
config: "HASNA_CONFIG_HOME",
|
|
121
119
|
data: "HASNA_DATA_HOME",
|
|
122
120
|
state: "HASNA_STATE_HOME",
|
|
123
121
|
cache: "HASNA_CACHE_HOME"
|
|
124
122
|
};
|
|
125
|
-
var
|
|
126
|
-
function
|
|
123
|
+
var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
124
|
+
function pathsResolverAssertApp(app) {
|
|
127
125
|
if (typeof app !== "string" || app.length === 0) {
|
|
128
126
|
throw new TypeError("paths: app must be a non-empty string");
|
|
129
127
|
}
|
|
130
|
-
if (!
|
|
128
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
131
129
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
132
130
|
}
|
|
133
131
|
}
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const value = envOf(options)[KIND_ENV[kind]];
|
|
139
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
140
|
-
}
|
|
141
|
-
function isMacOS(platform) {
|
|
142
|
-
return platform === "darwin";
|
|
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
|
+
}
|
|
143
136
|
}
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
|
147
142
|
return override;
|
|
148
|
-
const home = options.home ??
|
|
143
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
149
144
|
const platform = options.platform ?? process.platform;
|
|
150
|
-
if (
|
|
145
|
+
if (platform === "darwin") {
|
|
151
146
|
switch (kind) {
|
|
152
147
|
case "config":
|
|
153
148
|
case "data":
|
|
154
|
-
return
|
|
149
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
155
150
|
case "cache":
|
|
156
|
-
return
|
|
151
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
157
152
|
case "state":
|
|
158
|
-
return
|
|
153
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
159
154
|
}
|
|
160
155
|
}
|
|
161
156
|
switch (kind) {
|
|
162
157
|
case "config":
|
|
163
|
-
return
|
|
158
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
164
159
|
case "data":
|
|
165
|
-
return
|
|
160
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
166
161
|
case "state":
|
|
167
|
-
return
|
|
162
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
168
163
|
case "cache":
|
|
169
|
-
return
|
|
164
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
170
165
|
}
|
|
171
166
|
}
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
const appSegment = options.internal === true ?
|
|
175
|
-
return
|
|
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);
|
|
176
171
|
}
|
|
177
172
|
function dataDir(options) {
|
|
178
|
-
return
|
|
173
|
+
return pathsResolverResolve("data", options);
|
|
179
174
|
}
|
|
180
|
-
|
|
181
|
-
// src/lib/app-home.ts
|
|
182
175
|
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
183
176
|
var HASNA_SKILLS_HOME_ENV = "HASNA_SKILLS_HOME";
|
|
184
177
|
var SKILLS_HOME_ENV = "SKILLS_HOME";
|
|
185
178
|
var DEFAULT_SQLITE_FILENAME = "server.db";
|
|
186
179
|
var GLOBAL_CONFIG_FILENAME = "config.json";
|
|
187
180
|
function effectiveHome() {
|
|
188
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
181
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir() || "/tmp";
|
|
189
182
|
}
|
|
190
183
|
function legacyDataRoot() {
|
|
191
|
-
return
|
|
184
|
+
return join(effectiveHome(), ".hasna", "skills");
|
|
192
185
|
}
|
|
193
|
-
function resolverDataRoot(home = effectiveHome()) {
|
|
194
|
-
return dataDir({ app: "skills", home });
|
|
186
|
+
function resolverDataRoot(home = effectiveHome(), env) {
|
|
187
|
+
return dataDir({ app: "skills", home, env });
|
|
195
188
|
}
|
|
196
189
|
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
197
190
|
const dataOverride = env.HASNA_DATA_HOME;
|
|
198
191
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
199
192
|
return true;
|
|
200
|
-
return existsSync(
|
|
193
|
+
return existsSync(join(resolved, DEFAULT_SQLITE_FILENAME)) || existsSync(join(resolved, GLOBAL_CONFIG_FILENAME));
|
|
201
194
|
}
|
|
202
195
|
function exactDataRoot() {
|
|
203
196
|
for (const key of [DATA_DIR_ENV, HASNA_SKILLS_HOME_ENV, SKILLS_HOME_ENV]) {
|
|
@@ -221,8 +214,16 @@ function getDataRoot() {
|
|
|
221
214
|
return adoptResolverDataRoot(resolved) ? resolve(resolved) : resolve(legacyDataRoot());
|
|
222
215
|
}
|
|
223
216
|
function skillsDataRootForHome(home) {
|
|
224
|
-
const
|
|
225
|
-
|
|
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"));
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
// src/lib/config.ts
|
|
@@ -243,8 +244,8 @@ function mergeDirectoryContents(sourceDir, targetDir) {
|
|
|
243
244
|
return;
|
|
244
245
|
mkdirSync(targetDir, { recursive: true });
|
|
245
246
|
for (const entry of readdirSync(sourceDir)) {
|
|
246
|
-
const sourcePath =
|
|
247
|
-
const targetPath =
|
|
247
|
+
const sourcePath = join2(sourceDir, entry);
|
|
248
|
+
const targetPath = join2(targetDir, entry);
|
|
248
249
|
try {
|
|
249
250
|
const sourceStat = statSync(sourcePath);
|
|
250
251
|
if (sourceStat.isDirectory()) {
|
|
@@ -280,7 +281,7 @@ var INSTALLED_SKILLS_DIRNAME = "installed";
|
|
|
280
281
|
var SKILLS_CACHE_DIRNAME = "skills";
|
|
281
282
|
var LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
|
|
282
283
|
function isOwnerLayoutMigrated(appDir) {
|
|
283
|
-
return existsSync2(
|
|
284
|
+
return existsSync2(join2(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
|
|
284
285
|
}
|
|
285
286
|
function getDataDir() {
|
|
286
287
|
const root = getDataRoot();
|
|
@@ -290,23 +291,23 @@ function getDataDir() {
|
|
|
290
291
|
if (hasOperatorOverride())
|
|
291
292
|
return root;
|
|
292
293
|
const home = effectiveHome();
|
|
293
|
-
const oldDir =
|
|
294
|
-
const oldConfigFile =
|
|
294
|
+
const oldDir = join2(home, ".skills");
|
|
295
|
+
const oldConfigFile = join2(home, ".skillsrc");
|
|
295
296
|
try {
|
|
296
297
|
mergeDirectoryContents(oldDir, root);
|
|
297
298
|
} catch {}
|
|
298
|
-
if (existsSync2(oldConfigFile) && !existsSync2(
|
|
299
|
+
if (existsSync2(oldConfigFile) && !existsSync2(join2(root, "config.json"))) {
|
|
299
300
|
try {
|
|
300
|
-
copyFileSync(oldConfigFile,
|
|
301
|
+
copyFileSync(oldConfigFile, join2(root, "config.json"));
|
|
301
302
|
} catch {}
|
|
302
303
|
}
|
|
303
304
|
return root;
|
|
304
305
|
}
|
|
305
306
|
function getConfigPath(scope) {
|
|
306
307
|
if (scope === "global") {
|
|
307
|
-
return
|
|
308
|
+
return join2(getDataDir(), "config.json");
|
|
308
309
|
}
|
|
309
|
-
return
|
|
310
|
+
return join2(process.cwd(), "skills.config.json");
|
|
310
311
|
}
|
|
311
312
|
function readConfigFile(path) {
|
|
312
313
|
if (!existsSync2(path))
|
|
@@ -391,7 +392,7 @@ function unsetConfig(key, scope = "project") {
|
|
|
391
392
|
|
|
392
393
|
// src/lib/project-state.ts
|
|
393
394
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
394
|
-
import { join as
|
|
395
|
+
import { join as join3 } from "path";
|
|
395
396
|
|
|
396
397
|
// src/lib/utils.ts
|
|
397
398
|
function normalizeSkillName(name) {
|
|
@@ -413,10 +414,10 @@ var SKILLS_PROJECT_DIR = ".skills";
|
|
|
413
414
|
var PROJECT_CONFIG_FILE = "project.json";
|
|
414
415
|
var DEFAULT_EXPORT_DIR = ".skills/exports";
|
|
415
416
|
function getProjectStateDir(targetDir = process.cwd()) {
|
|
416
|
-
return
|
|
417
|
+
return join3(targetDir, SKILLS_PROJECT_DIR);
|
|
417
418
|
}
|
|
418
419
|
function getProjectConfigPath(targetDir = process.cwd()) {
|
|
419
|
-
return
|
|
420
|
+
return join3(getProjectStateDir(targetDir), PROJECT_CONFIG_FILE);
|
|
420
421
|
}
|
|
421
422
|
function loadProjectConfig(targetDir = process.cwd()) {
|
|
422
423
|
const path = getProjectConfigPath(targetDir);
|
|
@@ -673,7 +674,7 @@ function getSkillsNativeStorageStatus(options = {}) {
|
|
|
673
674
|
local: {
|
|
674
675
|
dataDir: getDataDir(),
|
|
675
676
|
projectStateDir: getProjectStateDir(targetDir),
|
|
676
|
-
feedbackDbPath:
|
|
677
|
+
feedbackDbPath: join4(getDataDir(), "skills.db")
|
|
677
678
|
},
|
|
678
679
|
remote: {
|
|
679
680
|
databaseConfigured: Boolean(config.databaseUrl),
|
|
@@ -1030,7 +1031,7 @@ function parsePositiveInteger(value) {
|
|
|
1030
1031
|
function walkFiles(dir) {
|
|
1031
1032
|
const files = [];
|
|
1032
1033
|
for (const entry of readdirSync2(dir)) {
|
|
1033
|
-
const full =
|
|
1034
|
+
const full = join4(dir, entry);
|
|
1034
1035
|
const stats = statSync2(full);
|
|
1035
1036
|
if (stats.isDirectory())
|
|
1036
1037
|
files.push(...walkFiles(full));
|
|
@@ -1047,7 +1048,7 @@ function resolveSnapshotPath(targetDir, snapshotPath) {
|
|
|
1047
1048
|
if (!toPosix(normalizedPath).startsWith(".skills/")) {
|
|
1048
1049
|
throw new Error(`Snapshot path must stay inside .skills: ${snapshotPath}`);
|
|
1049
1050
|
}
|
|
1050
|
-
return
|
|
1051
|
+
return join4(targetDir, normalizedPath);
|
|
1051
1052
|
}
|
|
1052
1053
|
function normalizeS3Prefix(prefix) {
|
|
1053
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",
|
|
@@ -92,7 +92,6 @@
|
|
|
92
92
|
"@aws-sdk/client-ecs": "^3.1079.0",
|
|
93
93
|
"@aws-sdk/client-s3": "^3.1079.0",
|
|
94
94
|
"@hasna/events": "0.1.16",
|
|
95
|
-
"@hasna/paths": "0.1.0",
|
|
96
95
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
97
96
|
"chalk": "^5.3.0",
|
|
98
97
|
"commander": "^12.1.0",
|