@hasna/skills 0.1.63 → 0.1.66

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.
Files changed (42) hide show
  1. package/README.md +61 -22
  2. package/bin/index.js +2120 -670
  3. package/bin/mcp.js +555 -249
  4. package/bin/migrate.js +229 -33
  5. package/bin/server.js +1542 -327
  6. package/bin/worker.js +716 -207
  7. package/dist/cli/commands/registry-reconcile.d.ts +2 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +682 -276
  10. package/dist/lib/agent-sync.d.ts +26 -6
  11. package/dist/lib/auth-store.d.ts +37 -0
  12. package/dist/lib/config.d.ts +51 -0
  13. package/dist/lib/home-census.d.ts +4 -0
  14. package/dist/lib/home-migration.d.ts +8 -9
  15. package/dist/lib/native-storage.d.ts +29 -1
  16. package/dist/lib/portable-skills.d.ts +49 -6
  17. package/dist/lib/pull.d.ts +31 -0
  18. package/dist/lib/registry-reconcile.d.ts +114 -0
  19. package/dist/lib/registry-types.d.ts +9 -0
  20. package/dist/lib/registry.d.ts +7 -4
  21. package/dist/lib/remote-client.d.ts +118 -1
  22. package/dist/lib/remote-registry.d.ts +26 -0
  23. package/dist/lib/revision.d.ts +29 -0
  24. package/dist/lib/run-routing.d.ts +60 -0
  25. package/dist/sdk/index.js +14412 -13338
  26. package/dist/server/app.d.ts +4 -1
  27. package/dist/server/config.d.ts +12 -2
  28. package/dist/server/rows.d.ts +2 -1
  29. package/dist/server/skills-api.d.ts +108 -6
  30. package/dist/server/sqlite-store.d.ts +20 -3
  31. package/dist/server/store-fixtures.d.ts +6 -0
  32. package/dist/server/store.d.ts +31 -5
  33. package/dist/server/types.d.ts +98 -3
  34. package/dist/storage.d.ts +1 -1
  35. package/dist/storage.js +57 -2
  36. package/migrations/postgres/0004_hosted_pins.sql +28 -0
  37. package/migrations/postgres/0005_revision_tombstone_registry.sql +37 -0
  38. package/migrations/postgres/0005_tag_projection.sql +36 -0
  39. package/migrations/sqlite/0004_hosted_pins.sql +21 -0
  40. package/migrations/sqlite/0005_revision_tombstone_registry.sql +18 -0
  41. package/migrations/sqlite/0005_tag_projection.sql +25 -0
  42. package/package.json +3 -2
@@ -1,12 +1,83 @@
1
1
  import { type RemoteSkillRunContract } from "./remote-run-contract.js";
2
+ /**
3
+ * A server that predates this client's pin/tag/incremental-sync routes answered
4
+ * 404/405 for them. The caller must never mistake that for "no pins" or "empty
5
+ * listing" — a silently-empty sync would look like success and drop nothing on
6
+ * the next push. This error is how the version-skew surfaces fail-closed.
7
+ */
8
+ export declare class RemoteRouteUnsupportedError extends Error {
9
+ readonly path: string;
10
+ readonly status: number;
11
+ readonly instance: string;
12
+ constructor(path: string, status: number, instance: string);
13
+ }
14
+ /** Any other non-ok response on the new-route methods, with the status attached. */
15
+ export declare class RemoteRequestError extends Error {
16
+ readonly path: string;
17
+ readonly status: number;
18
+ constructor(path: string, status: number, statusText: string);
19
+ }
20
+ /**
21
+ * A remote pin on a skill, matching the hosted-pins wire shape
22
+ * (`{ slug, pinnedAt, metadata }`). `pinnedAt`/`metadata` are server-reported
23
+ * and may be absent.
24
+ */
25
+ export interface RemotePin {
26
+ slug: string;
27
+ pinnedAt?: string;
28
+ metadata?: Record<string, unknown>;
29
+ }
30
+ /** The minimal per-skill row the pin/tag/updated-since routes serve. */
31
+ export interface RemoteSkillSummary {
32
+ slug: string;
33
+ name?: string;
34
+ version?: string;
35
+ updatedAt?: string;
36
+ }
37
+ /**
38
+ * One page of an incremental listing. `nextCursor` is an opaque continuation
39
+ * token; null (or an absent field) means the listing is complete.
40
+ */
41
+ export interface UpdatedSincePage {
42
+ skills: RemoteSkillSummary[];
43
+ nextCursor: string | null;
44
+ }
2
45
  export declare class RemoteSkillsClient {
3
46
  private apiUrl;
4
47
  private apiKey;
5
48
  constructor(apiKey: string, apiUrl?: string);
6
49
  private request;
50
+ /**
51
+ * Fail-closed version-skew guard for the pin/tag/updated-since routes.
52
+ *
53
+ * A server that predates these routes answers 404 (unmatched path) or 405
54
+ * (unmatched method). Both are surfaced as `RemoteRouteUnsupportedError` —
55
+ * never as an empty listing, which would read as "no pins / no changes" and
56
+ * silently desynchronize the caller. Every other non-ok response becomes a
57
+ * `RemoteRequestError` carrying the status.
58
+ *
59
+ * `domainNotFoundCodes` is the one deliberate exception: a route the server
60
+ * DOES have can 404 for a domain reason (the hosted-pins DELETE answers
61
+ * `{ code: "PIN_NOT_FOUND" }` when no pin exists). A 404 whose JSON body
62
+ * carries one of those codes is returned to the caller (status intact) so it
63
+ * can apply domain semantics instead of misreporting version skew. Every
64
+ * other 404 — including the dispatcher's `{ code: "NOT_FOUND" }` on a route
65
+ * the server lacks — still throws `RemoteRouteUnsupportedError`.
66
+ */
67
+ private requestNewRoute;
7
68
  listSkills(): Promise<any[]>;
8
69
  getSkillMd(slug: string): Promise<string | null>;
9
70
  getSkill(slug: string): Promise<any | null>;
71
+ /**
72
+ * Raw GET for one skill, with the HTTP status surfaced. Used by the reconcile
73
+ * re-check (registry-reconcile.ts) so it can distinguish "no such skill" (404) from
74
+ * "the registry failed to answer" (any other non-success status) instead of treating
75
+ * both as absent.
76
+ */
77
+ getSkillStatus(slug: string): Promise<{
78
+ status: number;
79
+ body: unknown;
80
+ }>;
10
81
  submitRun(slug: string, input?: Record<string, unknown>, args?: string[]): Promise<RemoteSkillRunContract>;
11
82
  getRun(runId: string): Promise<RemoteSkillRunContract | null>;
12
83
  getRunLogs(runId: string): Promise<any[]>;
@@ -24,8 +95,13 @@ export declare class RemoteSkillsClient {
24
95
  * Note the deliberate absence of `request()`: that helper pins
25
96
  * `Content-Type: application/json`, and a multipart body whose Content-Type does not
26
97
  * carry the generated boundary is unparseable at the other end.
98
+ *
99
+ * Optimistic concurrency (todos d061fcda): pass the revision id this client last read
100
+ * for the slug (from getSkill().revisionId) as `ifMatch`. The instance refuses a
101
+ * publish against a live slug that does not name its current revision with 409 — this
102
+ * is how a push never silently overwrites a newer remote revision.
27
103
  */
28
- publishSkill(manifest: Record<string, unknown>, bundle?: Uint8Array): Promise<Response>;
104
+ publishSkill(manifest: Record<string, unknown>, bundle?: Uint8Array, ifMatch?: string): Promise<Response>;
29
105
  deleteSkill(slug: string): Promise<Response>;
30
106
  downloadSkillBundle(slug: string): Promise<Response>;
31
107
  /**
@@ -34,5 +110,46 @@ export declare class RemoteSkillsClient {
34
110
  * instance serves no bundle for this skill (the metadata-only fallback path).
35
111
  */
36
112
  getBundle(slug: string): Promise<Response | null>;
113
+ /** List the pins the instance holds for this principal. */
114
+ listPins(): Promise<RemotePin[]>;
115
+ /**
116
+ * Pin a skill on the instance (upsert — pinning again refreshes it). The
117
+ * wire contract matches the hosted-pins routes: a PUT with an optional
118
+ * `{ metadata }` body, answered with the stored pin (`slug`, `pinnedAt`,
119
+ * `metadata`).
120
+ */
121
+ pin(slug: string, metadata?: Record<string, unknown>): Promise<RemotePin>;
122
+ /**
123
+ * Unpin a skill on the instance. Resolves true when a pin existed and was
124
+ * deleted; false when the instance has no pin for this slug (its 404
125
+ * carries `code: "PIN_NOT_FOUND"` — a domain answer, not version skew). A
126
+ * bare 404 (route not deployed) still throws `RemoteRouteUnsupportedError`.
127
+ */
128
+ unpin(slug: string): Promise<boolean>;
129
+ /** List the tag names the instance serves. */
130
+ listTags(): Promise<string[]>;
131
+ /** List the skills carrying a tag on the instance. */
132
+ skillsByTag(tag: string): Promise<RemoteSkillSummary[]>;
133
+ /**
134
+ * Cursor-based incremental listing of skills updated after `since` (ISO 8601).
135
+ * Each page carries an opaque `nextCursor`; null means the listing is complete.
136
+ * This is the feed T9's sync reconciliation verb consumes.
137
+ */
138
+ listUpdatedSince(since: string, options?: {
139
+ cursor?: string;
140
+ limit?: number;
141
+ }): Promise<UpdatedSincePage>;
37
142
  }
38
143
  export declare function createRemoteSkillsClient(): RemoteSkillsClient | null;
144
+ /**
145
+ * Write-free client resolution for read-only paths (e.g. `sync --dry-run`).
146
+ *
147
+ * createRemoteSkillsClient() resolves the stored credential through
148
+ * getAuthFilePath() and the stored origin through loadConfig() — both route
149
+ * through getDataDir(), which WRITES (mkdirs the app dir, merges legacy ~/.skills
150
+ * content, copies the legacy config). A dry run must resolve the same client a
151
+ * real run would without performing any of that: the credential and the origin
152
+ * are read from the same files at the same computed paths, and the MissingApiUrl
153
+ * failure mode is preserved (a key with no origin still fails loudly).
154
+ */
155
+ export declare function createRemoteSkillsClientReadOnly(): RemoteSkillsClient | null;
@@ -19,4 +19,30 @@ export declare function buildSkillsApiUrl(apiUrl: string, endpoint?: string): st
19
19
  export declare function parseRemoteRegistryPayload(payload: unknown): SkillMeta[];
20
20
  export declare function parseRemoteSkillPayload(payload: unknown): SkillMeta;
21
21
  export declare function loadRemoteRegistry(options?: RemoteRegistryOptions): Promise<SkillMeta[]>;
22
+ /**
23
+ * Merge the authenticated remote registry into a local listing, whenever the
24
+ * install is pointed at a hosted instance.
25
+ *
26
+ * This is the fail-closed (R1) default-read merge: a client configured with an
27
+ * origin sees the folder UNION cloud in the plain `list`/`search` path, while
28
+ * every other install keeps today's exact local behavior.
29
+ *
30
+ * - No origin configured -> the local list is returned unchanged and no
31
+ * request is attempted. An unconfigured install must stay byte-identical
32
+ * to the pre-merge output.
33
+ * - Origin configured but no credential -> the local list is returned
34
+ * unchanged and nothing throws. Auth-missing must never crash a read.
35
+ * - Origin + credential -> the remote registry is fetched and merged under
36
+ * the precedence in registry-merge.ts (custom > extension > private >
37
+ * private-hosted > remote > upstream > official), remote rows tagged
38
+ * `source: "remote"`.
39
+ * - A configured, authenticated read that FAILS (auth rejection, HTTP
40
+ * error, network failure) throws a clear error rather than silently
41
+ * returning the local half — a silent partial listing would report
42
+ * success for a union the caller asked to include.
43
+ *
44
+ * The explicit `--remote` path stays on loadRemoteRegistry(): an explicit
45
+ * request has always been fatal on failure, and that contract is unchanged.
46
+ */
47
+ export declare function mergeRemoteRegistry(local: SkillMeta[], options?: RemoteRegistryOptions): Promise<SkillMeta[]>;
22
48
  export declare function loadRemoteSkill(name: string, options?: RemoteRegistryOptions): Promise<SkillMeta>;
@@ -0,0 +1,29 @@
1
+ /** The row's content-addressed revision identity: a lowercase hex sha-256. */
2
+ export declare const REVISION_ID_PATTERN: RegExp;
3
+ export interface RevisionContent {
4
+ slug: string;
5
+ displayName: string;
6
+ description: string;
7
+ category: string;
8
+ tags: string[];
9
+ source: string;
10
+ kind: "executable" | "instruction";
11
+ version?: string;
12
+ skillMd?: string;
13
+ bundleSha256?: string;
14
+ bundleByteSize?: number;
15
+ }
16
+ export declare function revisionIdOf(content: RevisionContent): string;
17
+ export declare function revisionIdOfRecord(record: RevisionContent): string;
18
+ /**
19
+ * The legacy-row marker the 0004 migration leaves behind.
20
+ *
21
+ * Migration 0004 adds revision_id with DEFAULT '', so rows written before the migration
22
+ * carry an empty id. An empty id would make the If-Match guard vacuous for those rows:
23
+ * two clients that both read '' would both "match" and both land, silently overwriting
24
+ * each other. The stores replace the marker with a real content sha on first open
25
+ * (idempotent: new code always writes a full id, so the marker never reappears).
26
+ */
27
+ export declare const LEGACY_REVISION_ID = "";
28
+ /** True when a revision id is the empty legacy marker (needs backfill). */
29
+ export declare function isLegacyRevisionId(value: string): boolean;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * run-routing.ts — where a skill run executes: on this machine, or on the
3
+ * configured Skills API.
4
+ *
5
+ * One resolver serves every run surface (the CLI `skills run` path, the MCP
6
+ * `run_skill` tool, and the `skills schedule run` surface) so they cannot drift
7
+ * apart.
8
+ *
9
+ * A skill run is REMOTE only when all three hold:
10
+ *
11
+ * 1. an origin is configured (config `apiUrl` or `$SKILLS_API_URL`);
12
+ * 2. a credential is present (`$SKILLS_API_KEY` / `$SKILL_API_KEY` or the
13
+ * auth store written by `skills auth login`);
14
+ * 3. the skill carries the server-owned marker from its published contract
15
+ * (`skills.runtime: "hosted"` or `skills.source: "remote" |
16
+ * "private-hosted"` in the skill's `package.json` — see
17
+ * isHostedMetadataPackage in hosted-skill-set.ts).
18
+ *
19
+ * Otherwise the run is LOCAL, and local stays the default: an unconfigured
20
+ * install never sends anything anywhere.
21
+ *
22
+ * A server-owned skill never falls back to local execution. Without the origin
23
+ * or the credential the resolver fails closed with an error naming the exact
24
+ * setup command, per the product brief: "Premium or server-executed skills
25
+ * fail closed without API credentials and do not fall back to bundled local
26
+ * execution."
27
+ *
28
+ * Premium-catalog and pricing metadata is server-side and never ships in this
29
+ * package; the resolver concerns routing only.
30
+ */
31
+ import type { SkillMeta } from "./registry-types.js";
32
+ export type RunRoutingErrorCode = "REMOTE_REQUIRES_ORIGIN" | "REMOTE_REQUIRES_CREDENTIAL";
33
+ export type RunRouting = {
34
+ route: "remote";
35
+ apiKey: string;
36
+ } | {
37
+ route: "local";
38
+ } | {
39
+ route: "error";
40
+ code: RunRoutingErrorCode;
41
+ error: string;
42
+ };
43
+ /**
44
+ * Is execution of this skill server-owned per its published contract?
45
+ * The marker is the skill contract's declaration; absent means local
46
+ * execution is the contract.
47
+ */
48
+ export declare function isServerOwnedSkill(skill: Pick<SkillMeta, "serverOwned">): boolean;
49
+ /**
50
+ * Decide the route for one skill run from injectable inputs, so tests can mock
51
+ * the origin and the credential without touching the ambient environment.
52
+ */
53
+ export declare function resolveRunRouting(skill: Pick<SkillMeta, "name" | "serverOwned">, apiKey: string | null | undefined, apiUrl: string | undefined): RunRouting;
54
+ /**
55
+ * Resolve the route against the ambient configuration and credential store.
56
+ * The returned `apiKey` on the remote route is the same value the resolver
57
+ * validated, so the caller never re-reads (and cannot diverge from) the
58
+ * credential the decision was made with.
59
+ */
60
+ export declare function resolveConfiguredRunRouting(skill: Pick<SkillMeta, "name" | "serverOwned">): RunRouting;