@hasna/skills 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -113,6 +113,17 @@ export declare class ArtifactStorage {
113
113
  * <prefix>/skills/<org>/<slug>/<version>/manifest.json
114
114
  * The content-addressed object under bundles/ stays the read path (dedupe); these keys
115
115
  * are the durable, browsable history and are never deleted by orphan collection.
116
+ *
117
+ * RETENTION (hasna/apps#1671): these objects accumulate without bound by design — every
118
+ * version keeps a full copy — so the bucket lifecycle is the retention story, not the
119
+ * store. The intended rule (operated in the bucket's lifecycle config, outside this
120
+ * repository): expire `<prefix>/skills/` objects older than N days per the slot policy,
121
+ * while `<prefix>/bundles/` (content-addressed, deduped across versions and slugs) is
122
+ * NEVER expired by lifecycle — only the reference-guarded delete path removes those,
123
+ * because one object can serve many versions. A busy org that pushes thousands of
124
+ * versions should pair the lifecycle rule with a documented cap on retained versions
125
+ * per slug (e.g. the newest K, pruned by a maintenance job that deletes the version row
126
+ * and its two objects together); row deletion alone would strand the objects.
116
127
  * Returns the placement recorded on the version row; in db mode nothing is written.
117
128
  */
118
129
  putVersionObjects(orgId: string, slug: string, version: string, bytes: OwnedBytes, manifest: Record<string, unknown>, contentType?: string): Promise<{
@@ -22,7 +22,15 @@ export declare class SkillRequestError extends Error {
22
22
  }
23
23
  /** SkillMeta shape for a published row, so a client can treat both kinds alike. */
24
24
  export declare function publishedSkillMeta(record: ServerSkillRecord): SkillMeta;
25
- export declare function publishedPayload(record: ServerSkillRecord): Record<string, unknown>;
25
+ /**
26
+ * SkillMeta shape for a published row, plus a payload-level "alreadyPublished" flag:
27
+ * true when the accepted publish found the version already recorded with the same
28
+ * digest (an idempotent re-push), so the CLI can distinguish "published as X" from
29
+ * "already published as X" (hasna/apps#1671).
30
+ */
31
+ export declare function publishedPayload(record: ServerSkillRecord, opts?: {
32
+ alreadyPublished?: boolean;
33
+ }): Record<string, unknown>;
26
34
  /**
27
35
  * The HTTP ETag for a published row: the revision id, quoted exactly as RFC 9110 wants.
28
36
  * The quotes are load-bearing — a client that echoes the whole header value back as
@@ -143,7 +151,10 @@ interface ParsedPublish {
143
151
  * measured.
144
152
  */
145
153
  export declare function parsePublishRequest(request: Request, config: SkillsServerConfig): Promise<ParsedPublish>;
146
- export declare function storePublishedSkill(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, parsed: ParsedPublish, expectedRevisionId?: string): Promise<ServerSkillRecord>;
154
+ export declare function storePublishedSkill(store: SkillsProductStore, artifactStorage: ArtifactStorage, principal: ApiPrincipal, parsed: ParsedPublish, expectedRevisionId?: string): Promise<{
155
+ record: ServerSkillRecord;
156
+ alreadyPublished: boolean;
157
+ }>;
147
158
  /**
148
159
  * Tombstone a published skill (todos d061fcda): the row survives with a tombstone
149
160
  * marker for `tombstoneWindowMs`, so reads answer 410 and a pulling client can
package/dist/storage.js CHANGED
@@ -65,7 +65,12 @@ import { join as join2, dirname } from "path";
65
65
  // src/lib/retired-settings.ts
66
66
  var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
67
67
  var RETIRED_CONFIG_KEYS = {
68
- mode: "apiUrl"
68
+ mode: "a configured API origin",
69
+ apiUrl: "skills setup --api-url <origin>"
70
+ };
71
+ var RETIRED_CONFIG_KEY_REASONS = {
72
+ mode: "Deployment modes were removed: a Skills client either resolves a credential " + "or it does not, and that is the whole of it.",
73
+ apiUrl: "The service address is no longer kept in this app's config file: it is read from " + "HASNA_SKILLS_API_URL, then the macOS Keychain item hasna.credentials.skills.api-url, " + "then ~/.hasna/skills/config/credentials, then the fleet gateway."
69
74
  };
70
75
 
71
76
  class RetiredSettingError extends Error {
@@ -104,7 +109,7 @@ function assertNoRetiredConfigKeys(config, source) {
104
109
  for (const [key, replacement] of Object.entries(RETIRED_CONFIG_KEYS)) {
105
110
  if (!(key in config))
106
111
  continue;
107
- throw new RetiredSettingError(key, `${source}: "${key}" is no longer a configuration key. ` + "Deployment modes were removed: a Skills client either has an API origin " + "configured or it does not, and that is the whole of it. " + `Use "${replacement}" instead (skills config set ${replacement} <origin>), and ` + `remove the old key with: skills config unset ${key}. ` + "Refused rather than ignored, because silently dropping it would leave an " + "operator believing they had pointed this install at a server.");
112
+ throw new RetiredSettingError(key, `${source}: "${key}" is no longer a configuration key. ` + `${RETIRED_CONFIG_KEY_REASONS[key] ?? ""} ` + `Use ${replacement} instead, and remove the old key with: skills config unset ${key}. ` + "Refused rather than ignored, because silently dropping it would leave an " + "operator believing they had pointed this install at a server.");
108
113
  }
109
114
  }
110
115
 
@@ -232,7 +237,7 @@ var ENUM_KEYS = {
232
237
  defaultScope: ["global", "project"],
233
238
  format: ["compact", "json", "csv"]
234
239
  };
235
- var STRING_KEYS = ["apiUrl", "extensionsDir"];
240
+ var STRING_KEYS = ["extensionsDir"];
236
241
  function validKeys() {
237
242
  return [...Object.keys(ENUM_KEYS), ...STRING_KEYS];
238
243
  }
@@ -263,16 +268,6 @@ function normalizeConfigValue(key, value) {
263
268
  const allowed = allowedValues(key);
264
269
  if (allowed)
265
270
  return allowed.includes(value) ? value : undefined;
266
- if (key === "apiUrl") {
267
- try {
268
- const url = new URL(value);
269
- if (url.protocol !== "http:" && url.protocol !== "https:")
270
- return;
271
- return value.replace(/\/+$/, "");
272
- } catch {
273
- return;
274
- }
275
- }
276
271
  if (key === "extensionsDir")
277
272
  return value.trim() ? value : undefined;
278
273
  return;
@@ -342,7 +337,7 @@ function saveConfig(key, value, scope = "project") {
342
337
  const normalized = normalizeConfigValue(key, value);
343
338
  if (normalized === undefined) {
344
339
  const allowed = allowedValues(key);
345
- throw new Error(allowed ? `Invalid value '${value}' for ${key}. Allowed: ${allowed.join(", ")}` : `Invalid value '${value}' for ${key}. Expected ${key === "apiUrl" ? "an http(s) URL" : "a non-empty path"}`);
340
+ throw new Error(allowed ? `Invalid value '${value}' for ${key}. Allowed: ${allowed.join(", ")}` : `Invalid value '${value}' for ${key}. Expected a non-empty path`);
346
341
  }
347
342
  const filePath = getConfigPath(scope);
348
343
  let existing = {};
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "skills": "bin/index.js",
8
8
  "skills-mcp": "bin/mcp.js",
9
+ "skills-serve": "bin/server.js",
9
10
  "skills-server": "bin/server.js",
10
11
  "skills-worker": "bin/worker.js",
11
12
  "skills-migrate": "bin/migrate.js"
@@ -91,6 +92,7 @@
91
92
  "dependencies": {
92
93
  "@aws-sdk/client-ecs": "^3.1079.0",
93
94
  "@aws-sdk/client-s3": "^3.1079.0",
95
+ "@hasna/contracts": "1.0.1",
94
96
  "@hasna/events": "0.1.16",
95
97
  "@modelcontextprotocol/sdk": "^1.26.0",
96
98
  "chalk": "^5.3.0",