@hasna/skills 0.1.63 → 0.1.64
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 +28 -6
- package/bin/index.js +1593 -228
- package/bin/mcp.js +290 -24
- package/bin/migrate.js +229 -33
- package/bin/server.js +774 -116
- package/bin/worker.js +558 -79
- package/dist/cli/commands/registry-reconcile.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +559 -251
- package/dist/lib/agent-sync.d.ts +26 -6
- package/dist/lib/auth-store.d.ts +37 -0
- package/dist/lib/config.d.ts +51 -0
- package/dist/lib/home-census.d.ts +4 -0
- package/dist/lib/home-migration.d.ts +8 -9
- package/dist/lib/native-storage.d.ts +1 -1
- package/dist/lib/portable-skills.d.ts +35 -6
- package/dist/lib/pull.d.ts +31 -0
- package/dist/lib/registry-reconcile.d.ts +114 -0
- package/dist/lib/registry.d.ts +7 -4
- package/dist/lib/remote-client.d.ts +118 -1
- package/dist/lib/remote-registry.d.ts +26 -0
- package/dist/lib/revision.d.ts +29 -0
- package/dist/sdk/index.js +1351 -353
- package/dist/server/app.d.ts +1 -1
- package/dist/server/config.d.ts +12 -2
- package/dist/server/rows.d.ts +2 -1
- package/dist/server/skills-api.d.ts +108 -6
- package/dist/server/sqlite-store.d.ts +20 -3
- package/dist/server/store.d.ts +31 -5
- package/dist/server/types.d.ts +98 -3
- package/dist/storage.js +7 -2
- package/migrations/postgres/0004_hosted_pins.sql +28 -0
- package/migrations/postgres/0005_revision_tombstone_registry.sql +37 -0
- package/migrations/postgres/0005_tag_projection.sql +36 -0
- package/migrations/sqlite/0004_hosted_pins.sql +21 -0
- package/migrations/sqlite/0005_revision_tombstone_registry.sql +18 -0
- package/migrations/sqlite/0005_tag_projection.sql +25 -0
- package/package.json +3 -2
package/bin/index.js
CHANGED
|
@@ -36860,7 +36860,7 @@ var package_default;
|
|
|
36860
36860
|
var init_package = __esm(() => {
|
|
36861
36861
|
package_default = {
|
|
36862
36862
|
name: "@hasna/skills",
|
|
36863
|
-
version: "0.1.
|
|
36863
|
+
version: "0.1.64",
|
|
36864
36864
|
description: "Skills library for AI coding agents",
|
|
36865
36865
|
type: "module",
|
|
36866
36866
|
bin: {
|
|
@@ -36934,6 +36934,7 @@ var init_package = __esm(() => {
|
|
|
36934
36934
|
license: "Apache-2.0",
|
|
36935
36935
|
devDependencies: {
|
|
36936
36936
|
"@types/bun": "latest",
|
|
36937
|
+
"@types/node": "25.2.3",
|
|
36937
36938
|
"@types/react": "^18.2.0",
|
|
36938
36939
|
"bun-types": "1.3.14",
|
|
36939
36940
|
"react-devtools-core": "^7.0.1",
|
|
@@ -36942,7 +36943,7 @@ var init_package = __esm(() => {
|
|
|
36942
36943
|
dependencies: {
|
|
36943
36944
|
"@aws-sdk/client-ecs": "^3.1079.0",
|
|
36944
36945
|
"@aws-sdk/client-s3": "^3.1079.0",
|
|
36945
|
-
"@hasna/events": "0.1.
|
|
36946
|
+
"@hasna/events": "0.1.16",
|
|
36946
36947
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
36947
36948
|
chalk: "^5.3.0",
|
|
36948
36949
|
commander: "^12.1.0",
|
|
@@ -37956,6 +37957,9 @@ function normalizeConfigValue(key, value) {
|
|
|
37956
37957
|
return value.trim() ? value : undefined;
|
|
37957
37958
|
return;
|
|
37958
37959
|
}
|
|
37960
|
+
function isOwnerLayoutMigrated(appDir) {
|
|
37961
|
+
return existsSync3(join3(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
|
|
37962
|
+
}
|
|
37959
37963
|
function getDataDir() {
|
|
37960
37964
|
const override = process.env[DATA_DIR_ENV];
|
|
37961
37965
|
if (override) {
|
|
@@ -37979,6 +37983,33 @@ function getDataDir() {
|
|
|
37979
37983
|
}
|
|
37980
37984
|
return newDir;
|
|
37981
37985
|
}
|
|
37986
|
+
function getDataDirReadOnly() {
|
|
37987
|
+
const override = process.env[DATA_DIR_ENV];
|
|
37988
|
+
if (override)
|
|
37989
|
+
return override;
|
|
37990
|
+
return join3(process.env["HOME"] || process.env["USERPROFILE"] || homedir2(), ".hasna", "skills");
|
|
37991
|
+
}
|
|
37992
|
+
function getConfigPathReadOnly(scope) {
|
|
37993
|
+
if (scope === "global")
|
|
37994
|
+
return join3(getDataDirReadOnly(), "config.json");
|
|
37995
|
+
return join3(process.cwd(), "skills.config.json");
|
|
37996
|
+
}
|
|
37997
|
+
function loadConfigReadOnly() {
|
|
37998
|
+
const canonicalConfigPath = getConfigPathReadOnly("global");
|
|
37999
|
+
let globalConfig;
|
|
38000
|
+
if (existsSync3(canonicalConfigPath)) {
|
|
38001
|
+
globalConfig = readConfigFile(canonicalConfigPath);
|
|
38002
|
+
} else if (process.env[DATA_DIR_ENV] !== undefined) {
|
|
38003
|
+
globalConfig = {};
|
|
38004
|
+
} else {
|
|
38005
|
+
globalConfig = readConfigFile(legacyConfigFilePath());
|
|
38006
|
+
}
|
|
38007
|
+
const projectConfig = readConfigFile(getConfigPathReadOnly("project"));
|
|
38008
|
+
return { ...globalConfig, ...projectConfig };
|
|
38009
|
+
}
|
|
38010
|
+
function legacyConfigFilePath() {
|
|
38011
|
+
return join3(process.env["HOME"] || process.env["USERPROFILE"] || homedir2(), ".skillsrc");
|
|
38012
|
+
}
|
|
37982
38013
|
function getConfigPath(scope) {
|
|
37983
38014
|
if (scope === "global") {
|
|
37984
38015
|
return join3(getDataDir(), "config.json");
|
|
@@ -38065,7 +38096,7 @@ function unsetConfig(key, scope = "project") {
|
|
|
38065
38096
|
`);
|
|
38066
38097
|
return true;
|
|
38067
38098
|
}
|
|
38068
|
-
var ENUM_KEYS, STRING_KEYS, DATA_DIR_ENV = "HASNA_SKILLS_DIR", INSTALLED_SKILLS_DIRNAME = "installed";
|
|
38099
|
+
var ENUM_KEYS, STRING_KEYS, DATA_DIR_ENV = "HASNA_SKILLS_DIR", INSTALLED_SKILLS_DIRNAME = "installed", SKILLS_CACHE_DIRNAME = "skills", LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
|
|
38069
38100
|
var init_config = __esm(() => {
|
|
38070
38101
|
init_retired_settings();
|
|
38071
38102
|
ENUM_KEYS = {
|
|
@@ -38236,7 +38267,7 @@ var init_development_tools = __esm(() => {
|
|
|
38236
38267
|
{
|
|
38237
38268
|
name: "monitor",
|
|
38238
38269
|
displayName: "Monitor",
|
|
38239
|
-
description: "Operate the
|
|
38270
|
+
description: "Operate the monitor MCP for machine health, processes, cron jobs, and cleanup workflows",
|
|
38240
38271
|
category: "Development Tools",
|
|
38241
38272
|
tags: ["monitoring", "mcp", "processes", "operations"]
|
|
38242
38273
|
},
|
|
@@ -38282,6 +38313,14 @@ var init_development_tools = __esm(() => {
|
|
|
38282
38313
|
description: "Validate configuration files for syntax and schema compliance",
|
|
38283
38314
|
category: "Development Tools",
|
|
38284
38315
|
tags: ["config", "validation", "schema", "linting"]
|
|
38316
|
+
},
|
|
38317
|
+
{
|
|
38318
|
+
name: "session-inject-monitor",
|
|
38319
|
+
displayName: "Session Inject Monitor",
|
|
38320
|
+
description: "Set up a declarative monitor that injects a prompt into a live coding-agent session when a watched source (conversations, email, todos, knowledge, command output) has new content",
|
|
38321
|
+
category: "Development Tools",
|
|
38322
|
+
tags: ["monitor", "session", "injection", "automation", "wake"],
|
|
38323
|
+
kind: "instruction"
|
|
38285
38324
|
}
|
|
38286
38325
|
];
|
|
38287
38326
|
});
|
|
@@ -38693,7 +38732,7 @@ var init_design_branding = __esm(() => {
|
|
|
38693
38732
|
displayName: "Site Analyze",
|
|
38694
38733
|
description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
|
|
38695
38734
|
category: "Design & Branding",
|
|
38696
|
-
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "
|
|
38735
|
+
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "styles"]
|
|
38697
38736
|
}
|
|
38698
38737
|
];
|
|
38699
38738
|
});
|
|
@@ -40114,6 +40153,9 @@ function getPortableSkillsRoot(options = {}) {
|
|
|
40114
40153
|
if (options.rootDir)
|
|
40115
40154
|
return options.rootDir;
|
|
40116
40155
|
const appDir = options.homeDir ? join7(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
40156
|
+
const cache3 = join7(appDir, SKILLS_CACHE_DIRNAME);
|
|
40157
|
+
if (isOwnerLayoutMigrated(appDir) && safeIsDirectory(cache3))
|
|
40158
|
+
return cache3;
|
|
40117
40159
|
const installed = join7(appDir, INSTALLED_SKILLS_DIRNAME);
|
|
40118
40160
|
migrateLegacySkillLayout(appDir, installed);
|
|
40119
40161
|
return installed;
|
|
@@ -40333,10 +40375,7 @@ function writeCorpusSkill(input, options = {}) {
|
|
|
40333
40375
|
const skillPath = join7(root, name);
|
|
40334
40376
|
const created = !existsSync7(skillPath);
|
|
40335
40377
|
mkdirSync3(skillPath, { recursive: true });
|
|
40336
|
-
|
|
40337
|
-
`) ? input.skillMd : `${input.skillMd}
|
|
40338
|
-
`;
|
|
40339
|
-
writeFileSync3(join7(skillPath, "SKILL.md"), skillMd);
|
|
40378
|
+
writeFileSync3(join7(skillPath, "SKILL.md"), input.skillMd);
|
|
40340
40379
|
const frontmatter = parseSkillFrontmatter(input.skillMd) ?? undefined;
|
|
40341
40380
|
const kind = input.meta?.kind ?? parseSkillKind(frontmatter?.kind) ?? "instruction";
|
|
40342
40381
|
const manifest = {
|
|
@@ -42516,16 +42555,7 @@ import { join as join9 } from "path";
|
|
|
42516
42555
|
function layoutMigrationRecordPath(appDir) {
|
|
42517
42556
|
return join9(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD);
|
|
42518
42557
|
}
|
|
42519
|
-
function isOwnerLayoutMigrated(appDir) {
|
|
42520
|
-
return existsSync9(layoutMigrationRecordPath(appDir));
|
|
42521
|
-
}
|
|
42522
42558
|
function resolveCorpusRoot(options = {}) {
|
|
42523
|
-
if (options.rootDir)
|
|
42524
|
-
return options.rootDir;
|
|
42525
|
-
const appDir = options.homeDir ? join9(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
42526
|
-
const cache3 = join9(appDir, SKILLS_CACHE_DIRNAME);
|
|
42527
|
-
if (isOwnerLayoutMigrated(appDir) && existsSync9(cache3))
|
|
42528
|
-
return cache3;
|
|
42529
42559
|
return getPortableSkillsRoot(options);
|
|
42530
42560
|
}
|
|
42531
42561
|
function migrationAppDir(homeDir) {
|
|
@@ -42647,10 +42677,11 @@ function migrateOwnerLayout(options = {}) {
|
|
|
42647
42677
|
`);
|
|
42648
42678
|
return { status: "migrated", moved, created, record };
|
|
42649
42679
|
}
|
|
42650
|
-
var
|
|
42680
|
+
var LOGS_DIRNAME = "logs", OUTPUTS_DIRNAME = "outputs", LEGACY_CUSTOM_DIRNAME2 = "custom";
|
|
42651
42681
|
var init_home_migration = __esm(() => {
|
|
42652
42682
|
init_config();
|
|
42653
42683
|
init_portable_skills();
|
|
42684
|
+
init_config();
|
|
42654
42685
|
});
|
|
42655
42686
|
|
|
42656
42687
|
// src/lib/agent-sync.ts
|
|
@@ -42705,6 +42736,9 @@ ${lines.join(`
|
|
|
42705
42736
|
---
|
|
42706
42737
|
${body}`;
|
|
42707
42738
|
}
|
|
42739
|
+
function isPointerSkillMd(markdown) {
|
|
42740
|
+
return markdown.includes(POINTER_MARKER_PHRASE) && /^kind:\s*executable\b/m.test(markdown);
|
|
42741
|
+
}
|
|
42708
42742
|
function pointerSkillMd(name, description) {
|
|
42709
42743
|
const display = name.replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
42710
42744
|
return [
|
|
@@ -42730,7 +42764,7 @@ function resolveSyncCorpus(options = {}) {
|
|
|
42730
42764
|
if (explicit) {
|
|
42731
42765
|
const roots = packageSourceRoots(explicit);
|
|
42732
42766
|
if (roots.length === 0) {
|
|
42733
|
-
throw new Error(`SKILLS_SOURCE '${explicit}' contains no skills: expected a corpus directory or a package root with skills
|
|
42767
|
+
throw new Error(`SKILLS_SOURCE '${explicit}' contains no skills: expected a corpus directory or a package root with skills/`);
|
|
42734
42768
|
}
|
|
42735
42769
|
return { roots, source: "source" };
|
|
42736
42770
|
}
|
|
@@ -42738,7 +42772,7 @@ function resolveSyncCorpus(options = {}) {
|
|
|
42738
42772
|
}
|
|
42739
42773
|
function packageSourceRoots(source) {
|
|
42740
42774
|
const roots = [];
|
|
42741
|
-
for (const sub of ["skills"
|
|
42775
|
+
for (const sub of ["skills"]) {
|
|
42742
42776
|
const candidate = join10(source, sub);
|
|
42743
42777
|
if (existsSync10(candidate) && isDirectory(candidate))
|
|
42744
42778
|
roots.push(candidate);
|
|
@@ -42863,6 +42897,21 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
42863
42897
|
reason: "an unmanaged SKILL.md already exists here (hand-authored); pass --force to overwrite"
|
|
42864
42898
|
};
|
|
42865
42899
|
}
|
|
42900
|
+
if (dirExists && managed && hasSkillMd && !options.force && isPointerSkillMd(skillMd)) {
|
|
42901
|
+
let existingIsStub = false;
|
|
42902
|
+
try {
|
|
42903
|
+
existingIsStub = isPointerSkillMd(readFileSync7(skillMdPath, "utf-8"));
|
|
42904
|
+
} catch {
|
|
42905
|
+
existingIsStub = false;
|
|
42906
|
+
}
|
|
42907
|
+
if (!existingIsStub) {
|
|
42908
|
+
return {
|
|
42909
|
+
action: "skip",
|
|
42910
|
+
path: skillMdPath,
|
|
42911
|
+
reason: "refusing to replace a content-bearing managed home with an executable pointer stub (the corpus entry lacks kind: instruction); pass --force to overwrite"
|
|
42912
|
+
};
|
|
42913
|
+
}
|
|
42914
|
+
}
|
|
42866
42915
|
const action = dirExists ? "update" : "create";
|
|
42867
42916
|
if (options.dryRun)
|
|
42868
42917
|
return { action, path: skillMdPath };
|
|
@@ -42947,7 +42996,7 @@ function normalizeRequested(names) {
|
|
|
42947
42996
|
const normalized = names.map((name) => name.trim()).filter(Boolean).map((name) => normalizePortableSkillName(name));
|
|
42948
42997
|
return [...new Set(normalized)];
|
|
42949
42998
|
}
|
|
42950
|
-
var SYNC_AGENTS, SKILLS_SOURCE_ENV = "SKILLS_SOURCE", SYNC_MARKER_FILE = ".hasna-skills.json", SYNC_MARKER_MANAGED_BY = "@hasna/skills";
|
|
42999
|
+
var SYNC_AGENTS, SKILLS_SOURCE_ENV = "SKILLS_SOURCE", SYNC_MARKER_FILE = ".hasna-skills.json", SYNC_MARKER_MANAGED_BY = "@hasna/skills", POINTER_MARKER_PHRASE = "This is an executable skill from the @hasna/skills catalog";
|
|
42951
43000
|
var init_agent_sync = __esm(() => {
|
|
42952
43001
|
init_portable_skills();
|
|
42953
43002
|
init_home_migration();
|
|
@@ -47539,19 +47588,33 @@ var exports_auth_store = {};
|
|
|
47539
47588
|
__export(exports_auth_store, {
|
|
47540
47589
|
saveAuthConfig: () => saveAuthConfig,
|
|
47541
47590
|
normalizeSkillsApiOrigin: () => normalizeSkillsApiOrigin,
|
|
47591
|
+
getAuthFilePathReadOnly: () => getAuthFilePathReadOnly,
|
|
47592
|
+
getAuthFilePath: () => getAuthFilePath,
|
|
47593
|
+
getAuthConfigReadOnly: () => getAuthConfigReadOnly,
|
|
47542
47594
|
getAuthConfig: () => getAuthConfig,
|
|
47543
47595
|
getApiUrl: () => getApiUrl,
|
|
47596
|
+
getApiKeyReadOnly: () => getApiKeyReadOnly,
|
|
47544
47597
|
getApiKey: () => getApiKey,
|
|
47545
47598
|
clearAuthConfig: () => clearAuthConfig
|
|
47546
47599
|
});
|
|
47547
47600
|
import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync7, unlinkSync } from "fs";
|
|
47548
|
-
import { join as join13 } from "path";
|
|
47601
|
+
import { dirname as dirname6, join as join13 } from "path";
|
|
47549
47602
|
import { homedir as homedir5 } from "os";
|
|
47603
|
+
function getAuthFilePath() {
|
|
47604
|
+
return join13(getDataDir(), "auth.json");
|
|
47605
|
+
}
|
|
47606
|
+
function getAuthFilePathReadOnly() {
|
|
47607
|
+
return join13(getDataDirReadOnly(), "auth.json");
|
|
47608
|
+
}
|
|
47609
|
+
function legacyAuthFilePath() {
|
|
47610
|
+
return join13(process.env["HOME"] || process.env["USERPROFILE"] || homedir5(), ".skills", "auth.json");
|
|
47611
|
+
}
|
|
47550
47612
|
function getAuthConfig() {
|
|
47551
47613
|
if (cachedConfig !== undefined)
|
|
47552
47614
|
return cachedConfig;
|
|
47553
47615
|
try {
|
|
47554
|
-
const
|
|
47616
|
+
const file = existsSync13(getAuthFilePath()) ? getAuthFilePath() : legacyAuthFilePath();
|
|
47617
|
+
const raw = readFileSync10(file, "utf-8");
|
|
47555
47618
|
const config = JSON.parse(raw);
|
|
47556
47619
|
if (!config.apiKey) {
|
|
47557
47620
|
cachedConfig = null;
|
|
@@ -47565,19 +47628,20 @@ function getAuthConfig() {
|
|
|
47565
47628
|
}
|
|
47566
47629
|
}
|
|
47567
47630
|
function saveAuthConfig(config) {
|
|
47568
|
-
|
|
47569
|
-
|
|
47631
|
+
const file = getAuthFilePath();
|
|
47632
|
+
mkdirSync7(dirname6(file), { recursive: true, mode: 448 });
|
|
47633
|
+
writeFileSync7(file, JSON.stringify(config, null, 2) + `
|
|
47570
47634
|
`, { mode: 384 });
|
|
47571
47635
|
cachedConfig = config;
|
|
47572
47636
|
}
|
|
47573
47637
|
function clearAuthConfig() {
|
|
47574
47638
|
try {
|
|
47575
|
-
unlinkSync(
|
|
47639
|
+
unlinkSync(getAuthFilePath());
|
|
47576
47640
|
} catch {}
|
|
47577
47641
|
try {
|
|
47578
|
-
unlinkSync(
|
|
47642
|
+
unlinkSync(legacyAuthFilePath());
|
|
47579
47643
|
} catch {}
|
|
47580
|
-
cachedConfig =
|
|
47644
|
+
cachedConfig = undefined;
|
|
47581
47645
|
}
|
|
47582
47646
|
function getApiKey() {
|
|
47583
47647
|
if (process.env.SKILLS_API_KEY)
|
|
@@ -47586,6 +47650,25 @@ function getApiKey() {
|
|
|
47586
47650
|
return process.env.SKILL_API_KEY;
|
|
47587
47651
|
return getAuthConfig()?.apiKey || null;
|
|
47588
47652
|
}
|
|
47653
|
+
function getAuthConfigReadOnly() {
|
|
47654
|
+
try {
|
|
47655
|
+
const file = existsSync13(getAuthFilePathReadOnly()) ? getAuthFilePathReadOnly() : legacyAuthFilePath();
|
|
47656
|
+
const raw = readFileSync10(file, "utf-8");
|
|
47657
|
+
const config = JSON.parse(raw);
|
|
47658
|
+
if (!config.apiKey)
|
|
47659
|
+
return null;
|
|
47660
|
+
return config;
|
|
47661
|
+
} catch {
|
|
47662
|
+
return null;
|
|
47663
|
+
}
|
|
47664
|
+
}
|
|
47665
|
+
function getApiKeyReadOnly() {
|
|
47666
|
+
if (process.env.SKILLS_API_KEY)
|
|
47667
|
+
return process.env.SKILLS_API_KEY;
|
|
47668
|
+
if (process.env.SKILL_API_KEY)
|
|
47669
|
+
return process.env.SKILL_API_KEY;
|
|
47670
|
+
return getAuthConfigReadOnly()?.apiKey || null;
|
|
47671
|
+
}
|
|
47589
47672
|
function normalizeSkillsApiOrigin(apiUrl) {
|
|
47590
47673
|
const url = new URL(apiUrl);
|
|
47591
47674
|
const pathname = url.pathname.replace(/\/+$/, "");
|
|
@@ -47601,12 +47684,10 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
47601
47684
|
function getApiUrl(action) {
|
|
47602
47685
|
return normalizeSkillsApiOrigin(requireApiUrl(action));
|
|
47603
47686
|
}
|
|
47604
|
-
var
|
|
47687
|
+
var cachedConfig;
|
|
47605
47688
|
var init_auth_store = __esm(() => {
|
|
47606
47689
|
init_api_url();
|
|
47607
|
-
|
|
47608
|
-
AUTH_FILE = join13(AUTH_DIR, "auth.json");
|
|
47609
|
-
LEGACY_AUTH_FILE = join13(homedir5(), ".skills", "auth.json");
|
|
47690
|
+
init_config();
|
|
47610
47691
|
});
|
|
47611
47692
|
|
|
47612
47693
|
// src/lib/remote-registry.ts
|
|
@@ -47723,6 +47804,16 @@ async function loadRemoteRegistry(options = {}) {
|
|
|
47723
47804
|
const url = buildSkillsApiUrl(apiUrl, options.endpoint);
|
|
47724
47805
|
return parseRemoteRegistryPayload(await fetchRemoteJson(url, options));
|
|
47725
47806
|
}
|
|
47807
|
+
async function mergeRemoteRegistry(local, options = {}) {
|
|
47808
|
+
const apiUrl = options.apiUrl || getConfiguredApiUrl();
|
|
47809
|
+
if (!apiUrl)
|
|
47810
|
+
return local;
|
|
47811
|
+
const token = options.authToken !== undefined ? options.authToken : getApiKey();
|
|
47812
|
+
if (!token?.trim())
|
|
47813
|
+
return local;
|
|
47814
|
+
const remote = await loadRemoteRegistry({ ...options, apiUrl, authToken: token });
|
|
47815
|
+
return mergeSkillRegistryLists(local, remote);
|
|
47816
|
+
}
|
|
47726
47817
|
async function loadRemoteSkill(name, options = {}) {
|
|
47727
47818
|
const apiUrl = options.apiUrl || getConfiguredApiUrl();
|
|
47728
47819
|
if (!apiUrl) {
|
|
@@ -47739,6 +47830,7 @@ var init_remote_registry = __esm(() => {
|
|
|
47739
47830
|
init_auth_store();
|
|
47740
47831
|
init_config();
|
|
47741
47832
|
init_discovery();
|
|
47833
|
+
init_registry_merge();
|
|
47742
47834
|
remoteAvailabilitySchema = exports_external.object({
|
|
47743
47835
|
status: exports_external.enum(["available", "unavailable"]),
|
|
47744
47836
|
code: exports_external.string().optional(),
|
|
@@ -48077,7 +48169,7 @@ __export(exports_list, {
|
|
|
48077
48169
|
registerBrowse: () => registerBrowse
|
|
48078
48170
|
});
|
|
48079
48171
|
function registerBrowse(parent) {
|
|
48080
|
-
parent.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-p, --pinned", "Show only pinned skills", false).option("-t, --tags <tags>", "Filter by comma-separated tags (OR logic, case-insensitive)").option("--all", "Show the full skill registry instead of the default basic set", false).option("--remote", "Use remote registry from SKILLS_API_URL or config apiUrl", false).option("--json", "Output as JSON", false).option("--brief", "One line per skill: name \u2014 description [category]", false).option("--format <format>", "Output format: compact (names only) or csv (name,category,price,description)").option("--limit <n>", "Maximum rows to print for human output (default: 30, use 0 or all for every row)").option("--cursor <n>", "Numeric offset for human-output pagination", "0").option("--verbose", "Show longer descriptions and tags in human output", false).action((options) => {
|
|
48172
|
+
parent.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-p, --pinned", "Show only pinned skills", false).option("-t, --tags <tags>", "Filter by comma-separated tags (OR logic, case-insensitive)").option("--tag <tags>", "Filter by comma-separated tags (alias for --tags)").option("--all", "Show the full skill registry instead of the default basic set", false).option("--remote", "Use remote registry from SKILLS_API_URL or config apiUrl", false).option("--json", "Output as JSON", false).option("--brief", "One line per skill: name \u2014 description [category]", false).option("--format <format>", "Output format: compact (names only) or csv (name,category,price,description)").option("--limit <n>", "Maximum rows to print for human output (default: 30, use 0 or all for every row)").option("--cursor <n>", "Numeric offset for human-output pagination", "0").option("--verbose", "Show longer descriptions and tags in human output", false).action((options) => {
|
|
48081
48173
|
return handleList(options).catch(handleBrowseError);
|
|
48082
48174
|
});
|
|
48083
48175
|
parent.command("search").alias("s").argument("<query>", "Search term").option("--json", "Output as JSON", false).option("--brief", "One line per skill: name \u2014 description [category]", false).option("--format <format>", "Output format: compact (names only) or csv (name,category,price,description)").option("-c, --category <category>", "Filter results by category").option("-t, --tags <tags>", "Filter results by comma-separated tags (OR logic, case-insensitive)").option("--all", "Search the full skill registry instead of the default basic set", false).option("--remote", "Use remote registry from SKILLS_API_URL or config apiUrl", false).option("--limit <n>", "Maximum rows to print for human output (default: 20, use 0 or all for every row)").option("--cursor <n>", "Numeric offset for human-output pagination", "0").option("--verbose", "Show longer descriptions and tags in human output", false).description("Search for skills").action((query, options) => {
|
|
@@ -48129,10 +48221,11 @@ async function writeJson(value, space) {
|
|
|
48129
48221
|
async function getBrowseRegistry(options) {
|
|
48130
48222
|
const profile = options.all ? "all" : "basic";
|
|
48131
48223
|
const local = loadRegistryProfile(profile);
|
|
48132
|
-
if (
|
|
48133
|
-
|
|
48134
|
-
|
|
48135
|
-
|
|
48224
|
+
if (options.remote) {
|
|
48225
|
+
const remote = await loadRemoteRegistry();
|
|
48226
|
+
return mergeSkillRegistryLists(local, remote);
|
|
48227
|
+
}
|
|
48228
|
+
return mergeRemoteRegistry(local);
|
|
48136
48229
|
}
|
|
48137
48230
|
function registryCategories(registry) {
|
|
48138
48231
|
const known = CATEGORIES.filter((category) => registry.some((skill) => skill.category === category));
|
|
@@ -48140,7 +48233,13 @@ function registryCategories(registry) {
|
|
|
48140
48233
|
return [...known, ...extra];
|
|
48141
48234
|
}
|
|
48142
48235
|
function availableCategories(options, registry) {
|
|
48143
|
-
|
|
48236
|
+
if (options.remote)
|
|
48237
|
+
return registryCategories(registry);
|
|
48238
|
+
const hasRemoteRows = registry.some((skill) => skill.source === "remote");
|
|
48239
|
+
if (!hasRemoteRows)
|
|
48240
|
+
return [...CATEGORIES];
|
|
48241
|
+
const extras = Array.from(new Set(registry.map((skill) => skill.category))).filter((category) => !CATEGORIES.includes(category)).sort();
|
|
48242
|
+
return [...CATEGORIES, ...extras];
|
|
48144
48243
|
}
|
|
48145
48244
|
async function handleList(options) {
|
|
48146
48245
|
const brief = options.brief && !options.json;
|
|
@@ -48185,7 +48284,8 @@ Pinned skills (${showingLabel(installed.length, page2.items.length, page2.offset
|
|
|
48185
48284
|
printPageHint(page2, "skills list --pinned");
|
|
48186
48285
|
return;
|
|
48187
48286
|
}
|
|
48188
|
-
const
|
|
48287
|
+
const tagsOption = options.tags ?? options.tag;
|
|
48288
|
+
const tagFilter = tagsOption ? tagsOption.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean) : null;
|
|
48189
48289
|
if (options.category) {
|
|
48190
48290
|
const categories = availableCategories(options, registry);
|
|
48191
48291
|
const category = categories.find((c) => c.toLowerCase() === options.category.toLowerCase());
|
|
@@ -48396,6 +48496,8 @@ function listCommand(options) {
|
|
|
48396
48496
|
parts.push("--category", quoteArg(options.category));
|
|
48397
48497
|
if (options.tags)
|
|
48398
48498
|
parts.push("--tags", quoteArg(options.tags));
|
|
48499
|
+
else if (options.tag)
|
|
48500
|
+
parts.push("--tag", quoteArg(options.tag));
|
|
48399
48501
|
if (options.pinned)
|
|
48400
48502
|
parts.push("--pinned");
|
|
48401
48503
|
return parts.join(" ");
|
|
@@ -49100,7 +49202,8 @@ function handleDiff(name, options) {
|
|
|
49100
49202
|
const canonical = {
|
|
49101
49203
|
present: existsSync15(canonicalSkillMd),
|
|
49102
49204
|
path: canonicalDir,
|
|
49103
|
-
...existsSync15(canonicalSkillMd) ? { hash: hashSkillMarkdownFile(canonicalSkillMd) } : {}
|
|
49205
|
+
...existsSync15(canonicalSkillMd) ? { hash: hashSkillMarkdownFile(canonicalSkillMd) } : {},
|
|
49206
|
+
...existsSync15(canonicalSkillMd) ? { stub: isPointerSkillMd(readFileSync12(canonicalSkillMd, "utf-8")) } : {}
|
|
49104
49207
|
};
|
|
49105
49208
|
const pinned = getInstalledSkills().includes(bare);
|
|
49106
49209
|
const installMeta = getInstallMeta();
|
|
@@ -49120,6 +49223,14 @@ function handleDiff(name, options) {
|
|
|
49120
49223
|
const managed = existsSync15(join15(dir, SYNC_MARKER_FILE));
|
|
49121
49224
|
const skillMdPath = join15(dir, "SKILL.md");
|
|
49122
49225
|
const hash = present && existsSync15(skillMdPath) ? hashSkillMarkdownFile(skillMdPath) : undefined;
|
|
49226
|
+
let stub;
|
|
49227
|
+
if (present && existsSync15(skillMdPath)) {
|
|
49228
|
+
try {
|
|
49229
|
+
stub = isPointerSkillMd(readFileSync12(skillMdPath, "utf-8"));
|
|
49230
|
+
} catch {
|
|
49231
|
+
stub = undefined;
|
|
49232
|
+
}
|
|
49233
|
+
}
|
|
49123
49234
|
const diverged = present && canonical.present && managed && hash !== canonical.hash;
|
|
49124
49235
|
homes.push({
|
|
49125
49236
|
agent,
|
|
@@ -49127,6 +49238,7 @@ function handleDiff(name, options) {
|
|
|
49127
49238
|
present,
|
|
49128
49239
|
managed,
|
|
49129
49240
|
...hash ? { hash } : {},
|
|
49241
|
+
...stub !== undefined ? { stub } : {},
|
|
49130
49242
|
...diverged !== undefined ? { diverged } : {}
|
|
49131
49243
|
});
|
|
49132
49244
|
}
|
|
@@ -49156,7 +49268,8 @@ function handleDiff(name, options) {
|
|
|
49156
49268
|
} else if (!home.managed) {
|
|
49157
49269
|
console.log(`${source_default.dim("\u2022")} ${home.agent}: ${source_default.yellow("unmarked (adoption candidate)")}`);
|
|
49158
49270
|
} else if (home.diverged) {
|
|
49159
|
-
|
|
49271
|
+
const stubNote = home.stub === true ? " (home is a pointer stub; canonical holds content)" : canonical.stub === true ? " (home holds content; canonical renders a pointer stub \u2014 sync refuses to replace it)" : "";
|
|
49272
|
+
console.log(`${source_default.red("\u2717")} ${home.agent}: ${source_default.red("diverged")} ${source_default.dim(home.hash?.slice(0, 12))} \u2260 ${source_default.dim(canonical.hash?.slice(0, 12))}${stubNote}`);
|
|
49160
49273
|
} else {
|
|
49161
49274
|
console.log(`${source_default.green("\u2713")} ${home.agent}: ${source_default.green("matches canonical")}`);
|
|
49162
49275
|
}
|
|
@@ -50085,7 +50198,7 @@ var init_home_adoption = __esm(() => {
|
|
|
50085
50198
|
});
|
|
50086
50199
|
|
|
50087
50200
|
// src/lib/home-census.ts
|
|
50088
|
-
import { existsSync as existsSync18, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
|
|
50201
|
+
import { existsSync as existsSync18, readFileSync as readFileSync15, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
|
|
50089
50202
|
import { homedir as homedir7 } from "os";
|
|
50090
50203
|
import { join as join18 } from "path";
|
|
50091
50204
|
function sortEntries(entries) {
|
|
@@ -50145,7 +50258,20 @@ function censusHomeDrift(options = {}) {
|
|
|
50145
50258
|
}
|
|
50146
50259
|
const homeHash = hashSkillMarkdownFile(skillMdPath);
|
|
50147
50260
|
if (homeHash !== canonicalHash) {
|
|
50148
|
-
|
|
50261
|
+
let homeStub;
|
|
50262
|
+
try {
|
|
50263
|
+
homeStub = isPointerSkillMd(readFileSync15(skillMdPath, "utf-8"));
|
|
50264
|
+
} catch {
|
|
50265
|
+
homeStub = undefined;
|
|
50266
|
+
}
|
|
50267
|
+
let canonicalStub;
|
|
50268
|
+
const canonicalSkillMd = join18(corpusRoot, skill, "SKILL.md");
|
|
50269
|
+
try {
|
|
50270
|
+
canonicalStub = isPointerSkillMd(readFileSync15(canonicalSkillMd, "utf-8"));
|
|
50271
|
+
} catch {
|
|
50272
|
+
canonicalStub = undefined;
|
|
50273
|
+
}
|
|
50274
|
+
entries.push({ agent, skill, kind: "diverged", path: dir, homeHash, canonicalHash, homeStub, canonicalStub });
|
|
50149
50275
|
}
|
|
50150
50276
|
}
|
|
50151
50277
|
for (const [name, canonicalHash] of index) {
|
|
@@ -50180,7 +50306,7 @@ var exports_diagnostic = {};
|
|
|
50180
50306
|
__export(exports_diagnostic, {
|
|
50181
50307
|
registerDiagnostic: () => registerDiagnostic
|
|
50182
50308
|
});
|
|
50183
|
-
import { existsSync as existsSync19, readFileSync as
|
|
50309
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
50184
50310
|
import { join as join19 } from "path";
|
|
50185
50311
|
import { execSync as execSync2 } from "child_process";
|
|
50186
50312
|
function registerDiagnostic(parent) {
|
|
@@ -50319,7 +50445,7 @@ function handleAuth(name, options) {
|
|
|
50319
50445
|
process.exitCode = 1;
|
|
50320
50446
|
return;
|
|
50321
50447
|
}
|
|
50322
|
-
let existing = existsSync19(envFilePath) ?
|
|
50448
|
+
let existing = existsSync19(envFilePath) ? readFileSync16(envFilePath, "utf-8") : "";
|
|
50323
50449
|
const keyPattern = new RegExp(`^${key}=.*$`, "m");
|
|
50324
50450
|
const updated = keyPattern.test(existing) ? existing.replace(keyPattern, `${key}=${value}`) : existing.endsWith(`
|
|
50325
50451
|
`) || existing === "" ? existing + `${key}=${value}
|
|
@@ -50427,7 +50553,7 @@ function handleOutdated(options) {
|
|
|
50427
50553
|
let registryVersion = "unknown";
|
|
50428
50554
|
if (existsSync19(registryPkgPath))
|
|
50429
50555
|
try {
|
|
50430
|
-
registryVersion = JSON.parse(
|
|
50556
|
+
registryVersion = JSON.parse(readFileSync16(registryPkgPath, "utf-8")).version || "unknown";
|
|
50431
50557
|
} catch {}
|
|
50432
50558
|
if (installedVersion !== registryVersion)
|
|
50433
50559
|
pins.push({ skill: name, installedVersion, registryVersion });
|
|
@@ -50691,7 +50817,7 @@ var init_runs = __esm(() => {
|
|
|
50691
50817
|
|
|
50692
50818
|
// src/lib/run-state.ts
|
|
50693
50819
|
import { createHash as createHash2, randomBytes } from "crypto";
|
|
50694
|
-
import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as
|
|
50820
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync17, readdirSync as readdirSync11, statSync as statSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
50695
50821
|
import { extname, join as join20, relative as relative2 } from "path";
|
|
50696
50822
|
function createSkillRun(params, targetDir = process.cwd()) {
|
|
50697
50823
|
const now3 = new Date;
|
|
@@ -50762,7 +50888,7 @@ function appendRunEvent(context, event, data = {}) {
|
|
|
50762
50888
|
const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
|
|
50763
50889
|
`;
|
|
50764
50890
|
const path = join20(context.runDir, "events.ndjson");
|
|
50765
|
-
const previous = existsSync20(path) ?
|
|
50891
|
+
const previous = existsSync20(path) ? readFileSync17(path, "utf-8") : "";
|
|
50766
50892
|
writeFileSync11(path, previous + line);
|
|
50767
50893
|
}
|
|
50768
50894
|
function listSkillRuns(targetDir = process.cwd(), limit = 50) {
|
|
@@ -50822,7 +50948,7 @@ function collectRunArtifacts(context) {
|
|
|
50822
50948
|
const artifacts = [];
|
|
50823
50949
|
for (const path of walkFiles(context.exportDir)) {
|
|
50824
50950
|
const stat = statSync11(path);
|
|
50825
|
-
const bytes =
|
|
50951
|
+
const bytes = readFileSync17(path);
|
|
50826
50952
|
artifacts.push({
|
|
50827
50953
|
path: toProjectRelative(context.targetDir, path),
|
|
50828
50954
|
mime: mimeForPath(path),
|
|
@@ -50837,7 +50963,7 @@ function readRunRecord(runDir) {
|
|
|
50837
50963
|
if (!existsSync20(path))
|
|
50838
50964
|
return null;
|
|
50839
50965
|
try {
|
|
50840
|
-
return JSON.parse(
|
|
50966
|
+
return JSON.parse(readFileSync17(path, "utf-8"));
|
|
50841
50967
|
} catch {
|
|
50842
50968
|
return null;
|
|
50843
50969
|
}
|
|
@@ -68063,8 +68189,11 @@ var init_discovery_tools = __esm(() => {
|
|
|
68063
68189
|
// src/lib/remote-client.ts
|
|
68064
68190
|
var exports_remote_client = {};
|
|
68065
68191
|
__export(exports_remote_client, {
|
|
68192
|
+
createRemoteSkillsClientReadOnly: () => createRemoteSkillsClientReadOnly,
|
|
68066
68193
|
createRemoteSkillsClient: () => createRemoteSkillsClient,
|
|
68067
|
-
RemoteSkillsClient: () => RemoteSkillsClient
|
|
68194
|
+
RemoteSkillsClient: () => RemoteSkillsClient,
|
|
68195
|
+
RemoteRouteUnsupportedError: () => RemoteRouteUnsupportedError,
|
|
68196
|
+
RemoteRequestError: () => RemoteRequestError
|
|
68068
68197
|
});
|
|
68069
68198
|
|
|
68070
68199
|
class RemoteSkillsClient {
|
|
@@ -68084,6 +68213,20 @@ class RemoteSkillsClient {
|
|
|
68084
68213
|
}
|
|
68085
68214
|
});
|
|
68086
68215
|
}
|
|
68216
|
+
async requestNewRoute(path, options, opts = {}) {
|
|
68217
|
+
const response = await this.request(path, options);
|
|
68218
|
+
const routePath = path.split("?")[0];
|
|
68219
|
+
if (response.status === 404 || response.status === 405) {
|
|
68220
|
+
if (response.status === 404 && opts.domainNotFoundCodes?.length && await responseBodyCarriesCode(response, opts.domainNotFoundCodes)) {
|
|
68221
|
+
return response;
|
|
68222
|
+
}
|
|
68223
|
+
throw new RemoteRouteUnsupportedError(routePath, response.status, this.apiUrl);
|
|
68224
|
+
}
|
|
68225
|
+
if (!response.ok) {
|
|
68226
|
+
throw new RemoteRequestError(routePath, response.status, response.statusText);
|
|
68227
|
+
}
|
|
68228
|
+
return response;
|
|
68229
|
+
}
|
|
68087
68230
|
async listSkills() {
|
|
68088
68231
|
const res = await this.request("/api/v1/skills");
|
|
68089
68232
|
return res.json();
|
|
@@ -68100,6 +68243,14 @@ class RemoteSkillsClient {
|
|
|
68100
68243
|
return null;
|
|
68101
68244
|
return res.json();
|
|
68102
68245
|
}
|
|
68246
|
+
async getSkillStatus(slug) {
|
|
68247
|
+
const res = await this.request(`/api/v1/skills/${encodeURIComponent(slug)}`, { method: "GET" });
|
|
68248
|
+
let body = null;
|
|
68249
|
+
try {
|
|
68250
|
+
body = await res.json();
|
|
68251
|
+
} catch {}
|
|
68252
|
+
return { status: res.status, body };
|
|
68253
|
+
}
|
|
68103
68254
|
async submitRun(slug, input, args) {
|
|
68104
68255
|
const res = await this.request(`/api/v1/runs/${slug}`, {
|
|
68105
68256
|
method: "POST",
|
|
@@ -68133,15 +68284,18 @@ class RemoteSkillsClient {
|
|
|
68133
68284
|
method: "GET"
|
|
68134
68285
|
});
|
|
68135
68286
|
}
|
|
68136
|
-
async publishSkill(manifest, bundle) {
|
|
68287
|
+
async publishSkill(manifest, bundle, ifMatch) {
|
|
68137
68288
|
const form = new FormData;
|
|
68138
68289
|
form.set("manifest", JSON.stringify(manifest));
|
|
68139
68290
|
if (bundle) {
|
|
68140
68291
|
form.set("bundle", new Blob([bundle], { type: "application/gzip" }), `${String(manifest.slug ?? "skill")}.tar.gz`);
|
|
68141
68292
|
}
|
|
68293
|
+
const headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
68294
|
+
if (ifMatch)
|
|
68295
|
+
headers["If-Match"] = ifMatch;
|
|
68142
68296
|
return fetch(`${this.apiUrl}/api/v1/skills`, {
|
|
68143
68297
|
method: "POST",
|
|
68144
|
-
headers
|
|
68298
|
+
headers,
|
|
68145
68299
|
body: form
|
|
68146
68300
|
});
|
|
68147
68301
|
}
|
|
@@ -68157,6 +68311,135 @@ class RemoteSkillsClient {
|
|
|
68157
68311
|
return null;
|
|
68158
68312
|
return response;
|
|
68159
68313
|
}
|
|
68314
|
+
async listPins() {
|
|
68315
|
+
const response = await this.requestNewRoute("/api/v1/pins");
|
|
68316
|
+
return normalizePinList(await response.json());
|
|
68317
|
+
}
|
|
68318
|
+
async pin(slug, metadata) {
|
|
68319
|
+
const path = `/api/v1/pins/${encodeURIComponent(slug)}`;
|
|
68320
|
+
const response = await this.requestNewRoute(path, {
|
|
68321
|
+
method: "PUT",
|
|
68322
|
+
body: JSON.stringify({ ...metadata ? { metadata } : {} })
|
|
68323
|
+
});
|
|
68324
|
+
return normalizePin(await response.json());
|
|
68325
|
+
}
|
|
68326
|
+
async unpin(slug) {
|
|
68327
|
+
const path = `/api/v1/pins/${encodeURIComponent(slug)}`;
|
|
68328
|
+
const response = await this.requestNewRoute(path, { method: "DELETE" }, { domainNotFoundCodes: ["PIN_NOT_FOUND"] });
|
|
68329
|
+
return response.status !== 404;
|
|
68330
|
+
}
|
|
68331
|
+
async listTags() {
|
|
68332
|
+
const response = await this.requestNewRoute("/api/v1/tags");
|
|
68333
|
+
const payload = await response.json();
|
|
68334
|
+
if (!Array.isArray(payload)) {
|
|
68335
|
+
throw new Error("Remote tags payload did not match the expected contract (expected an array of tag names)");
|
|
68336
|
+
}
|
|
68337
|
+
for (const tag of payload) {
|
|
68338
|
+
if (typeof tag !== "string" || tag.trim().length === 0) {
|
|
68339
|
+
throw new Error("Remote tags payload did not match the expected contract (every element must be a non-empty tag name)");
|
|
68340
|
+
}
|
|
68341
|
+
}
|
|
68342
|
+
return payload;
|
|
68343
|
+
}
|
|
68344
|
+
async skillsByTag(tag) {
|
|
68345
|
+
const path = `/api/v1/tags/${encodeURIComponent(tag)}/skills`;
|
|
68346
|
+
const response = await this.requestNewRoute(path);
|
|
68347
|
+
return normalizeSkillSummaryList(await response.json());
|
|
68348
|
+
}
|
|
68349
|
+
async listUpdatedSince(since, options = {}) {
|
|
68350
|
+
const params = new URLSearchParams({ since });
|
|
68351
|
+
if (options.cursor)
|
|
68352
|
+
params.set("cursor", options.cursor);
|
|
68353
|
+
if (options.limit !== undefined)
|
|
68354
|
+
params.set("limit", String(options.limit));
|
|
68355
|
+
const response = await this.requestNewRoute(`/api/v1/skills/updated?${params.toString()}`);
|
|
68356
|
+
return normalizeUpdatedSincePage(await response.json());
|
|
68357
|
+
}
|
|
68358
|
+
}
|
|
68359
|
+
function requireOptionalString(record3, field) {
|
|
68360
|
+
if (record3[field] === undefined)
|
|
68361
|
+
return;
|
|
68362
|
+
if (typeof record3[field] !== "string") {
|
|
68363
|
+
throw new Error(`Remote payload did not match the expected contract (${field} must be a string when present)`);
|
|
68364
|
+
}
|
|
68365
|
+
return record3[field];
|
|
68366
|
+
}
|
|
68367
|
+
function normalizePin(entry) {
|
|
68368
|
+
if (!entry || typeof entry !== "object") {
|
|
68369
|
+
throw new Error("Remote pin payload did not match the expected contract (expected an object)");
|
|
68370
|
+
}
|
|
68371
|
+
const record3 = entry;
|
|
68372
|
+
const slug = typeof record3.slug === "string" && record3.slug.trim() ? record3.slug.trim() : undefined;
|
|
68373
|
+
if (!slug) {
|
|
68374
|
+
throw new Error("Remote pin payload did not match the expected contract (missing slug)");
|
|
68375
|
+
}
|
|
68376
|
+
let metadata;
|
|
68377
|
+
if (record3.metadata !== undefined) {
|
|
68378
|
+
if (!record3.metadata || typeof record3.metadata !== "object" || Array.isArray(record3.metadata)) {
|
|
68379
|
+
throw new Error("Remote pin payload did not match the expected contract (metadata must be a JSON object when present)");
|
|
68380
|
+
}
|
|
68381
|
+
metadata = record3.metadata;
|
|
68382
|
+
}
|
|
68383
|
+
const pinnedAt = requireOptionalString(record3, "pinnedAt");
|
|
68384
|
+
return {
|
|
68385
|
+
slug,
|
|
68386
|
+
...pinnedAt !== undefined ? { pinnedAt } : {},
|
|
68387
|
+
...metadata ? { metadata } : {}
|
|
68388
|
+
};
|
|
68389
|
+
}
|
|
68390
|
+
function normalizePinList(payload) {
|
|
68391
|
+
if (!Array.isArray(payload)) {
|
|
68392
|
+
throw new Error("Remote pins payload did not match the expected contract (expected an array of pins)");
|
|
68393
|
+
}
|
|
68394
|
+
return payload.map(normalizePin);
|
|
68395
|
+
}
|
|
68396
|
+
function normalizeSkillSummary(entry) {
|
|
68397
|
+
if (!entry || typeof entry !== "object") {
|
|
68398
|
+
throw new Error("Remote skill payload did not match the expected contract (expected an object)");
|
|
68399
|
+
}
|
|
68400
|
+
const record3 = entry;
|
|
68401
|
+
const slug = typeof record3.slug === "string" && record3.slug.trim() ? record3.slug.trim() : undefined;
|
|
68402
|
+
if (!slug) {
|
|
68403
|
+
throw new Error("Remote skill payload did not match the expected contract (missing slug)");
|
|
68404
|
+
}
|
|
68405
|
+
return {
|
|
68406
|
+
slug,
|
|
68407
|
+
...requireOptionalString(record3, "name") !== undefined ? { name: requireOptionalString(record3, "name") } : {},
|
|
68408
|
+
...requireOptionalString(record3, "version") !== undefined ? { version: requireOptionalString(record3, "version") } : {},
|
|
68409
|
+
...requireOptionalString(record3, "updatedAt") !== undefined ? { updatedAt: requireOptionalString(record3, "updatedAt") } : {}
|
|
68410
|
+
};
|
|
68411
|
+
}
|
|
68412
|
+
function normalizeSkillSummaryList(payload) {
|
|
68413
|
+
if (!Array.isArray(payload)) {
|
|
68414
|
+
throw new Error("Remote skills payload did not match the expected contract (expected an array of skills)");
|
|
68415
|
+
}
|
|
68416
|
+
return payload.map(normalizeSkillSummary);
|
|
68417
|
+
}
|
|
68418
|
+
async function responseBodyCarriesCode(response, codes) {
|
|
68419
|
+
try {
|
|
68420
|
+
const payload = await response.clone().json();
|
|
68421
|
+
if (!payload || typeof payload !== "object")
|
|
68422
|
+
return false;
|
|
68423
|
+
const code = payload.code;
|
|
68424
|
+
return typeof code === "string" && codes.includes(code);
|
|
68425
|
+
} catch {
|
|
68426
|
+
return false;
|
|
68427
|
+
}
|
|
68428
|
+
}
|
|
68429
|
+
function normalizeUpdatedSincePage(payload) {
|
|
68430
|
+
if (!payload || typeof payload !== "object") {
|
|
68431
|
+
throw new Error("Updated-since payload did not match the expected contract (expected an object)");
|
|
68432
|
+
}
|
|
68433
|
+
const record3 = payload;
|
|
68434
|
+
if (!Array.isArray(record3.skills)) {
|
|
68435
|
+
throw new Error("Updated-since payload did not match the expected contract (missing skills array)");
|
|
68436
|
+
}
|
|
68437
|
+
const skills = record3.skills.map(normalizeSkillSummary);
|
|
68438
|
+
const nextCursor = record3.nextCursor === undefined || record3.nextCursor === null ? null : record3.nextCursor;
|
|
68439
|
+
if (nextCursor !== null && typeof nextCursor !== "string") {
|
|
68440
|
+
throw new Error("Updated-since payload did not match the expected contract (nextCursor must be a string or absent)");
|
|
68441
|
+
}
|
|
68442
|
+
return { skills, nextCursor };
|
|
68160
68443
|
}
|
|
68161
68444
|
function createRemoteSkillsClient() {
|
|
68162
68445
|
const apiKey = getApiKey();
|
|
@@ -68164,8 +68447,42 @@ function createRemoteSkillsClient() {
|
|
|
68164
68447
|
return null;
|
|
68165
68448
|
return new RemoteSkillsClient(apiKey);
|
|
68166
68449
|
}
|
|
68450
|
+
function createRemoteSkillsClientReadOnly() {
|
|
68451
|
+
const apiKey = getApiKeyReadOnly();
|
|
68452
|
+
if (!apiKey)
|
|
68453
|
+
return null;
|
|
68454
|
+
const apiUrl = resolveApiUrl(loadConfigReadOnly(), process.env);
|
|
68455
|
+
if (!apiUrl)
|
|
68456
|
+
throw new MissingApiUrlError("the cloud group's sync verb (--dry-run)");
|
|
68457
|
+
return new RemoteSkillsClient(apiKey, apiUrl);
|
|
68458
|
+
}
|
|
68459
|
+
var RemoteRouteUnsupportedError, RemoteRequestError;
|
|
68167
68460
|
var init_remote_client = __esm(() => {
|
|
68168
68461
|
init_auth_store();
|
|
68462
|
+
init_api_url();
|
|
68463
|
+
init_config();
|
|
68464
|
+
RemoteRouteUnsupportedError = class RemoteRouteUnsupportedError extends Error {
|
|
68465
|
+
path;
|
|
68466
|
+
status;
|
|
68467
|
+
instance;
|
|
68468
|
+
constructor(path, status, instance) {
|
|
68469
|
+
super(`The configured Skills instance does not support ${path} (HTTP ${status}). ` + `The instance at ${instance} predates this client feature \u2014 upgrade the server, or ` + `use a client version that matches it.`);
|
|
68470
|
+
this.path = path;
|
|
68471
|
+
this.status = status;
|
|
68472
|
+
this.instance = instance;
|
|
68473
|
+
this.name = "RemoteRouteUnsupportedError";
|
|
68474
|
+
}
|
|
68475
|
+
};
|
|
68476
|
+
RemoteRequestError = class RemoteRequestError extends Error {
|
|
68477
|
+
path;
|
|
68478
|
+
status;
|
|
68479
|
+
constructor(path, status, statusText) {
|
|
68480
|
+
super(`Remote request to ${path} failed: HTTP ${status}${statusText ? ` ${statusText}` : ""}`);
|
|
68481
|
+
this.path = path;
|
|
68482
|
+
this.status = status;
|
|
68483
|
+
this.name = "RemoteRequestError";
|
|
68484
|
+
}
|
|
68485
|
+
};
|
|
68169
68486
|
});
|
|
68170
68487
|
|
|
68171
68488
|
// src/mcp/operation-tools.ts
|
|
@@ -68630,14 +68947,14 @@ var init_operation_tools = __esm(() => {
|
|
|
68630
68947
|
|
|
68631
68948
|
// src/lib/feedback.ts
|
|
68632
68949
|
import { existsSync as existsSync22, mkdirSync as mkdirSync10 } from "fs";
|
|
68633
|
-
import { dirname as
|
|
68950
|
+
import { dirname as dirname7, join as join22 } from "path";
|
|
68634
68951
|
import { Database } from "bun:sqlite";
|
|
68635
68952
|
function getFeedbackDbPath() {
|
|
68636
68953
|
return join22(getDataDir(), "skills.db");
|
|
68637
68954
|
}
|
|
68638
68955
|
function getFeedbackDb() {
|
|
68639
68956
|
const dbPath = getFeedbackDbPath();
|
|
68640
|
-
const dir =
|
|
68957
|
+
const dir = dirname7(dbPath);
|
|
68641
68958
|
if (!existsSync22(dir))
|
|
68642
68959
|
mkdirSync10(dir, { recursive: true });
|
|
68643
68960
|
const db = new Database(dbPath);
|
|
@@ -68812,7 +69129,7 @@ var init_resource_meta_tools = __esm(() => {
|
|
|
68812
69129
|
});
|
|
68813
69130
|
|
|
68814
69131
|
// src/lib/scheduler.ts
|
|
68815
|
-
import { existsSync as existsSync23, readFileSync as
|
|
69132
|
+
import { existsSync as existsSync23, readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync11 } from "fs";
|
|
68816
69133
|
import { join as join23 } from "path";
|
|
68817
69134
|
function getSchedulesPath(targetDir = process.cwd()) {
|
|
68818
69135
|
return join23(targetDir, ".skills", "schedules.json");
|
|
@@ -68821,7 +69138,7 @@ function loadSchedules(targetDir = process.cwd()) {
|
|
|
68821
69138
|
const path = getSchedulesPath(targetDir);
|
|
68822
69139
|
if (existsSync23(path)) {
|
|
68823
69140
|
try {
|
|
68824
|
-
return JSON.parse(
|
|
69141
|
+
return JSON.parse(readFileSync18(path, "utf-8"));
|
|
68825
69142
|
} catch {}
|
|
68826
69143
|
}
|
|
68827
69144
|
return { version: 1, schedules: [] };
|
|
@@ -69131,12 +69448,12 @@ import { createHash as createHash3, createHmac as createHmac2 } from "crypto";
|
|
|
69131
69448
|
import {
|
|
69132
69449
|
existsSync as existsSync24,
|
|
69133
69450
|
mkdirSync as mkdirSync12,
|
|
69134
|
-
readFileSync as
|
|
69451
|
+
readFileSync as readFileSync19,
|
|
69135
69452
|
readdirSync as readdirSync13,
|
|
69136
69453
|
statSync as statSync13,
|
|
69137
69454
|
writeFileSync as writeFileSync13
|
|
69138
69455
|
} from "fs";
|
|
69139
|
-
import { dirname as
|
|
69456
|
+
import { dirname as dirname8, join as join24, normalize as normalize3, relative as relative3, sep as sep2 } from "path";
|
|
69140
69457
|
function resolveSkillsNativeStorageConfig(env3 = process.env) {
|
|
69141
69458
|
assertNoRetiredModeEnvVars(env3, {
|
|
69142
69459
|
app: SKILLS_ENV_NAMESPACE,
|
|
@@ -69168,7 +69485,7 @@ function getSkillsNativeStorageStatus(options = {}) {
|
|
|
69168
69485
|
const s3BucketEnv = readStorageEnv(env3, "s3Bucket");
|
|
69169
69486
|
const targetDir = options.targetDir ?? process.cwd();
|
|
69170
69487
|
return {
|
|
69171
|
-
package: "
|
|
69488
|
+
package: "skills",
|
|
69172
69489
|
tables: [...SKILLS_STORAGE_TABLES],
|
|
69173
69490
|
env: {
|
|
69174
69491
|
databaseUrl: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
|
|
@@ -69199,7 +69516,7 @@ function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
|
|
|
69199
69516
|
const files = [];
|
|
69200
69517
|
if (existsSync24(projectStateDir)) {
|
|
69201
69518
|
for (const filePath of walkFiles2(projectStateDir)) {
|
|
69202
|
-
const bytes =
|
|
69519
|
+
const bytes = readFileSync19(filePath);
|
|
69203
69520
|
const relativePath = toPosix(relative3(targetDir, filePath));
|
|
69204
69521
|
files.push({
|
|
69205
69522
|
path: relativePath,
|
|
@@ -69361,7 +69678,7 @@ function registerStorageTools(server) {
|
|
|
69361
69678
|
const snapshot = exportSkillsLocalSnapshot(targetDir, { includeFileContents: false });
|
|
69362
69679
|
const s3Plan = config2.s3Bucket ? planSkillsS3SnapshotUpload(snapshot, { prefix: config2.s3Prefix }) : [];
|
|
69363
69680
|
return mcpJson({
|
|
69364
|
-
package: "
|
|
69681
|
+
package: "skills",
|
|
69365
69682
|
noNetwork: true,
|
|
69366
69683
|
databaseConfigured: Boolean(config2.databaseUrl),
|
|
69367
69684
|
s3Configured: Boolean(config2.s3Bucket),
|
|
@@ -69802,9 +70119,9 @@ var init_mcp2 = __esm(() => {
|
|
|
69802
70119
|
});
|
|
69803
70120
|
|
|
69804
70121
|
// src/cli/commands/runtime-mcp.ts
|
|
69805
|
-
import { existsSync as existsSync25, mkdirSync as mkdirSync13, readFileSync as
|
|
70122
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync13, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
|
|
69806
70123
|
import { homedir as homedir8 } from "os";
|
|
69807
|
-
import { dirname as
|
|
70124
|
+
import { dirname as dirname9, join as join25 } from "path";
|
|
69808
70125
|
async function handleMcp(options) {
|
|
69809
70126
|
if (options.register) {
|
|
69810
70127
|
let agents;
|
|
@@ -69912,7 +70229,7 @@ function registerCodexMcp(command) {
|
|
|
69912
70229
|
const config2 = `[mcp_servers.${MCP_SERVER_NAME}]
|
|
69913
70230
|
command = ${JSON.stringify(command)}`;
|
|
69914
70231
|
try {
|
|
69915
|
-
const current = existsSync25(path) ?
|
|
70232
|
+
const current = existsSync25(path) ? readFileSync20(path, "utf-8") : "";
|
|
69916
70233
|
writeTextFile(path, upsertTomlSection(current, `[mcp_servers.${MCP_SERVER_NAME}]`, `command = ${JSON.stringify(command)}`));
|
|
69917
70234
|
return { agent: "codex", success: true, path, config: config2 };
|
|
69918
70235
|
} catch (err) {
|
|
@@ -69964,7 +70281,7 @@ function registerJsonMcpServer(agent, path, containerKey, server2) {
|
|
|
69964
70281
|
function readJsonObject2(path) {
|
|
69965
70282
|
if (!existsSync25(path))
|
|
69966
70283
|
return {};
|
|
69967
|
-
const raw =
|
|
70284
|
+
const raw = readFileSync20(path, "utf-8").trim();
|
|
69968
70285
|
if (!raw)
|
|
69969
70286
|
return {};
|
|
69970
70287
|
const parsed = JSON.parse(raw);
|
|
@@ -69977,7 +70294,7 @@ function writeJsonObject(path, data) {
|
|
|
69977
70294
|
`);
|
|
69978
70295
|
}
|
|
69979
70296
|
function writeTextFile(path, content) {
|
|
69980
|
-
mkdirSync13(
|
|
70297
|
+
mkdirSync13(dirname9(path), { recursive: true });
|
|
69981
70298
|
writeFileSync14(path, content.endsWith(`
|
|
69982
70299
|
`) ? content : `${content}
|
|
69983
70300
|
`);
|
|
@@ -70029,7 +70346,7 @@ __export(exports_runtime, {
|
|
|
70029
70346
|
registerRuntime: () => registerRuntime
|
|
70030
70347
|
});
|
|
70031
70348
|
import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync15 } from "fs";
|
|
70032
|
-
import { dirname as
|
|
70349
|
+
import { dirname as dirname10, join as join26 } from "path";
|
|
70033
70350
|
import { createInterface } from "readline";
|
|
70034
70351
|
function registerRuntime(parent) {
|
|
70035
70352
|
parent.command("run").argument("<skill>", "Skill name").argument("[args...]", "Arguments to pass to the skill").allowUnknownOption(true).passThroughOptions(true).option("--json", "Output result as JSON", false).option("--wait", "Poll remote runs until a terminal status", false).option("--poll-interval-ms <ms>", "Remote polling interval in milliseconds", "1000").option("--poll-timeout-ms <ms>", "Maximum time to wait for a remote run", "300000").description("Run a skill directly").action(async (name, args2, options) => handleRun(name, args2, options));
|
|
@@ -70514,7 +70831,7 @@ async function handleExportsDownload(runId, options) {
|
|
|
70514
70831
|
throw new Error(`download failed for artifact ${artifactId}: ${response.status}`);
|
|
70515
70832
|
const relativePath = safeArtifactRelativePath(typeof artifact.relativePath === "string" ? artifact.relativePath : artifact.fileName, String(artifact.fileName || artifactId));
|
|
70516
70833
|
const outputPath = join26(exportDir, relativePath);
|
|
70517
|
-
mkdirSync14(
|
|
70834
|
+
mkdirSync14(dirname10(outputPath), { recursive: true });
|
|
70518
70835
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
70519
70836
|
writeFileSync15(outputPath, bytes);
|
|
70520
70837
|
downloaded.push({ id: artifactId, path: outputPath, byteSize: bytes.byteLength });
|
|
@@ -70954,7 +71271,7 @@ function registerCreateSync(parent) {
|
|
|
70954
71271
|
console.log(`${source_default.cyan("project")}: ${pp}${existsSync26(pp) ? source_default.green(" (exists)") : source_default.dim(" (not found)")}`);
|
|
70955
71272
|
});
|
|
70956
71273
|
parent.command("create").argument("<name>", "Skill name (e.g. my-tool)").option("--category <category>", "Skill category", "Development Tools").option("--description <description>", "Short description of what the skill does").option("--tags <tags>", "Comma-separated tags (e.g. api,testing,automation)").option("--global", "Deprecated; custom skills are always global", false).option("--json", "Output result as JSON", false).description("Scaffold a new custom skill directory").action((name, options) => handleCreate(name, options));
|
|
70957
|
-
parent.command("sync").alias("render").argument("[names...]", "Skills to sync (default: every skill in this machine's corpus)").option("--for <agent>", `Target one agent (${SYNC_AGENTS.join(", ")}, or all)`, "all").option("--all", "Sync every corpus skill (the default)", false).option("--source <path>", "Canonical corpus source: a directory of skill folders, or a package root with skills/
|
|
71274
|
+
parent.command("sync").alias("render").argument("[names...]", "Skills to sync (default: every skill in this machine's corpus)").option("--for <agent>", `Target one agent (${SYNC_AGENTS.join(", ")}, or all)`, "all").option("--all", "Sync every corpus skill (the default)", false).option("--source <path>", "Canonical corpus source: a directory of skill folders, or a package root with skills/ (overrides $SKILLS_SOURCE)").option("--dry-run", "Show what would be written without touching any agent folder", false).option("--force", "Adopt an unmanaged skill that already has SKILL.md; other unmarked directories are never overwritten", false).option("--check", "Home drift census (missing-from-home / stray-in-home / diverged); exits non-zero on drift, writes nothing", false).option("--adopt", "Unmarked-home adoption mode: hash unmarked home skills against the corpus; exact matches are marked (dry-run by default)", false).option("--prune", "Prune mode: list (or with --apply, remove) marked home skill dirs that have no canonical corpus entry", false).option("--apply", "Write adoption markers / conflicts ledger, or perform prune removals", false).option("--json", "Output as JSON", false).description("Write corpus skills into each coding agent's global skills folder, per-tool adapted").action((names, options) => handleSync(names, options));
|
|
70958
71275
|
}
|
|
70959
71276
|
function handleCreate(name, options) {
|
|
70960
71277
|
const bare = name.trim();
|
|
@@ -71139,7 +71456,12 @@ Home drift census: ${census.entries.length} drift entr${census.entries.length ==
|
|
|
71139
71456
|
`));
|
|
71140
71457
|
for (const entry of census.entries) {
|
|
71141
71458
|
const kind = entry.kind === "missing-from-home" ? source_default.red("missing-from-home") : entry.kind === "stray-in-home" ? source_default.yellow("stray-in-home") : source_default.yellow("diverged");
|
|
71142
|
-
|
|
71459
|
+
let note = "";
|
|
71460
|
+
if (entry.homeStub === true)
|
|
71461
|
+
note = " (home is a pointer stub; canonical holds content)";
|
|
71462
|
+
else if (entry.canonicalStub === true)
|
|
71463
|
+
note = " (home holds content; canonical renders a pointer stub \u2014 sync refuses to replace it)";
|
|
71464
|
+
console.log(` ${kind} ${source_default.bold(entry.skill)} \u2192 ${entry.agent} ${source_default.dim(entry.path)}${note}`);
|
|
71143
71465
|
}
|
|
71144
71466
|
console.log(source_default.dim(`
|
|
71145
71467
|
${census.managed} managed, ${census.unmarked} unmarked (adoption candidates). Exit code is non-zero while drift exists.`));
|
|
@@ -71497,7 +71819,7 @@ var init_schedule = __esm(() => {
|
|
|
71497
71819
|
|
|
71498
71820
|
// src/lib/registry-sync.ts
|
|
71499
71821
|
import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync17 } from "fs";
|
|
71500
|
-
import { dirname as
|
|
71822
|
+
import { dirname as dirname11, relative as relative4 } from "path";
|
|
71501
71823
|
function createRegistrySyncArtifact(options = {}) {
|
|
71502
71824
|
const profile = options.profile ?? "all";
|
|
71503
71825
|
const includeDocs = options.includeDocs ?? true;
|
|
@@ -71554,7 +71876,7 @@ function createRegistrySyncArtifact(options = {}) {
|
|
|
71554
71876
|
};
|
|
71555
71877
|
}
|
|
71556
71878
|
function writeRegistrySyncArtifact(path, artifact) {
|
|
71557
|
-
mkdirSync16(
|
|
71879
|
+
mkdirSync16(dirname11(path), { recursive: true });
|
|
71558
71880
|
writeFileSync17(path, `${JSON.stringify(artifact, null, 2)}
|
|
71559
71881
|
`);
|
|
71560
71882
|
}
|
|
@@ -71573,9 +71895,32 @@ var init_registry_sync = __esm(() => {
|
|
|
71573
71895
|
init_skill_validation();
|
|
71574
71896
|
});
|
|
71575
71897
|
|
|
71576
|
-
// src/lib/
|
|
71898
|
+
// src/lib/revision.ts
|
|
71577
71899
|
import { createHash as createHash4 } from "crypto";
|
|
71578
|
-
|
|
71900
|
+
function revisionIdOf(content) {
|
|
71901
|
+
const canonical = JSON.stringify({
|
|
71902
|
+
slug: content.slug,
|
|
71903
|
+
displayName: content.displayName,
|
|
71904
|
+
description: content.description,
|
|
71905
|
+
category: content.category,
|
|
71906
|
+
tags: content.tags,
|
|
71907
|
+
source: content.source,
|
|
71908
|
+
kind: content.kind,
|
|
71909
|
+
version: content.version ?? null,
|
|
71910
|
+
skillMd: content.skillMd ?? null,
|
|
71911
|
+
bundleSha256: content.bundleSha256 ?? null,
|
|
71912
|
+
bundleByteSize: content.bundleByteSize ?? null
|
|
71913
|
+
});
|
|
71914
|
+
return createHash4("sha256").update(canonical).digest("hex");
|
|
71915
|
+
}
|
|
71916
|
+
var REVISION_ID_PATTERN;
|
|
71917
|
+
var init_revision = __esm(() => {
|
|
71918
|
+
REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
71919
|
+
});
|
|
71920
|
+
|
|
71921
|
+
// src/lib/skill-bundle.ts
|
|
71922
|
+
import { createHash as createHash5 } from "crypto";
|
|
71923
|
+
import { readFileSync as readFileSync21, readdirSync as readdirSync14, statSync as statSync14 } from "fs";
|
|
71579
71924
|
import { join as join28, relative as relative5 } from "path";
|
|
71580
71925
|
function isDotenvFile(lower) {
|
|
71581
71926
|
if (lower === ".env" || lower.startsWith(".env."))
|
|
@@ -71605,7 +71950,7 @@ function ownBytes(view) {
|
|
|
71605
71950
|
return out;
|
|
71606
71951
|
}
|
|
71607
71952
|
function sha256Hex(bytes) {
|
|
71608
|
-
return
|
|
71953
|
+
return createHash5("sha256").update(bytes).digest("hex");
|
|
71609
71954
|
}
|
|
71610
71955
|
function collectSkillBundleEntries(dir) {
|
|
71611
71956
|
const entries = [];
|
|
@@ -71621,6 +71966,8 @@ function walk(root, current, out) {
|
|
|
71621
71966
|
continue;
|
|
71622
71967
|
if (isRootLevel && ROOT_EXCLUDES.has(entry.name.toLowerCase()))
|
|
71623
71968
|
continue;
|
|
71969
|
+
if (isRootLevel && TOOL_SIDECAR_FILENAMES.has(entry.name.toLowerCase()))
|
|
71970
|
+
continue;
|
|
71624
71971
|
if (entry.name.startsWith("._"))
|
|
71625
71972
|
continue;
|
|
71626
71973
|
if (entry.isSymbolicLink())
|
|
@@ -71636,7 +71983,7 @@ function walk(root, current, out) {
|
|
|
71636
71983
|
const stats = statSync14(absolute);
|
|
71637
71984
|
out.push({
|
|
71638
71985
|
path: rel,
|
|
71639
|
-
bytes: ownBytes(
|
|
71986
|
+
bytes: ownBytes(readFileSync21(absolute)),
|
|
71640
71987
|
mode: stats.mode & 64 ? 493 : 420
|
|
71641
71988
|
});
|
|
71642
71989
|
}
|
|
@@ -71774,7 +72121,7 @@ function concat2(chunks) {
|
|
|
71774
72121
|
}
|
|
71775
72122
|
return merged;
|
|
71776
72123
|
}
|
|
71777
|
-
var BLOCK = 512, ANY_SEGMENT_EXCLUDES, ROOT_EXCLUDES, CREDENTIAL_FILENAMES, CREDENTIAL_EXTENSIONS, ENV_TEMPLATE_NAMES, NON_DOTENV_EXTENSIONS;
|
|
72124
|
+
var BLOCK = 512, ANY_SEGMENT_EXCLUDES, ROOT_EXCLUDES, TOOL_SIDECAR_FILENAMES, CREDENTIAL_FILENAMES, CREDENTIAL_EXTENSIONS, ENV_TEMPLATE_NAMES, NON_DOTENV_EXTENSIONS;
|
|
71778
72125
|
var init_skill_bundle = __esm(() => {
|
|
71779
72126
|
ANY_SEGMENT_EXCLUDES = new Set([
|
|
71780
72127
|
".git",
|
|
@@ -71787,6 +72134,7 @@ var init_skill_bundle = __esm(() => {
|
|
|
71787
72134
|
".docker"
|
|
71788
72135
|
]);
|
|
71789
72136
|
ROOT_EXCLUDES = new Set(["dist", "build", ".turbo"]);
|
|
72137
|
+
TOOL_SIDECAR_FILENAMES = new Set([".hasna-skills.json"]);
|
|
71790
72138
|
CREDENTIAL_FILENAMES = new Set([
|
|
71791
72139
|
".npmrc",
|
|
71792
72140
|
".pypirc",
|
|
@@ -71921,8 +72269,8 @@ var init_skill_bundles = __esm(() => {
|
|
|
71921
72269
|
});
|
|
71922
72270
|
|
|
71923
72271
|
// src/lib/pull.ts
|
|
71924
|
-
import { existsSync as existsSync27, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync2, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
|
|
71925
|
-
import { dirname as
|
|
72272
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync2, readFileSync as readFileSync22, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync18 } from "fs";
|
|
72273
|
+
import { dirname as dirname12, join as join29 } from "path";
|
|
71926
72274
|
async function pullSkills(options = {}) {
|
|
71927
72275
|
const client = options.client !== undefined ? options.client : createRemoteSkillsClient();
|
|
71928
72276
|
if (!client) {
|
|
@@ -71963,6 +72311,15 @@ async function pullOne(client, rawName, corpusOptions, verify) {
|
|
|
71963
72311
|
return { name: slug, success: false, error: `Failed to fetch '${slug}': ${error2.message}` };
|
|
71964
72312
|
}
|
|
71965
72313
|
if (bundleResponse && !bundleResponse.ok) {
|
|
72314
|
+
if (bundleResponse.status === 410) {
|
|
72315
|
+
return reconcileTombstone(slug, corpusOptions);
|
|
72316
|
+
}
|
|
72317
|
+
if (bundleResponse.status === 404) {
|
|
72318
|
+
const marker = readPullMarker(join29(getPortableSkillsRoot(corpusOptions), slug));
|
|
72319
|
+
if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
|
|
72320
|
+
return { name: slug, success: true, purged: true, removed: false };
|
|
72321
|
+
}
|
|
72322
|
+
}
|
|
71966
72323
|
return {
|
|
71967
72324
|
name: slug,
|
|
71968
72325
|
success: false,
|
|
@@ -71988,17 +72345,78 @@ async function pullOne(client, rawName, corpusOptions, verify) {
|
|
|
71988
72345
|
if (skillMd === null) {
|
|
71989
72346
|
return { name: slug, success: false, error: `Skill '${slug}' was not found on the configured Skills instance.` };
|
|
71990
72347
|
}
|
|
72348
|
+
if (!meta?.revisionId) {
|
|
72349
|
+
const marker = readPullMarker(join29(getPortableSkillsRoot(corpusOptions), slug));
|
|
72350
|
+
if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
|
|
72351
|
+
return { name: slug, success: true, purged: true, removed: false };
|
|
72352
|
+
}
|
|
72353
|
+
}
|
|
72354
|
+
let provenRevisionId;
|
|
72355
|
+
try {
|
|
72356
|
+
provenRevisionId = meta?.revisionId ? provenRevision({ ...meta, skillMd }, slug, {}) : undefined;
|
|
72357
|
+
} catch (error2) {
|
|
72358
|
+
if (error2 instanceof PullSkillError) {
|
|
72359
|
+
return { name: slug, success: false, error: error2.message };
|
|
72360
|
+
}
|
|
72361
|
+
throw error2;
|
|
72362
|
+
}
|
|
71991
72363
|
const written = writeCorpusSkill({ name: slug, skillMd, meta }, corpusOptions);
|
|
71992
|
-
writePullMarker(written.path, {
|
|
72364
|
+
writePullMarker(written.path, {
|
|
72365
|
+
skill: slug,
|
|
72366
|
+
version: written.manifest.version,
|
|
72367
|
+
...provenRevisionId ? { revisionId: provenRevisionId } : {}
|
|
72368
|
+
});
|
|
71993
72369
|
return {
|
|
71994
72370
|
name: slug,
|
|
71995
72371
|
success: true,
|
|
71996
72372
|
path: written.path,
|
|
71997
72373
|
kind: written.manifest.kind,
|
|
71998
72374
|
version: written.manifest.version,
|
|
71999
|
-
created: written.created
|
|
72375
|
+
created: written.created,
|
|
72376
|
+
...provenRevisionId ? { revisionId: provenRevisionId } : {}
|
|
72000
72377
|
};
|
|
72001
72378
|
}
|
|
72379
|
+
function provenRevision(meta, slug, bundle) {
|
|
72380
|
+
const declared = meta.revisionId;
|
|
72381
|
+
if (!declared)
|
|
72382
|
+
return "";
|
|
72383
|
+
const source = meta.publishedSource;
|
|
72384
|
+
if (typeof source !== "string" || source.length === 0) {
|
|
72385
|
+
throw new PullSkillError(`Revision proof failed for '${slug}': the instance declared revision '${declared.slice(0, 12)}\u2026' but did not serve the content fields needed to recompute it (publishedSource). Nothing was installed.`);
|
|
72386
|
+
}
|
|
72387
|
+
const recomputed = revisionIdOf({
|
|
72388
|
+
slug,
|
|
72389
|
+
displayName: meta.displayName ?? "",
|
|
72390
|
+
description: meta.description ?? "",
|
|
72391
|
+
category: meta.category ?? "",
|
|
72392
|
+
tags: meta.tags ?? [],
|
|
72393
|
+
source,
|
|
72394
|
+
kind: meta.kind ?? "instruction",
|
|
72395
|
+
...meta.version ? { version: meta.version } : {},
|
|
72396
|
+
...typeof meta.skillMd === "string" && meta.skillMd.length > 0 ? { skillMd: meta.skillMd } : {},
|
|
72397
|
+
...bundle.sha256 ? { bundleSha256: bundle.sha256 } : {},
|
|
72398
|
+
...bundle.byteSize !== undefined && bundle.byteSize !== null ? { bundleByteSize: bundle.byteSize } : {}
|
|
72399
|
+
});
|
|
72400
|
+
if (recomputed !== declared) {
|
|
72401
|
+
throw new PullSkillError(`Revision proof failed for '${slug}': the instance declared revision '${declared.slice(0, 12)}\u2026' but the served content recomputes to '${recomputed.slice(0, 12)}\u2026'. The declared revision does not identify the content that was received. Nothing was installed.`);
|
|
72402
|
+
}
|
|
72403
|
+
return declared;
|
|
72404
|
+
}
|
|
72405
|
+
function reconcileTombstone(slug, corpusOptions) {
|
|
72406
|
+
const target = join29(getPortableSkillsRoot(corpusOptions), slug);
|
|
72407
|
+
if (!existsSync27(join29(target, PULL_MARKER_FILE))) {
|
|
72408
|
+
return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
|
|
72409
|
+
}
|
|
72410
|
+
rmSync6(target, { recursive: true, force: true });
|
|
72411
|
+
return { name: slug, success: true, tombstoned: true, removed: true };
|
|
72412
|
+
}
|
|
72413
|
+
function readPullMarker(dir) {
|
|
72414
|
+
try {
|
|
72415
|
+
return JSON.parse(readFileSync22(join29(dir, PULL_MARKER_FILE), "utf-8"));
|
|
72416
|
+
} catch {
|
|
72417
|
+
return null;
|
|
72418
|
+
}
|
|
72419
|
+
}
|
|
72002
72420
|
function installVerifiedBundle(slug, response, meta, corpusOptions, verify) {
|
|
72003
72421
|
return response.arrayBuffer().then((buffer) => {
|
|
72004
72422
|
const verified = verifyBundleResponseBytes(buffer, response, verify);
|
|
@@ -72010,11 +72428,17 @@ function installVerifiedBundle(slug, response, meta, corpusOptions, verify) {
|
|
|
72010
72428
|
}
|
|
72011
72429
|
const version2 = str(meta?.version) ?? versionFromEntries(entries) ?? "unknown";
|
|
72012
72430
|
const sourceCommit = sourceCommitFromEntries(entries);
|
|
72431
|
+
const declaredRevision = verified.revisionId ?? meta?.revisionId;
|
|
72432
|
+
if (declaredRevision && meta?.revisionId && verified.revisionId && declaredRevision !== meta.revisionId) {
|
|
72433
|
+
throw new PullSkillError(`Revision proof failed for '${slug}': the bundle header declares '${verified.revisionId.slice(0, 12)}\u2026' but the metadata declares '${meta.revisionId.slice(0, 12)}\u2026'. The instance is inconsistent. Nothing was installed.`);
|
|
72434
|
+
}
|
|
72435
|
+
const provenRevisionId = declaredRevision ? provenRevision(meta ?? {}, slug, { sha256: verified.contentHash, byteSize: verified.bytes.byteLength }) : undefined;
|
|
72013
72436
|
const installed = installBundleAtomically(slug, entries, corpusOptions, {
|
|
72014
72437
|
version: version2,
|
|
72015
72438
|
contentHash: verified.contentHash,
|
|
72016
72439
|
...sourceCommit ? { sourceCommit } : {},
|
|
72017
|
-
...verified.signature ? { signature: verified.signature } : {}
|
|
72440
|
+
...verified.signature ? { signature: verified.signature } : {},
|
|
72441
|
+
...provenRevisionId ? { revisionId: provenRevisionId } : {}
|
|
72018
72442
|
});
|
|
72019
72443
|
return {
|
|
72020
72444
|
name: slug,
|
|
@@ -72024,6 +72448,7 @@ function installVerifiedBundle(slug, response, meta, corpusOptions, verify) {
|
|
|
72024
72448
|
version: version2,
|
|
72025
72449
|
contentHash: verified.contentHash,
|
|
72026
72450
|
...sourceCommit ? { sourceCommit } : {},
|
|
72451
|
+
...provenRevisionId ? { revisionId: provenRevisionId } : {},
|
|
72027
72452
|
created: installed.created
|
|
72028
72453
|
};
|
|
72029
72454
|
});
|
|
@@ -72074,11 +72499,20 @@ function sourceCommitFromEntries(entries) {
|
|
|
72074
72499
|
function verifyBundleResponseBytes(buffer, response, verify = {}) {
|
|
72075
72500
|
const serverHash = response.headers.get(BUNDLE_DIGEST_HEADER);
|
|
72076
72501
|
const signature = response.headers.get(BUNDLE_SIGNATURE_HEADER);
|
|
72502
|
+
const revisionId = response.headers.get(BUNDLE_REVISION_ID_HEADER);
|
|
72503
|
+
const revisionNumberRaw = response.headers.get(BUNDLE_REVISION_NUMBER_HEADER);
|
|
72077
72504
|
const bytes = new Uint8Array(buffer.slice(0));
|
|
72078
72505
|
const contentHash = sha256Hex(bytes);
|
|
72079
72506
|
if (serverHash && serverHash.toLowerCase() !== contentHash) {
|
|
72080
72507
|
throw new PullSkillError(`Bundle digest mismatch: the instance declared ${serverHash} but the received bundle hashes to ${contentHash}.`, ["The bundle was tampered with or truncated in transit. Nothing was installed."]);
|
|
72081
72508
|
}
|
|
72509
|
+
if (revisionId && !REVISION_ID_PATTERN.test(revisionId)) {
|
|
72510
|
+
throw new PullSkillError(`Malformed revision id: the instance declared '${revisionId}', which is not a 64-character lowercase hex sha-256.`, ["The revision headers were tampered with or the instance is broken. Nothing was installed."]);
|
|
72511
|
+
}
|
|
72512
|
+
const revisionNumber = revisionNumberRaw === null ? undefined : Number(revisionNumberRaw);
|
|
72513
|
+
if (revisionNumberRaw !== null && (!Number.isInteger(revisionNumber) || revisionNumber < 0)) {
|
|
72514
|
+
throw new PullSkillError(`Malformed revision number: the instance declared '${revisionNumberRaw}', which is not a non-negative integer.`, ["The revision headers were tampered with or the instance is broken. Nothing was installed."]);
|
|
72515
|
+
}
|
|
72082
72516
|
if (signature) {
|
|
72083
72517
|
const key = verify.signingKey;
|
|
72084
72518
|
if (!key) {
|
|
@@ -72091,7 +72525,9 @@ function verifyBundleResponseBytes(buffer, response, verify = {}) {
|
|
|
72091
72525
|
bytes,
|
|
72092
72526
|
contentHash,
|
|
72093
72527
|
...serverHash ? { serverHash } : {},
|
|
72094
|
-
...signature ? { signature } : {}
|
|
72528
|
+
...signature ? { signature } : {},
|
|
72529
|
+
...revisionId ? { revisionId } : {},
|
|
72530
|
+
...revisionNumberRaw === null || revisionNumber === undefined ? {} : { revisionNumber }
|
|
72095
72531
|
};
|
|
72096
72532
|
}
|
|
72097
72533
|
function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
@@ -72105,7 +72541,7 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
|
72105
72541
|
try {
|
|
72106
72542
|
for (const entry of entries) {
|
|
72107
72543
|
const destination = join29(staging, entry.path);
|
|
72108
|
-
mkdirSync17(
|
|
72544
|
+
mkdirSync17(dirname12(destination), { recursive: true });
|
|
72109
72545
|
writeFileSync18(destination, entry.bytes, { mode: entry.mode });
|
|
72110
72546
|
}
|
|
72111
72547
|
writePullMarker(staging, {
|
|
@@ -72113,7 +72549,8 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
|
72113
72549
|
...marker.version ? { version: marker.version } : {},
|
|
72114
72550
|
...marker.contentHash ? { contentHash: marker.contentHash } : {},
|
|
72115
72551
|
...marker.sourceCommit ? { sourceCommit: marker.sourceCommit } : {},
|
|
72116
|
-
...marker.signature ? { signature: marker.signature } : {}
|
|
72552
|
+
...marker.signature ? { signature: marker.signature } : {},
|
|
72553
|
+
...marker.revisionId ? { revisionId: marker.revisionId } : {}
|
|
72117
72554
|
});
|
|
72118
72555
|
if (existsSync27(target)) {
|
|
72119
72556
|
backup = mkdtempSync2(join29(root, `.pull-backup-${name}-`));
|
|
@@ -72138,11 +72575,12 @@ function writePullMarker(dir, record3) {
|
|
|
72138
72575
|
const marker = {
|
|
72139
72576
|
managedBy: "@hasna/skills",
|
|
72140
72577
|
skill: record3.skill,
|
|
72141
|
-
source: "pull",
|
|
72578
|
+
source: record3.source ?? "pull",
|
|
72142
72579
|
...record3.version ? { version: record3.version } : {},
|
|
72143
72580
|
...record3.contentHash ? { contentHash: record3.contentHash } : {},
|
|
72144
72581
|
...record3.sourceCommit ? { sourceCommit: record3.sourceCommit } : {},
|
|
72145
72582
|
...record3.signature ? { signature: record3.signature } : {},
|
|
72583
|
+
...record3.revisionId ? { revisionId: record3.revisionId } : {},
|
|
72146
72584
|
syncedAt: new Date().toISOString()
|
|
72147
72585
|
};
|
|
72148
72586
|
writeFileSync18(join29(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
|
|
@@ -72166,20 +72604,14 @@ async function safeMeta(client, slug) {
|
|
|
72166
72604
|
...str(record3.category) ? { category: str(record3.category) } : {},
|
|
72167
72605
|
...tags && tags.length ? { tags } : {},
|
|
72168
72606
|
...str(record3.version) ? { version: str(record3.version) } : {},
|
|
72169
|
-
...kind ? { kind } : {}
|
|
72607
|
+
...kind ? { kind } : {},
|
|
72608
|
+
...REVISION_ID_PATTERN.test(str(record3.revisionId) ?? "") ? { revisionId: str(record3.revisionId) } : {},
|
|
72609
|
+
...typeof record3.skillMd === "string" && record3.skillMd.length > 0 ? { skillMd: record3.skillMd } : {},
|
|
72610
|
+
...str(record3.publishedSource) ? { publishedSource: str(record3.publishedSource) } : {}
|
|
72170
72611
|
};
|
|
72171
72612
|
}
|
|
72172
72613
|
function pickCorpusOptions(options) {
|
|
72173
|
-
return { rootDir:
|
|
72174
|
-
}
|
|
72175
|
-
function resolvePullCorpusRoot(options) {
|
|
72176
|
-
if (options.rootDir)
|
|
72177
|
-
return options.rootDir;
|
|
72178
|
-
const appDir = options.homeDir ? join29(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
72179
|
-
const cache3 = join29(appDir, SKILLS_CACHE_DIRNAME2);
|
|
72180
|
-
if (existsSync27(join29(cache3, LAYOUT_MIGRATION_RECORD2)) && existsSync27(cache3))
|
|
72181
|
-
return cache3;
|
|
72182
|
-
return getPortableSkillsRoot(options);
|
|
72614
|
+
return { rootDir: resolveCorpusRoot(options) };
|
|
72183
72615
|
}
|
|
72184
72616
|
function extractSlug(entry) {
|
|
72185
72617
|
if (!entry || typeof entry !== "object")
|
|
@@ -72193,13 +72625,15 @@ function dedupe(values2) {
|
|
|
72193
72625
|
function str(value) {
|
|
72194
72626
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
72195
72627
|
}
|
|
72196
|
-
var BUNDLE_DIGEST_HEADER = "X-Skill-Bundle-Sha256", BUNDLE_SIGNATURE_HEADER = "X-Skill-Bundle-Signature",
|
|
72628
|
+
var BUNDLE_DIGEST_HEADER = "X-Skill-Bundle-Sha256", BUNDLE_SIGNATURE_HEADER = "X-Skill-Bundle-Signature", BUNDLE_REVISION_ID_HEADER = "X-Skill-Revision-Id", BUNDLE_REVISION_NUMBER_HEADER = "X-Skill-Revision-Number", PULL_MARKER_FILE = ".hasna-skills.json", PullSkillError;
|
|
72197
72629
|
var init_pull = __esm(() => {
|
|
72198
72630
|
init_remote_client();
|
|
72199
72631
|
init_portable_skills();
|
|
72200
|
-
|
|
72632
|
+
init_home_migration();
|
|
72633
|
+
init_revision();
|
|
72201
72634
|
init_skill_bundle();
|
|
72202
72635
|
init_skill_bundles();
|
|
72636
|
+
init_revision();
|
|
72203
72637
|
PullSkillError = class PullSkillError extends Error {
|
|
72204
72638
|
detail;
|
|
72205
72639
|
constructor(message, detail) {
|
|
@@ -72320,10 +72754,10 @@ __export(exports_publish, {
|
|
|
72320
72754
|
pushSkill: () => pushSkill,
|
|
72321
72755
|
PushSkillError: () => PushSkillError
|
|
72322
72756
|
});
|
|
72323
|
-
import { existsSync as existsSync28, readFileSync as
|
|
72757
|
+
import { existsSync as existsSync28, readFileSync as readFileSync23 } from "fs";
|
|
72324
72758
|
import { join as join30 } from "path";
|
|
72325
72759
|
function registerPublish(parent) {
|
|
72326
|
-
parent.command("push").argument("<name>", "Name of a skill in ~/.hasna/skills/installed").option("--version <version>", "Override the version recorded on the instance").option("--dry-run", "Pack and validate without uploading", false).option("--json", "Output result as JSON", false).description("Publish a local skill to the configured skills instance").action(async (name, options) => {
|
|
72760
|
+
parent.command("push").argument("<name>", "Name of a skill in the local corpus (~/.hasna/skills/installed or the migrated ~/.hasna/skills/skills)").option("--version <version>", "Override the version recorded on the instance").option("--dry-run", "Pack and validate without uploading", false).option("--json", "Output result as JSON", false).description("Publish a local skill to the configured skills instance").action(async (name, options) => {
|
|
72327
72761
|
try {
|
|
72328
72762
|
const result2 = await pushSkill(name, {
|
|
72329
72763
|
dryRun: options.dryRun,
|
|
@@ -72363,7 +72797,7 @@ async function pushSkill(name, options = {}) {
|
|
|
72363
72797
|
const manifest = readPortableSkillManifest(skill.path, skill.name);
|
|
72364
72798
|
const packed = packSkillBundle(skill.path, { maxUnpackedBytes: MAX_UNPACKED_BYTES });
|
|
72365
72799
|
const skillMdPath = join30(skill.path, "SKILL.md");
|
|
72366
|
-
const skillMd = existsSync28(skillMdPath) ?
|
|
72800
|
+
const skillMd = existsSync28(skillMdPath) ? readFileSync23(skillMdPath, "utf-8") : undefined;
|
|
72367
72801
|
const base2 = {
|
|
72368
72802
|
slug: skill.name,
|
|
72369
72803
|
path: skill.path,
|
|
@@ -72381,6 +72815,8 @@ async function pushSkill(name, options = {}) {
|
|
|
72381
72815
|
if (!client) {
|
|
72382
72816
|
throw new PushSkillError("No API key configured, so there is nowhere to publish to.", ["Run `skills login`, or set SKILLS_API_KEY and SKILLS_API_URL for this instance."]);
|
|
72383
72817
|
}
|
|
72818
|
+
const current = await client.getSkill(skill.name);
|
|
72819
|
+
const ifMatch = current && typeof current.revisionId === "string" && current.revisionId ? current.revisionId : undefined;
|
|
72384
72820
|
const response = await client.publishSkill({
|
|
72385
72821
|
slug: skill.name,
|
|
72386
72822
|
displayName: manifest.displayName ?? skill.displayName,
|
|
@@ -72393,10 +72829,17 @@ async function pushSkill(name, options = {}) {
|
|
|
72393
72829
|
bundleSha256: packed.sha256,
|
|
72394
72830
|
contentHash: packed.sha256,
|
|
72395
72831
|
...skillMd ? { skillMd } : {}
|
|
72396
|
-
}, packed.bytes);
|
|
72832
|
+
}, packed.bytes, ifMatch);
|
|
72397
72833
|
const payload = await readBody(response);
|
|
72398
72834
|
if (!response.ok) {
|
|
72399
|
-
|
|
72835
|
+
const code = typeof payload === "object" && payload && "code" in payload ? String(payload.code) : undefined;
|
|
72836
|
+
if (response.status === 409 || code === "REVISION_CONFLICT") {
|
|
72837
|
+
throw new PushSkillError(`Publishing '${skill.name}' failed: the instance serves a NEWER revision of this skill. ` + "Your push would silently overwrite it, so it was refused.", [
|
|
72838
|
+
`code: REVISION_CONFLICT`,
|
|
72839
|
+
"Reconcile first: pull the current revision (skills pull <name>), merge your changes, then push again."
|
|
72840
|
+
]);
|
|
72841
|
+
}
|
|
72842
|
+
throw new PushSkillError(`Publishing '${skill.name}' failed: ${response.status} ${describeError(payload)}`, code ? [`code: ${code}`] : undefined);
|
|
72400
72843
|
}
|
|
72401
72844
|
return { ...base2, published: true, status: response.status, response: payload };
|
|
72402
72845
|
}
|
|
@@ -72680,7 +73123,7 @@ function printLoginSuccess(loginResult, json) {
|
|
|
72680
73123
|
\u2713 Signed in as ${loginResult.user.email}`));
|
|
72681
73124
|
console.log(source_default.dim(` Organization: ${loginResult.organization.name}`));
|
|
72682
73125
|
if (loginResult.firstLogin) {
|
|
72683
|
-
console.log(source_default.dim(` API key saved to
|
|
73126
|
+
console.log(source_default.dim(` API key saved to ${getAuthFilePath()}`));
|
|
72684
73127
|
}
|
|
72685
73128
|
}
|
|
72686
73129
|
async function doLogin(email2, code, json) {
|
|
@@ -73055,7 +73498,7 @@ function registerStorage(parent) {
|
|
|
73055
73498
|
const snapshot = exportSkillsLocalSnapshot(process.cwd(), { includeFileContents: false });
|
|
73056
73499
|
const s3Plan = config2.s3Bucket ? planSkillsS3SnapshotUpload(snapshot, { prefix: config2.s3Prefix }) : [];
|
|
73057
73500
|
const plan = {
|
|
73058
|
-
package: "
|
|
73501
|
+
package: "skills",
|
|
73059
73502
|
noNetwork: true,
|
|
73060
73503
|
databaseConfigured: Boolean(config2.databaseUrl),
|
|
73061
73504
|
s3Configured: Boolean(config2.s3Bucket),
|
|
@@ -73124,6 +73567,428 @@ var init_storage = __esm(() => {
|
|
|
73124
73567
|
init_home_migration();
|
|
73125
73568
|
});
|
|
73126
73569
|
|
|
73570
|
+
// src/lib/registry-reconcile.ts
|
|
73571
|
+
import { existsSync as existsSync29, readFileSync as readFileSync24, statSync as statSync15, writeFileSync as writeFileSync19 } from "fs";
|
|
73572
|
+
import { join as join31 } from "path";
|
|
73573
|
+
function isDirectory2(path) {
|
|
73574
|
+
try {
|
|
73575
|
+
return statSync15(path).isDirectory();
|
|
73576
|
+
} catch {
|
|
73577
|
+
return false;
|
|
73578
|
+
}
|
|
73579
|
+
}
|
|
73580
|
+
function migrationNeeded(options) {
|
|
73581
|
+
if (options.rootDir)
|
|
73582
|
+
return false;
|
|
73583
|
+
const appDir = options.homeDir ? join31(options.homeDir, ".hasna", "skills") : getDataDir();
|
|
73584
|
+
return !(isOwnerLayoutMigrated(appDir) && isDirectory2(join31(appDir, SKILLS_CACHE_DIRNAME)));
|
|
73585
|
+
}
|
|
73586
|
+
function readBaseline(skillDir) {
|
|
73587
|
+
const markerPath = join31(skillDir, PULL_MARKER_FILE);
|
|
73588
|
+
if (!existsSync29(markerPath))
|
|
73589
|
+
return;
|
|
73590
|
+
try {
|
|
73591
|
+
const marker = JSON.parse(readFileSync24(markerPath, "utf-8"));
|
|
73592
|
+
return {
|
|
73593
|
+
...typeof marker.contentHash === "string" && marker.contentHash ? { contentHash: marker.contentHash } : {},
|
|
73594
|
+
...typeof marker.version === "string" && marker.version ? { version: marker.version } : {}
|
|
73595
|
+
};
|
|
73596
|
+
} catch {
|
|
73597
|
+
return;
|
|
73598
|
+
}
|
|
73599
|
+
}
|
|
73600
|
+
function readCursor(root) {
|
|
73601
|
+
const path = join31(root, SYNC_CURSOR_FILE);
|
|
73602
|
+
if (!existsSync29(path))
|
|
73603
|
+
return { runCount: 0 };
|
|
73604
|
+
try {
|
|
73605
|
+
const cursor = JSON.parse(readFileSync24(path, "utf-8"));
|
|
73606
|
+
return { runCount: typeof cursor.runCount === "number" ? cursor.runCount : 0 };
|
|
73607
|
+
} catch {
|
|
73608
|
+
return { runCount: 0 };
|
|
73609
|
+
}
|
|
73610
|
+
}
|
|
73611
|
+
function resolveCorpusRootReadOnly(options) {
|
|
73612
|
+
if (options.rootDir)
|
|
73613
|
+
return { root: options.rootDir, migrationPending: false };
|
|
73614
|
+
const appDir = options.homeDir ? join31(options.homeDir, ".hasna", "skills") : getDataDirReadOnly();
|
|
73615
|
+
const cache3 = join31(appDir, SKILLS_CACHE_DIRNAME);
|
|
73616
|
+
if (isOwnerLayoutMigrated(appDir) && isDirectory2(cache3)) {
|
|
73617
|
+
return { root: cache3, migrationPending: false };
|
|
73618
|
+
}
|
|
73619
|
+
return { root: join31(appDir, INSTALLED_SKILLS_DIRNAME), migrationPending: true };
|
|
73620
|
+
}
|
|
73621
|
+
function remoteRowToSkill(record3) {
|
|
73622
|
+
const slug = typeof record3.slug === "string" ? record3.slug : typeof record3.name === "string" ? record3.name : undefined;
|
|
73623
|
+
if (!slug)
|
|
73624
|
+
return;
|
|
73625
|
+
return {
|
|
73626
|
+
slug,
|
|
73627
|
+
version: typeof record3.version === "string" ? record3.version : undefined,
|
|
73628
|
+
sha256: typeof record3.bundleSha256 === "string" && record3.bundleSha256 ? record3.bundleSha256 : undefined
|
|
73629
|
+
};
|
|
73630
|
+
}
|
|
73631
|
+
function recheckLocalSide(plannedLocal, localDir, ops = {
|
|
73632
|
+
pack: (dir) => packSkillBundle(dir).sha256,
|
|
73633
|
+
exists: existsSync29
|
|
73634
|
+
}) {
|
|
73635
|
+
let localNow;
|
|
73636
|
+
try {
|
|
73637
|
+
localNow = ops.pack(localDir);
|
|
73638
|
+
} catch {
|
|
73639
|
+
localNow = undefined;
|
|
73640
|
+
}
|
|
73641
|
+
const dirExists = ops.exists(localDir);
|
|
73642
|
+
return plannedLocal !== undefined ? localNow !== plannedLocal : localNow !== undefined || dirExists;
|
|
73643
|
+
}
|
|
73644
|
+
function classifySkill(local, remote, baseline) {
|
|
73645
|
+
if (!remote)
|
|
73646
|
+
return { state: "local-only" };
|
|
73647
|
+
if (!local)
|
|
73648
|
+
return { state: "remote-only" };
|
|
73649
|
+
const baselineHash = baseline?.contentHash;
|
|
73650
|
+
const baselineVersion = baseline?.version;
|
|
73651
|
+
if (remote.sha256) {
|
|
73652
|
+
if (remote.sha256 === local.sha256) {
|
|
73653
|
+
const effectiveLocalVersion = baselineVersion ?? local.version ?? undefined;
|
|
73654
|
+
const remoteVersion = remote.version ?? undefined;
|
|
73655
|
+
if (effectiveLocalVersion !== undefined && remoteVersion !== undefined && effectiveLocalVersion !== remoteVersion) {
|
|
73656
|
+
return { state: "conflict", reason: "version divergence with identical digest" };
|
|
73657
|
+
}
|
|
73658
|
+
return { state: "in-sync" };
|
|
73659
|
+
}
|
|
73660
|
+
if (!baselineHash) {
|
|
73661
|
+
return { state: "conflict", reason: "no baseline marker: cannot prove which side changed" };
|
|
73662
|
+
}
|
|
73663
|
+
if (baselineHash === local.sha256)
|
|
73664
|
+
return { state: "changed-remotely" };
|
|
73665
|
+
if (baselineHash === remote.sha256)
|
|
73666
|
+
return { state: "changed-locally" };
|
|
73667
|
+
return { state: "conflict", reason: "both sides changed since the baseline marker" };
|
|
73668
|
+
}
|
|
73669
|
+
if (baselineHash && baselineHash !== local.sha256) {
|
|
73670
|
+
return { state: "changed-locally", reason: "local digest moved since the baseline marker" };
|
|
73671
|
+
}
|
|
73672
|
+
if (local.version !== undefined && remote.version !== undefined && local.version !== remote.version) {
|
|
73673
|
+
return { state: "changed-locally", reason: "version diverged from the bundled row" };
|
|
73674
|
+
}
|
|
73675
|
+
return { state: "in-sync", reason: "no remote digest; no version or baseline evidence of local divergence" };
|
|
73676
|
+
}
|
|
73677
|
+
function isDigestless(remote) {
|
|
73678
|
+
return remote !== undefined && !remote.sha256;
|
|
73679
|
+
}
|
|
73680
|
+
function resolveAction(state, direction, conflict) {
|
|
73681
|
+
switch (state) {
|
|
73682
|
+
case "local-only":
|
|
73683
|
+
case "changed-locally":
|
|
73684
|
+
return direction === "push" || direction === "all" ? { action: "push" } : { action: "skip", reason: "push not requested" };
|
|
73685
|
+
case "remote-only":
|
|
73686
|
+
case "changed-remotely":
|
|
73687
|
+
return direction === "pull" || direction === "all" ? { action: "pull" } : { action: "skip", reason: "pull not requested" };
|
|
73688
|
+
case "conflict":
|
|
73689
|
+
if (conflict === "local" && (direction === "push" || direction === "all"))
|
|
73690
|
+
return { action: "push" };
|
|
73691
|
+
if (conflict === "remote" && (direction === "pull" || direction === "all"))
|
|
73692
|
+
return { action: "pull" };
|
|
73693
|
+
return { action: "skip", reason: `conflict policy '${conflict}' does not resolve in this direction` };
|
|
73694
|
+
case "in-sync":
|
|
73695
|
+
return { action: "none" };
|
|
73696
|
+
}
|
|
73697
|
+
}
|
|
73698
|
+
async function currentRemoteDigest(client, slug) {
|
|
73699
|
+
const { status, body } = await client.getSkillStatus(slug);
|
|
73700
|
+
if (status === 404)
|
|
73701
|
+
return;
|
|
73702
|
+
if (status !== 200) {
|
|
73703
|
+
throw new ReconcileRegistryError(`Registry re-check for '${slug}' failed: HTTP ${status}.`);
|
|
73704
|
+
}
|
|
73705
|
+
if (!body || typeof body !== "object")
|
|
73706
|
+
return;
|
|
73707
|
+
const digest = body.bundleSha256;
|
|
73708
|
+
return typeof digest === "string" && digest ? digest : undefined;
|
|
73709
|
+
}
|
|
73710
|
+
async function reconcileRegistry(options = {}) {
|
|
73711
|
+
const conflict = options.conflict ?? "skip";
|
|
73712
|
+
if (!CONFLICT_POLICIES.includes(conflict)) {
|
|
73713
|
+
throw new ReconcileRegistryError(`Unknown conflict policy '${String(conflict)}'. Use one of: ${CONFLICT_POLICIES.join(", ")}.`);
|
|
73714
|
+
}
|
|
73715
|
+
const direction = options.pull && !options.push && !options.all ? "pull" : options.push && !options.pull && !options.all ? "push" : "all";
|
|
73716
|
+
const dryRun = options.dryRun ?? false;
|
|
73717
|
+
const client = options.client !== undefined ? options.client : dryRun ? createRemoteSkillsClientReadOnly() : createRemoteSkillsClient();
|
|
73718
|
+
if (!client) {
|
|
73719
|
+
throw new ReconcileRegistryError("No API key configured, so there is nowhere to sync to.", ["Run `skills login`, or set SKILLS_API_KEY and SKILLS_API_URL for this instance."]);
|
|
73720
|
+
}
|
|
73721
|
+
const { root, migrationPending } = dryRun ? resolveCorpusRootReadOnly(options) : { root: resolveCorpusRoot(options), migrationPending: migrationNeeded(options) };
|
|
73722
|
+
const localSkills = listPortableSkillMetas({ rootDir: root });
|
|
73723
|
+
const locals = new Map;
|
|
73724
|
+
for (const meta of localSkills) {
|
|
73725
|
+
try {
|
|
73726
|
+
const path = getPortableSkillPath(meta.name, { rootDir: root });
|
|
73727
|
+
const packed = packSkillBundle(path);
|
|
73728
|
+
locals.set(meta.name, { slug: meta.name, version: meta.version, sha256: packed.sha256 });
|
|
73729
|
+
} catch {
|
|
73730
|
+
locals.set(meta.name, { slug: meta.name, version: meta.version, sha256: "" });
|
|
73731
|
+
}
|
|
73732
|
+
}
|
|
73733
|
+
const remoteRowsPayload = await client.listSkills();
|
|
73734
|
+
if (!Array.isArray(remoteRowsPayload)) {
|
|
73735
|
+
const shape = remoteRowsPayload && typeof remoteRowsPayload === "object" ? `object with keys [${Object.keys(remoteRowsPayload).join(", ")}]` : typeof remoteRowsPayload;
|
|
73736
|
+
throw new ReconcileRegistryError(`Registry listing failed: expected an array of skills, got ${shape}.`, ["Check SKILLS_API_URL and the stored credential; a failed listing must not be read as an empty registry."]);
|
|
73737
|
+
}
|
|
73738
|
+
const remotes = new Map;
|
|
73739
|
+
for (const row of remoteRowsPayload) {
|
|
73740
|
+
if (!row || typeof row !== "object")
|
|
73741
|
+
continue;
|
|
73742
|
+
const skill = remoteRowToSkill(row);
|
|
73743
|
+
if (skill)
|
|
73744
|
+
remotes.set(skill.slug, skill);
|
|
73745
|
+
}
|
|
73746
|
+
const allSlugs = [...new Set([...locals.keys(), ...remotes.keys()])].sort();
|
|
73747
|
+
const skills = [];
|
|
73748
|
+
for (const slug of allSlugs) {
|
|
73749
|
+
const local = locals.get(slug);
|
|
73750
|
+
const remote = remotes.get(slug);
|
|
73751
|
+
const baseline = local ? readBaseline(join31(root, slug)) : undefined;
|
|
73752
|
+
const { state, reason } = classifySkill(local, remote, baseline);
|
|
73753
|
+
let { action, reason: actionReason } = resolveAction(state, direction, conflict);
|
|
73754
|
+
if (state === "remote-only" && isDigestless(remote)) {
|
|
73755
|
+
action = "skip";
|
|
73756
|
+
actionReason = "no bundle digest; verified pulls only (use `skills pull --all` for metadata-only)";
|
|
73757
|
+
}
|
|
73758
|
+
skills.push({
|
|
73759
|
+
slug,
|
|
73760
|
+
state,
|
|
73761
|
+
action,
|
|
73762
|
+
...local?.version ? { localVersion: local.version } : {},
|
|
73763
|
+
...remote?.version ? { remoteVersion: remote.version } : {},
|
|
73764
|
+
...local?.sha256 ? { localSha256: local.sha256 } : {},
|
|
73765
|
+
...remote?.sha256 ? { remoteSha256: remote.sha256 } : {},
|
|
73766
|
+
...reason ?? actionReason ? { reason: reason ?? actionReason } : {}
|
|
73767
|
+
});
|
|
73768
|
+
}
|
|
73769
|
+
const summary = {
|
|
73770
|
+
local: locals.size,
|
|
73771
|
+
remote: remotes.size,
|
|
73772
|
+
inSync: 0,
|
|
73773
|
+
pushed: 0,
|
|
73774
|
+
pulled: 0,
|
|
73775
|
+
conflicts: 0,
|
|
73776
|
+
skipped: 0,
|
|
73777
|
+
errors: 0
|
|
73778
|
+
};
|
|
73779
|
+
for (const entry of skills) {
|
|
73780
|
+
if (entry.state === "in-sync")
|
|
73781
|
+
summary.inSync += 1;
|
|
73782
|
+
if (entry.state === "conflict")
|
|
73783
|
+
summary.conflicts += 1;
|
|
73784
|
+
if (entry.action === "skip")
|
|
73785
|
+
summary.skipped += 1;
|
|
73786
|
+
}
|
|
73787
|
+
const pushSlugs = skills.filter((entry) => entry.action === "push").map((entry) => entry.slug);
|
|
73788
|
+
const pullSlugs = skills.filter((entry) => entry.action === "pull").map((entry) => entry.slug);
|
|
73789
|
+
if (!dryRun) {
|
|
73790
|
+
for (const slug of pushSlugs) {
|
|
73791
|
+
const entry = skills.find((item) => item.slug === slug);
|
|
73792
|
+
const plannedRemote = remotes.get(slug)?.sha256;
|
|
73793
|
+
let currentRemote;
|
|
73794
|
+
try {
|
|
73795
|
+
currentRemote = await currentRemoteDigest(client, slug);
|
|
73796
|
+
} catch (error2) {
|
|
73797
|
+
entry.result = { ok: false, detail: error2.message };
|
|
73798
|
+
summary.errors += 1;
|
|
73799
|
+
continue;
|
|
73800
|
+
}
|
|
73801
|
+
if (plannedRemote === undefined ? currentRemote !== undefined : currentRemote !== plannedRemote) {
|
|
73802
|
+
entry.result = { ok: false, detail: "remote changed during sync; push skipped" };
|
|
73803
|
+
entry.reason = (entry.reason ? `${entry.reason}; ` : "") + "remote changed during sync";
|
|
73804
|
+
summary.skipped += 1;
|
|
73805
|
+
continue;
|
|
73806
|
+
}
|
|
73807
|
+
try {
|
|
73808
|
+
await pushSkill(slug, { rootDir: root, client });
|
|
73809
|
+
const pushed = locals.get(slug);
|
|
73810
|
+
writePullMarker(join31(root, slug), {
|
|
73811
|
+
skill: slug,
|
|
73812
|
+
...pushed?.version ? { version: pushed.version } : {},
|
|
73813
|
+
...pushed?.sha256 ? { contentHash: pushed.sha256 } : {},
|
|
73814
|
+
source: "sync"
|
|
73815
|
+
});
|
|
73816
|
+
entry.result = { ok: true };
|
|
73817
|
+
summary.pushed += 1;
|
|
73818
|
+
} catch (error2) {
|
|
73819
|
+
entry.result = { ok: false, detail: error2.message };
|
|
73820
|
+
summary.errors += 1;
|
|
73821
|
+
}
|
|
73822
|
+
}
|
|
73823
|
+
const verifiedPullSlugs = [];
|
|
73824
|
+
for (const slug of pullSlugs) {
|
|
73825
|
+
const entry = skills.find((item) => item.slug === slug);
|
|
73826
|
+
const plannedRemote = remotes.get(slug)?.sha256;
|
|
73827
|
+
let currentRemote;
|
|
73828
|
+
try {
|
|
73829
|
+
currentRemote = await currentRemoteDigest(client, slug);
|
|
73830
|
+
} catch (error2) {
|
|
73831
|
+
entry.result = { ok: false, detail: error2.message };
|
|
73832
|
+
summary.errors += 1;
|
|
73833
|
+
continue;
|
|
73834
|
+
}
|
|
73835
|
+
if (plannedRemote === undefined ? currentRemote !== undefined : currentRemote !== plannedRemote) {
|
|
73836
|
+
entry.result = { ok: false, detail: "remote changed during sync; pull skipped" };
|
|
73837
|
+
entry.reason = (entry.reason ? `${entry.reason}; ` : "") + "remote changed during sync";
|
|
73838
|
+
summary.skipped += 1;
|
|
73839
|
+
continue;
|
|
73840
|
+
}
|
|
73841
|
+
const plannedLocal = locals.get(slug)?.sha256;
|
|
73842
|
+
const localDir = getPortableSkillPath(slug, { rootDir: root });
|
|
73843
|
+
const localMoved = recheckLocalSide(plannedLocal, localDir);
|
|
73844
|
+
if (localMoved) {
|
|
73845
|
+
entry.result = { ok: false, detail: "local changed during sync; pull skipped" };
|
|
73846
|
+
entry.reason = (entry.reason ? `${entry.reason}; ` : "") + "local changed during sync";
|
|
73847
|
+
summary.skipped += 1;
|
|
73848
|
+
continue;
|
|
73849
|
+
}
|
|
73850
|
+
verifiedPullSlugs.push(slug);
|
|
73851
|
+
}
|
|
73852
|
+
if (verifiedPullSlugs.length > 0) {
|
|
73853
|
+
try {
|
|
73854
|
+
const pulled = await pullSkills({ names: verifiedPullSlugs, rootDir: root, client, signingKey: options.signingKey });
|
|
73855
|
+
for (const result2 of pulled.results) {
|
|
73856
|
+
const entry = skills.find((item) => item.slug === result2.name);
|
|
73857
|
+
if (entry) {
|
|
73858
|
+
entry.result = { ok: result2.success, ...result2.error ? { detail: result2.error } : {} };
|
|
73859
|
+
if (result2.success)
|
|
73860
|
+
summary.pulled += 1;
|
|
73861
|
+
else
|
|
73862
|
+
summary.errors += 1;
|
|
73863
|
+
}
|
|
73864
|
+
}
|
|
73865
|
+
} catch (error2) {
|
|
73866
|
+
for (const slug of verifiedPullSlugs) {
|
|
73867
|
+
const entry = skills.find((item) => item.slug === slug);
|
|
73868
|
+
if (entry)
|
|
73869
|
+
entry.result = { ok: false, detail: error2.message };
|
|
73870
|
+
}
|
|
73871
|
+
summary.errors += verifiedPullSlugs.length;
|
|
73872
|
+
}
|
|
73873
|
+
}
|
|
73874
|
+
if (summary.errors === 0) {
|
|
73875
|
+
const cursor = {
|
|
73876
|
+
schemaVersion: SYNC_CURSOR_SCHEMA_VERSION,
|
|
73877
|
+
managedBy: "@hasna/skills",
|
|
73878
|
+
lastSyncedAt: new Date().toISOString(),
|
|
73879
|
+
runCount: readCursor(root).runCount + 1,
|
|
73880
|
+
summary
|
|
73881
|
+
};
|
|
73882
|
+
writeFileSync19(join31(root, SYNC_CURSOR_FILE), `${JSON.stringify(cursor, null, 2)}
|
|
73883
|
+
`);
|
|
73884
|
+
return {
|
|
73885
|
+
corpusRoot: root,
|
|
73886
|
+
migrationPending,
|
|
73887
|
+
direction,
|
|
73888
|
+
dryRun: false,
|
|
73889
|
+
conflictPolicy: conflict,
|
|
73890
|
+
conflictPolicyDescription: conflict === "skip" ? DEFAULT_CONFLICT_POLICY : conflict,
|
|
73891
|
+
summary,
|
|
73892
|
+
skills,
|
|
73893
|
+
cursor: { lastSyncedAt: cursor.lastSyncedAt, runCount: cursor.runCount }
|
|
73894
|
+
};
|
|
73895
|
+
}
|
|
73896
|
+
}
|
|
73897
|
+
if (dryRun) {
|
|
73898
|
+
summary.pushed = pushSlugs.length;
|
|
73899
|
+
summary.pulled = pullSlugs.length;
|
|
73900
|
+
}
|
|
73901
|
+
return {
|
|
73902
|
+
corpusRoot: root,
|
|
73903
|
+
migrationPending,
|
|
73904
|
+
direction,
|
|
73905
|
+
dryRun,
|
|
73906
|
+
conflictPolicy: conflict,
|
|
73907
|
+
conflictPolicyDescription: conflict === "skip" ? DEFAULT_CONFLICT_POLICY : conflict,
|
|
73908
|
+
summary,
|
|
73909
|
+
skills
|
|
73910
|
+
};
|
|
73911
|
+
}
|
|
73912
|
+
var DEFAULT_CONFLICT_POLICY = "local-wins-on-identical-digest-else-skip-and-report", CONFLICT_POLICIES, SYNC_CURSOR_FILE = ".sync-cursor.json", SYNC_CURSOR_SCHEMA_VERSION = 1, ReconcileRegistryError;
|
|
73913
|
+
var init_registry_reconcile = __esm(() => {
|
|
73914
|
+
init_publish();
|
|
73915
|
+
init_config();
|
|
73916
|
+
init_home_migration();
|
|
73917
|
+
init_portable_skills();
|
|
73918
|
+
init_pull();
|
|
73919
|
+
init_remote_client();
|
|
73920
|
+
init_skill_bundle();
|
|
73921
|
+
CONFLICT_POLICIES = ["local", "remote", "skip"];
|
|
73922
|
+
ReconcileRegistryError = class ReconcileRegistryError extends Error {
|
|
73923
|
+
detail;
|
|
73924
|
+
constructor(message, detail) {
|
|
73925
|
+
super(message);
|
|
73926
|
+
this.detail = detail;
|
|
73927
|
+
this.name = "ReconcileRegistryError";
|
|
73928
|
+
}
|
|
73929
|
+
};
|
|
73930
|
+
});
|
|
73931
|
+
|
|
73932
|
+
// src/cli/commands/registry-reconcile.ts
|
|
73933
|
+
var exports_registry_reconcile = {};
|
|
73934
|
+
__export(exports_registry_reconcile, {
|
|
73935
|
+
registerRegistryReconcile: () => registerRegistryReconcile
|
|
73936
|
+
});
|
|
73937
|
+
function registerRegistryReconcile(parent) {
|
|
73938
|
+
const group = parent.command("cloud").description("Synchronize this machine's skill corpus with the hosted registry");
|
|
73939
|
+
group.command("sync").option("--push", "Push local changes to the registry (local-only, changed-locally, and conflicts won by local)", false).option("--pull", "Pull registry changes into the local corpus (remote-only, changed-remotely, and conflicts won by remote)", false).option("--all", "Both directions (the default when neither --push nor --pull is given)", false).option("--dry-run", "Plan and report without writing anything", false).option("--json", "Output the full result as JSON", false).option("--conflict <policy>", `Conflict policy: ${CONFLICT_POLICIES.join(" | ")}. Default: ${DEFAULT_CONFLICT_POLICY}.`, "skip").description("Two-way reconcile between the local corpus and the hosted registry").action(async (options) => {
|
|
73940
|
+
try {
|
|
73941
|
+
const result2 = await reconcileRegistry({
|
|
73942
|
+
push: options.push,
|
|
73943
|
+
pull: options.pull,
|
|
73944
|
+
all: options.all,
|
|
73945
|
+
dryRun: options.dryRun,
|
|
73946
|
+
conflict: options.conflict
|
|
73947
|
+
});
|
|
73948
|
+
if (options.json) {
|
|
73949
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
73950
|
+
if (result2.summary.errors > 0)
|
|
73951
|
+
process.exitCode = 1;
|
|
73952
|
+
return;
|
|
73953
|
+
}
|
|
73954
|
+
printHuman2(result2);
|
|
73955
|
+
if (result2.summary.errors > 0)
|
|
73956
|
+
process.exitCode = 1;
|
|
73957
|
+
} catch (error2) {
|
|
73958
|
+
if (options.json) {
|
|
73959
|
+
console.log(JSON.stringify({ error: error2.message }, null, 2));
|
|
73960
|
+
} else {
|
|
73961
|
+
console.error(source_default.red(error2.message));
|
|
73962
|
+
if (error2 instanceof ReconcileRegistryError)
|
|
73963
|
+
for (const line of error2.detail ?? [])
|
|
73964
|
+
console.error(source_default.dim(` - ${line}`));
|
|
73965
|
+
}
|
|
73966
|
+
process.exitCode = 1;
|
|
73967
|
+
}
|
|
73968
|
+
});
|
|
73969
|
+
}
|
|
73970
|
+
function printHuman2(result2) {
|
|
73971
|
+
const { summary } = result2;
|
|
73972
|
+
const heading = result2.dryRun ? "Dry run: what a sync would do" : summary.errors > 0 ? "Sync completed with errors" : "Sync complete";
|
|
73973
|
+
console.log(source_default.bold(`${heading} (${result2.direction === "all" ? "all" : result2.direction}${result2.dryRun ? ", dry-run" : ""})`));
|
|
73974
|
+
console.log(` ${source_default.dim("corpus")} ${result2.corpusRoot}`);
|
|
73975
|
+
console.log(` ${source_default.dim("conflict")} ${result2.conflictPolicy}`);
|
|
73976
|
+
console.log(` ${source_default.dim("summary")} ${summary.pushed} pushed \xB7 ${summary.pulled} pulled \xB7 ${summary.inSync} in-sync \xB7 ${summary.conflicts} conflict(s) \xB7 ${summary.skipped} skipped \xB7 ${summary.errors} error(s)`);
|
|
73977
|
+
const interesting = result2.skills.filter((entry) => entry.action !== "none" || entry.state === "conflict");
|
|
73978
|
+
for (const entry of interesting) {
|
|
73979
|
+
const verb = entry.action === "push" ? source_default.green("push") : entry.action === "pull" ? source_default.cyan("pull") : source_default.yellow(entry.action);
|
|
73980
|
+
console.log(` ${verb.padEnd(7)} ${entry.slug}${entry.reason ? source_default.dim(` \u2014 ${entry.reason}`) : ""}`);
|
|
73981
|
+
if (entry.result && !entry.result.ok) {
|
|
73982
|
+
console.log(source_default.dim(` failed: ${entry.result.detail ?? "unknown error"}`));
|
|
73983
|
+
}
|
|
73984
|
+
}
|
|
73985
|
+
console.log("");
|
|
73986
|
+
}
|
|
73987
|
+
var init_registry_reconcile2 = __esm(() => {
|
|
73988
|
+
init_source();
|
|
73989
|
+
init_registry_reconcile();
|
|
73990
|
+
});
|
|
73991
|
+
|
|
73127
73992
|
// ../../node_modules/.bun/ink@5.2.1+6e93904c7bfe2418/node_modules/ink/build/render.js
|
|
73128
73993
|
import { Stream } from "stream";
|
|
73129
73994
|
import process12 from "process";
|
|
@@ -78144,8 +79009,9 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
78144
79009
|
// src/cli/index.tsx
|
|
78145
79010
|
init_esm();
|
|
78146
79011
|
|
|
78147
|
-
//
|
|
79012
|
+
// ../events/dist/commander.js
|
|
78148
79013
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
79014
|
+
import { Buffer as Buffer22 } from "buffer";
|
|
78149
79015
|
import { existsSync as existsSync2 } from "fs";
|
|
78150
79016
|
import { homedir } from "os";
|
|
78151
79017
|
import { join as join2 } from "path";
|
|
@@ -78161,29 +79027,83 @@ function getPathValue(input, path) {
|
|
|
78161
79027
|
return;
|
|
78162
79028
|
}, input);
|
|
78163
79029
|
}
|
|
78164
|
-
function
|
|
78165
|
-
const
|
|
78166
|
-
|
|
79030
|
+
function getFieldValues(input, path) {
|
|
79031
|
+
const values2 = [];
|
|
79032
|
+
const push = (value) => {
|
|
79033
|
+
if (!values2.some((item) => Object.is(item, value)))
|
|
79034
|
+
values2.push(value);
|
|
79035
|
+
};
|
|
79036
|
+
if (path.includes(".") && path in input)
|
|
79037
|
+
push(input[path]);
|
|
79038
|
+
const nestedValue = getPathValue(input, path);
|
|
79039
|
+
if (nestedValue !== undefined || !path.includes("."))
|
|
79040
|
+
push(nestedValue);
|
|
79041
|
+
return values2;
|
|
78167
79042
|
}
|
|
78168
|
-
function
|
|
79043
|
+
function wildcardToRegExp(pattern, options = {}) {
|
|
79044
|
+
let body = "";
|
|
79045
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
79046
|
+
const char = pattern[index];
|
|
79047
|
+
if (char === "*") {
|
|
79048
|
+
if (pattern[index + 1] === "*") {
|
|
79049
|
+
body += ".*";
|
|
79050
|
+
index += 1;
|
|
79051
|
+
} else {
|
|
79052
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
79053
|
+
}
|
|
79054
|
+
} else {
|
|
79055
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
79056
|
+
}
|
|
79057
|
+
}
|
|
79058
|
+
return new RegExp(`^${body}$`);
|
|
79059
|
+
}
|
|
79060
|
+
function matchString(value, matcher, options = {}) {
|
|
78169
79061
|
if (matcher === undefined)
|
|
78170
79062
|
return true;
|
|
78171
79063
|
if (value === undefined)
|
|
78172
79064
|
return false;
|
|
78173
79065
|
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
78174
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
79066
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
78175
79067
|
}
|
|
78176
79068
|
function matchRecord(input, matcher) {
|
|
78177
79069
|
if (!matcher)
|
|
78178
79070
|
return true;
|
|
78179
79071
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
78180
|
-
const
|
|
78181
|
-
|
|
78182
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
78183
|
-
}
|
|
78184
|
-
return actual === expected;
|
|
79072
|
+
const actualValues = getFieldValues(input, path);
|
|
79073
|
+
return matchField(actualValues, expected, path);
|
|
78185
79074
|
});
|
|
78186
79075
|
}
|
|
79076
|
+
function matchField(actualValues, expected, path) {
|
|
79077
|
+
if (isNegativeMatcher(expected)) {
|
|
79078
|
+
return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
|
|
79079
|
+
}
|
|
79080
|
+
return actualValues.some((actual) => matchPositiveField(actual, expected, path));
|
|
79081
|
+
}
|
|
79082
|
+
function matchPositiveField(actual, expected, path) {
|
|
79083
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
79084
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
79085
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
79086
|
+
}));
|
|
79087
|
+
}
|
|
79088
|
+
if (Array.isArray(actual)) {
|
|
79089
|
+
return actual.some((item) => item === expected);
|
|
79090
|
+
}
|
|
79091
|
+
return actual === expected;
|
|
79092
|
+
}
|
|
79093
|
+
function stringCandidates(actual) {
|
|
79094
|
+
if (actual === undefined)
|
|
79095
|
+
return [];
|
|
79096
|
+
if (Array.isArray(actual)) {
|
|
79097
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
79098
|
+
}
|
|
79099
|
+
return [String(actual)];
|
|
79100
|
+
}
|
|
79101
|
+
function isPrimitiveFieldValue(value) {
|
|
79102
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
79103
|
+
}
|
|
79104
|
+
function isNegativeMatcher(value) {
|
|
79105
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
79106
|
+
}
|
|
78187
79107
|
function eventMatchesFilter(event, filter2) {
|
|
78188
79108
|
return matchString(event.source, filter2.source) && matchString(event.type, filter2.type) && matchString(event.subject, filter2.subject) && matchString(event.severity, filter2.severity) && matchRecord(event.data, filter2.data) && matchRecord(event.metadata, filter2.metadata);
|
|
78189
79109
|
}
|
|
@@ -78196,17 +79116,29 @@ function channelMatchesEvent(channel, event) {
|
|
|
78196
79116
|
}
|
|
78197
79117
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
78198
79118
|
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
79119
|
+
var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
|
|
79120
|
+
var DEFAULT_EVENT_PAGE_LIMIT = 100;
|
|
79121
|
+
var MAX_EVENT_PAGE_LIMIT = 1000;
|
|
78199
79122
|
function getEventsDataDir(override) {
|
|
78200
79123
|
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join2(homedir(), ".hasna", "events");
|
|
78201
79124
|
}
|
|
79125
|
+
function getActiveEventsDirEnv() {
|
|
79126
|
+
if (process.env[HASNA_EVENTS_DIR_ENV])
|
|
79127
|
+
return HASNA_EVENTS_DIR_ENV;
|
|
79128
|
+
if (process.env[HASNA_EVENTS_HOME_ENV])
|
|
79129
|
+
return HASNA_EVENTS_HOME_ENV;
|
|
79130
|
+
return null;
|
|
79131
|
+
}
|
|
78202
79132
|
|
|
78203
79133
|
class JsonEventsStore {
|
|
78204
79134
|
dataDir;
|
|
79135
|
+
runtime;
|
|
78205
79136
|
channelsPath;
|
|
78206
79137
|
eventsPath;
|
|
78207
79138
|
deliveriesPath;
|
|
78208
79139
|
constructor(dataDir = getEventsDataDir()) {
|
|
78209
79140
|
this.dataDir = dataDir;
|
|
79141
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
78210
79142
|
this.channelsPath = join2(dataDir, "channels.json");
|
|
78211
79143
|
this.eventsPath = join2(dataDir, "events.json");
|
|
78212
79144
|
this.deliveriesPath = join2(dataDir, "deliveries.json");
|
|
@@ -78254,13 +79186,58 @@ class JsonEventsStore {
|
|
|
78254
79186
|
await this.writeJson(this.eventsPath, events);
|
|
78255
79187
|
return event;
|
|
78256
79188
|
}
|
|
78257
|
-
async
|
|
79189
|
+
async appendEventOnce(event, options = {}) {
|
|
79190
|
+
await this.init();
|
|
79191
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
79192
|
+
const dedupe = options.dedupe !== false;
|
|
79193
|
+
if (dedupe) {
|
|
79194
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
79195
|
+
if (existing) {
|
|
79196
|
+
return {
|
|
79197
|
+
event: existing,
|
|
79198
|
+
stored: false,
|
|
79199
|
+
deduped: true,
|
|
79200
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
79201
|
+
};
|
|
79202
|
+
}
|
|
79203
|
+
}
|
|
79204
|
+
events.push(event);
|
|
79205
|
+
await this.writeJson(this.eventsPath, events);
|
|
79206
|
+
return {
|
|
79207
|
+
event,
|
|
79208
|
+
stored: true,
|
|
79209
|
+
deduped: false,
|
|
79210
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
79211
|
+
};
|
|
79212
|
+
}
|
|
79213
|
+
async listEvents(options = {}) {
|
|
78258
79214
|
await this.init();
|
|
78259
|
-
|
|
79215
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
79216
|
+
return queryEvents(events, options);
|
|
79217
|
+
}
|
|
79218
|
+
async listEventsPage(options = {}) {
|
|
79219
|
+
await this.init();
|
|
79220
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
79221
|
+
const queried = queryEvents(events, {
|
|
79222
|
+
eventId: options.eventId,
|
|
79223
|
+
source: options.source,
|
|
79224
|
+
type: options.type
|
|
79225
|
+
});
|
|
79226
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
79227
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
79228
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
79229
|
+
const nextOffset = offset + pageEvents.length;
|
|
79230
|
+
const hasMore = nextOffset < queried.length;
|
|
79231
|
+
return {
|
|
79232
|
+
events: pageEvents,
|
|
79233
|
+
cursor: options.cursor,
|
|
79234
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
79235
|
+
hasMore
|
|
79236
|
+
};
|
|
78260
79237
|
}
|
|
78261
79238
|
async findEventByIdentity(identity2) {
|
|
78262
79239
|
const events = await this.listEvents();
|
|
78263
|
-
return events
|
|
79240
|
+
return findEventByIdentity(events, identity2);
|
|
78264
79241
|
}
|
|
78265
79242
|
async appendDelivery(result2) {
|
|
78266
79243
|
await this.init();
|
|
@@ -78311,6 +79288,130 @@ class JsonEventsStore {
|
|
|
78311
79288
|
});
|
|
78312
79289
|
}
|
|
78313
79290
|
}
|
|
79291
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
79292
|
+
return {
|
|
79293
|
+
mode: "local-files",
|
|
79294
|
+
name: "json-events-store",
|
|
79295
|
+
remote: false,
|
|
79296
|
+
localFiles: true,
|
|
79297
|
+
localSqlite: false,
|
|
79298
|
+
postgres: false,
|
|
79299
|
+
s3: false,
|
|
79300
|
+
aws: false,
|
|
79301
|
+
durable: true,
|
|
79302
|
+
idempotency: "best-effort-local",
|
|
79303
|
+
replayCursors: true,
|
|
79304
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
79305
|
+
};
|
|
79306
|
+
}
|
|
79307
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
79308
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
79309
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
79310
|
+
const payload = {
|
|
79311
|
+
offset,
|
|
79312
|
+
eventId: options.eventId,
|
|
79313
|
+
source: options.source,
|
|
79314
|
+
type: options.type
|
|
79315
|
+
};
|
|
79316
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer22.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
79317
|
+
}
|
|
79318
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
79319
|
+
if (!cursor)
|
|
79320
|
+
return 0;
|
|
79321
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
79322
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
79323
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
79324
|
+
let payload;
|
|
79325
|
+
try {
|
|
79326
|
+
payload = JSON.parse(Buffer22.from(rawPayload, "base64url").toString("utf-8"));
|
|
79327
|
+
} catch {
|
|
79328
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
79329
|
+
}
|
|
79330
|
+
const offset = payload.offset;
|
|
79331
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
79332
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
79333
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
79334
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
79335
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
79336
|
+
return offset;
|
|
79337
|
+
}
|
|
79338
|
+
function normalizeEventPageLimit(limit) {
|
|
79339
|
+
if (limit === undefined)
|
|
79340
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
79341
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
79342
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
79343
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
79344
|
+
}
|
|
79345
|
+
function queryEvents(events, options) {
|
|
79346
|
+
let rows = events;
|
|
79347
|
+
if (options.eventId)
|
|
79348
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
79349
|
+
if (options.source)
|
|
79350
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
79351
|
+
if (options.type)
|
|
79352
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
79353
|
+
if (options.cursor) {
|
|
79354
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
79355
|
+
rows = rows.slice(offset);
|
|
79356
|
+
}
|
|
79357
|
+
if (options.limit !== undefined)
|
|
79358
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
79359
|
+
return rows;
|
|
79360
|
+
}
|
|
79361
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
79362
|
+
if (cursorValue !== optionValue)
|
|
79363
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
79364
|
+
}
|
|
79365
|
+
function findEventByIdentity(events, identity2) {
|
|
79366
|
+
return events.find((event) => identity2.id !== undefined && event.id === identity2.id || identity2.dedupeKey !== undefined && event.dedupeKey === identity2.dedupeKey);
|
|
79367
|
+
}
|
|
79368
|
+
async function getEventsStatus(dataDir) {
|
|
79369
|
+
const store = new JsonEventsStore(dataDir);
|
|
79370
|
+
await store.init();
|
|
79371
|
+
const [channels, events, deliveries] = await Promise.all([
|
|
79372
|
+
store.listChannels(),
|
|
79373
|
+
store.listEvents(),
|
|
79374
|
+
store.listDeliveries()
|
|
79375
|
+
]);
|
|
79376
|
+
const transports = channels.reduce((counts, channel) => {
|
|
79377
|
+
counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
|
|
79378
|
+
return counts;
|
|
79379
|
+
}, {});
|
|
79380
|
+
return {
|
|
79381
|
+
service: "events",
|
|
79382
|
+
schemaVersion: "1.0",
|
|
79383
|
+
dataDir: store.dataDir,
|
|
79384
|
+
storage: store.runtime,
|
|
79385
|
+
env: {
|
|
79386
|
+
primary: HASNA_EVENTS_DIR_ENV,
|
|
79387
|
+
fallback: HASNA_EVENTS_HOME_ENV,
|
|
79388
|
+
active: getActiveEventsDirEnv()
|
|
79389
|
+
},
|
|
79390
|
+
files: {
|
|
79391
|
+
channels: statusFile(store.dataDir, "channels.json", channels.length),
|
|
79392
|
+
events: statusFile(store.dataDir, "events.json", events.length),
|
|
79393
|
+
deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
|
|
79394
|
+
},
|
|
79395
|
+
counts: {
|
|
79396
|
+
channels: channels.length,
|
|
79397
|
+
enabledChannels: channels.filter((channel) => channel.enabled).length,
|
|
79398
|
+
disabledChannels: channels.filter((channel) => !channel.enabled).length,
|
|
79399
|
+
events: events.length,
|
|
79400
|
+
deliveries: deliveries.length
|
|
79401
|
+
},
|
|
79402
|
+
transports,
|
|
79403
|
+
safety: {
|
|
79404
|
+
includesEventPayloads: false,
|
|
79405
|
+
includesWebhookSecrets: false,
|
|
79406
|
+
listOutputsRedactSecrets: true,
|
|
79407
|
+
statusOutputIsMetadataOnly: true
|
|
79408
|
+
}
|
|
79409
|
+
};
|
|
79410
|
+
}
|
|
79411
|
+
function statusFile(dataDir, fileName, records) {
|
|
79412
|
+
const path = join2(dataDir, fileName);
|
|
79413
|
+
return { path, exists: existsSync2(path), records };
|
|
79414
|
+
}
|
|
78314
79415
|
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
78315
79416
|
function buildSignatureBase(timestamp, body) {
|
|
78316
79417
|
return `${timestamp}.${body}`;
|
|
@@ -78325,21 +79426,27 @@ function now2() {
|
|
|
78325
79426
|
function truncate2(value, max2 = 4096) {
|
|
78326
79427
|
return value.length > max2 ? `${value.slice(0, max2)}...` : value;
|
|
78327
79428
|
}
|
|
78328
|
-
function buildWebhookRequest(event, channel) {
|
|
79429
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
78329
79430
|
if (!channel.webhook)
|
|
78330
79431
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
79432
|
+
for (const name of Object.keys(channel.webhook.headers ?? {})) {
|
|
79433
|
+
if (/^x-hasna-/i.test(name)) {
|
|
79434
|
+
throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
|
|
79435
|
+
}
|
|
79436
|
+
}
|
|
78331
79437
|
const body = JSON.stringify(event);
|
|
78332
|
-
const timestamp =
|
|
79438
|
+
const timestamp = options.timestamp ?? new Date().toISOString();
|
|
78333
79439
|
const headers = {
|
|
78334
79440
|
"Content-Type": "application/json",
|
|
78335
79441
|
"User-Agent": "@hasna/events",
|
|
78336
79442
|
"X-Hasna-Event-Id": event.id,
|
|
78337
79443
|
"X-Hasna-Event-Type": event.type,
|
|
78338
|
-
|
|
78339
|
-
|
|
79444
|
+
...channel.webhook.headers,
|
|
79445
|
+
"X-Hasna-Timestamp": timestamp
|
|
78340
79446
|
};
|
|
78341
|
-
|
|
78342
|
-
|
|
79447
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
79448
|
+
if (secret) {
|
|
79449
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
|
|
78343
79450
|
}
|
|
78344
79451
|
return { body, headers };
|
|
78345
79452
|
}
|
|
@@ -78347,7 +79454,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
78347
79454
|
if (!channel.webhook)
|
|
78348
79455
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
78349
79456
|
const startedAt = now2();
|
|
78350
|
-
|
|
79457
|
+
let secret = channel.webhook.secret;
|
|
79458
|
+
if (channel.webhook.secretRef) {
|
|
79459
|
+
if (!options.secretResolver) {
|
|
79460
|
+
return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
|
|
79461
|
+
}
|
|
79462
|
+
try {
|
|
79463
|
+
secret = await options.secretResolver(channel.webhook.secretRef);
|
|
79464
|
+
} catch {
|
|
79465
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
79466
|
+
}
|
|
79467
|
+
if (!secret)
|
|
79468
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
79469
|
+
}
|
|
79470
|
+
const timestamp = (options.now?.() ?? new Date).toISOString();
|
|
79471
|
+
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
|
|
78351
79472
|
const controller = new AbortController;
|
|
78352
79473
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
78353
79474
|
try {
|
|
@@ -78379,6 +79500,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
78379
79500
|
clearTimeout(timeout);
|
|
78380
79501
|
}
|
|
78381
79502
|
}
|
|
79503
|
+
function failedAttempt(startedAt, error) {
|
|
79504
|
+
return {
|
|
79505
|
+
attempt: 1,
|
|
79506
|
+
status: "failed",
|
|
79507
|
+
startedAt,
|
|
79508
|
+
completedAt: now2(),
|
|
79509
|
+
error
|
|
79510
|
+
};
|
|
79511
|
+
}
|
|
78382
79512
|
async function dispatchCommand(event, channel) {
|
|
78383
79513
|
if (!channel.command)
|
|
78384
79514
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -78467,6 +79597,90 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
78467
79597
|
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
78468
79598
|
};
|
|
78469
79599
|
}
|
|
79600
|
+
|
|
79601
|
+
class EventValidationError extends Error {
|
|
79602
|
+
eventType;
|
|
79603
|
+
issues;
|
|
79604
|
+
constructor(eventType, issues) {
|
|
79605
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
79606
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
79607
|
+
this.name = "EventValidationError";
|
|
79608
|
+
this.eventType = eventType;
|
|
79609
|
+
this.issues = issues;
|
|
79610
|
+
}
|
|
79611
|
+
}
|
|
79612
|
+
|
|
79613
|
+
class EventTypeCatalog {
|
|
79614
|
+
definitions = new Map;
|
|
79615
|
+
register(definition) {
|
|
79616
|
+
this.definitions.set(definition.type, definition);
|
|
79617
|
+
return this;
|
|
79618
|
+
}
|
|
79619
|
+
unregister(type) {
|
|
79620
|
+
return this.definitions.delete(type);
|
|
79621
|
+
}
|
|
79622
|
+
has(type) {
|
|
79623
|
+
return this.definitions.has(type);
|
|
79624
|
+
}
|
|
79625
|
+
get(type) {
|
|
79626
|
+
return this.definitions.get(type);
|
|
79627
|
+
}
|
|
79628
|
+
list() {
|
|
79629
|
+
return [...this.definitions.values()];
|
|
79630
|
+
}
|
|
79631
|
+
validateEvent(event) {
|
|
79632
|
+
const definition = this.definitions.get(event.type);
|
|
79633
|
+
if (!definition)
|
|
79634
|
+
return { ok: true };
|
|
79635
|
+
return definition.validate(event.data, event);
|
|
79636
|
+
}
|
|
79637
|
+
assertEventValid(event) {
|
|
79638
|
+
const result2 = this.validateEvent(event);
|
|
79639
|
+
if (!result2.ok) {
|
|
79640
|
+
throw new EventValidationError(event.type, result2.issues);
|
|
79641
|
+
}
|
|
79642
|
+
}
|
|
79643
|
+
}
|
|
79644
|
+
var defaultEventTypeCatalog = new EventTypeCatalog;
|
|
79645
|
+
var APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
79646
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
79647
|
+
if (paths.length === 0)
|
|
79648
|
+
return event;
|
|
79649
|
+
const copy = structuredClone(event);
|
|
79650
|
+
for (const path of paths) {
|
|
79651
|
+
setPath(copy, path, replacement);
|
|
79652
|
+
}
|
|
79653
|
+
return copy;
|
|
79654
|
+
}
|
|
79655
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
79656
|
+
return redactValue(event, replacement);
|
|
79657
|
+
}
|
|
79658
|
+
function shouldRedactKey(key) {
|
|
79659
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
79660
|
+
}
|
|
79661
|
+
function redactValue(value, replacement) {
|
|
79662
|
+
if (Array.isArray(value))
|
|
79663
|
+
return value.map((item) => redactValue(item, replacement));
|
|
79664
|
+
if (!value || typeof value !== "object")
|
|
79665
|
+
return value;
|
|
79666
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
79667
|
+
key,
|
|
79668
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
79669
|
+
]));
|
|
79670
|
+
}
|
|
79671
|
+
function setPath(input, path, replacement) {
|
|
79672
|
+
const parts = path.split(".");
|
|
79673
|
+
let cursor = input;
|
|
79674
|
+
for (const part of parts.slice(0, -1)) {
|
|
79675
|
+
const next = cursor[part];
|
|
79676
|
+
if (!next || typeof next !== "object")
|
|
79677
|
+
return;
|
|
79678
|
+
cursor = next;
|
|
79679
|
+
}
|
|
79680
|
+
const last2 = parts.at(-1);
|
|
79681
|
+
if (last2 && last2 in cursor)
|
|
79682
|
+
cursor[last2] = replacement;
|
|
79683
|
+
}
|
|
78470
79684
|
function createEvent(input) {
|
|
78471
79685
|
return {
|
|
78472
79686
|
id: input.id ?? randomUUID2(),
|
|
@@ -78487,10 +79701,18 @@ class EventsClient {
|
|
|
78487
79701
|
store;
|
|
78488
79702
|
redactors;
|
|
78489
79703
|
transportOptions;
|
|
79704
|
+
catalog;
|
|
79705
|
+
validateCatalogTypes;
|
|
78490
79706
|
constructor(options = {}) {
|
|
78491
79707
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
78492
79708
|
this.redactors = options.redactors ?? [];
|
|
78493
|
-
this.transportOptions = {
|
|
79709
|
+
this.transportOptions = {
|
|
79710
|
+
fetchImpl: options.fetchImpl,
|
|
79711
|
+
secretResolver: options.secretResolver,
|
|
79712
|
+
now: options.now
|
|
79713
|
+
};
|
|
79714
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
79715
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
78494
79716
|
}
|
|
78495
79717
|
async addChannel(input) {
|
|
78496
79718
|
const timestamp = new Date().toISOString();
|
|
@@ -78508,18 +79730,40 @@ class EventsClient {
|
|
|
78508
79730
|
}
|
|
78509
79731
|
async emit(input, options = {}) {
|
|
78510
79732
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
78511
|
-
if (options.
|
|
78512
|
-
|
|
78513
|
-
|
|
78514
|
-
|
|
78515
|
-
|
|
78516
|
-
|
|
78517
|
-
|
|
78518
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
78519
|
-
return { event, deliveries, deduped: false };
|
|
78520
|
-
}
|
|
78521
|
-
async listEvents() {
|
|
78522
|
-
|
|
79733
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
79734
|
+
this.catalog.assertEventValid(event);
|
|
79735
|
+
}
|
|
79736
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
79737
|
+
if (append.deduped) {
|
|
79738
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
79739
|
+
}
|
|
79740
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
79741
|
+
return { event: append.event, deliveries, deduped: false };
|
|
79742
|
+
}
|
|
79743
|
+
async listEvents(options = {}) {
|
|
79744
|
+
if (Object.keys(options).length === 0)
|
|
79745
|
+
return this.store.listEvents();
|
|
79746
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
79747
|
+
}
|
|
79748
|
+
async listEventsPage(options = {}) {
|
|
79749
|
+
if (this.store.listEventsPage)
|
|
79750
|
+
return this.store.listEventsPage(options);
|
|
79751
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
79752
|
+
eventId: options.eventId,
|
|
79753
|
+
source: options.source,
|
|
79754
|
+
type: options.type
|
|
79755
|
+
});
|
|
79756
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
79757
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
79758
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
79759
|
+
const nextOffset = offset + pageEvents.length;
|
|
79760
|
+
const hasMore = nextOffset < events.length;
|
|
79761
|
+
return {
|
|
79762
|
+
events: pageEvents,
|
|
79763
|
+
cursor: options.cursor,
|
|
79764
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
79765
|
+
hasMore
|
|
79766
|
+
};
|
|
78523
79767
|
}
|
|
78524
79768
|
async listDeliveries() {
|
|
78525
79769
|
return this.store.listDeliveries();
|
|
@@ -78536,7 +79780,7 @@ class EventsClient {
|
|
|
78536
79780
|
}
|
|
78537
79781
|
return deliveries;
|
|
78538
79782
|
}
|
|
78539
|
-
async
|
|
79783
|
+
async matchChannel(id, input = {}) {
|
|
78540
79784
|
const channel = await this.store.getChannel(id);
|
|
78541
79785
|
if (!channel)
|
|
78542
79786
|
throw new Error(`Channel not found: ${id}`);
|
|
@@ -78553,28 +79797,71 @@ class EventsClient {
|
|
|
78553
79797
|
time: input.time,
|
|
78554
79798
|
id: input.id
|
|
78555
79799
|
});
|
|
79800
|
+
const matched = channelMatchesEvent(channel, event);
|
|
79801
|
+
return {
|
|
79802
|
+
channelId: channel.id,
|
|
79803
|
+
matched,
|
|
79804
|
+
event,
|
|
79805
|
+
filters: channel.filters,
|
|
79806
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
79807
|
+
};
|
|
79808
|
+
}
|
|
79809
|
+
async testChannel(id, input = {}, options = {}) {
|
|
79810
|
+
const channel = await this.store.getChannel(id);
|
|
79811
|
+
if (!channel)
|
|
79812
|
+
throw new Error(`Channel not found: ${id}`);
|
|
79813
|
+
const match = await this.matchChannel(id, input);
|
|
79814
|
+
const event = match.event;
|
|
79815
|
+
if (options.honorFilters && !match.matched) {
|
|
79816
|
+
const timestamp = new Date().toISOString();
|
|
79817
|
+
const result22 = createDeliveryResult(event, channel, [{
|
|
79818
|
+
attempt: 1,
|
|
79819
|
+
status: "skipped",
|
|
79820
|
+
startedAt: timestamp,
|
|
79821
|
+
completedAt: timestamp,
|
|
79822
|
+
error: match.reason
|
|
79823
|
+
}]);
|
|
79824
|
+
result22.metadata = { reason: "filter_mismatch" };
|
|
79825
|
+
await this.store.appendDelivery(result22);
|
|
79826
|
+
return result22;
|
|
79827
|
+
}
|
|
78556
79828
|
const eventForChannel = await this.applyRedaction(event, channel);
|
|
78557
79829
|
const result2 = await this.deliverWithRetry(eventForChannel, channel);
|
|
78558
79830
|
await this.store.appendDelivery(result2);
|
|
78559
79831
|
return result2;
|
|
78560
79832
|
}
|
|
78561
79833
|
async replay(options = {}) {
|
|
78562
|
-
const
|
|
78563
|
-
if (options.eventId && event.id !== options.eventId)
|
|
78564
|
-
return false;
|
|
78565
|
-
if (options.source && event.source !== options.source)
|
|
78566
|
-
return false;
|
|
78567
|
-
if (options.type && event.type !== options.type)
|
|
78568
|
-
return false;
|
|
78569
|
-
return true;
|
|
78570
|
-
});
|
|
79834
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
78571
79835
|
if (options.dryRun)
|
|
78572
|
-
return { events, deliveries: [] };
|
|
79836
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
78573
79837
|
const deliveries = [];
|
|
78574
|
-
for (const event of events) {
|
|
79838
|
+
for (const event of page.events) {
|
|
78575
79839
|
deliveries.push(...await this.deliver(event));
|
|
78576
79840
|
}
|
|
78577
|
-
return { events, deliveries };
|
|
79841
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
79842
|
+
}
|
|
79843
|
+
async appendEvent(event, options) {
|
|
79844
|
+
if (this.store.appendEventOnce) {
|
|
79845
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
79846
|
+
}
|
|
79847
|
+
if (options.dedupe) {
|
|
79848
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
79849
|
+
if (existing) {
|
|
79850
|
+
return {
|
|
79851
|
+
event: existing,
|
|
79852
|
+
stored: false,
|
|
79853
|
+
deduped: true,
|
|
79854
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
79855
|
+
};
|
|
79856
|
+
}
|
|
79857
|
+
}
|
|
79858
|
+
const stored = await this.store.appendEvent(event);
|
|
79859
|
+
return {
|
|
79860
|
+
event: stored,
|
|
79861
|
+
stored: true,
|
|
79862
|
+
deduped: false,
|
|
79863
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
79864
|
+
};
|
|
78578
79865
|
}
|
|
78579
79866
|
async applyRedaction(event, channel) {
|
|
78580
79867
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -78601,15 +79888,6 @@ class EventsClient {
|
|
|
78601
79888
|
return createDeliveryResult(event, channel, attempts);
|
|
78602
79889
|
}
|
|
78603
79890
|
}
|
|
78604
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
78605
|
-
if (paths.length === 0)
|
|
78606
|
-
return event;
|
|
78607
|
-
const copy = structuredClone(event);
|
|
78608
|
-
for (const path of paths) {
|
|
78609
|
-
setPath(copy, path, replacement);
|
|
78610
|
-
}
|
|
78611
|
-
return copy;
|
|
78612
|
-
}
|
|
78613
79891
|
function sanitizeChannelForOutput(channel) {
|
|
78614
79892
|
const copy = structuredClone(channel);
|
|
78615
79893
|
if (copy.webhook?.secret)
|
|
@@ -78622,34 +79900,19 @@ function sanitizeChannelForOutput(channel) {
|
|
|
78622
79900
|
function sanitizeChannelsForOutput(channels) {
|
|
78623
79901
|
return channels.map(sanitizeChannelForOutput);
|
|
78624
79902
|
}
|
|
78625
|
-
function
|
|
78626
|
-
|
|
78627
|
-
|
|
78628
|
-
|
|
78629
|
-
|
|
78630
|
-
|
|
78631
|
-
|
|
78632
|
-
|
|
78633
|
-
|
|
78634
|
-
|
|
78635
|
-
|
|
78636
|
-
|
|
78637
|
-
|
|
78638
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
78639
|
-
]));
|
|
78640
|
-
}
|
|
78641
|
-
function setPath(input, path, replacement) {
|
|
78642
|
-
const parts = path.split(".");
|
|
78643
|
-
let cursor = input;
|
|
78644
|
-
for (const part of parts.slice(0, -1)) {
|
|
78645
|
-
const next = cursor[part];
|
|
78646
|
-
if (!next || typeof next !== "object")
|
|
78647
|
-
return;
|
|
78648
|
-
cursor = next;
|
|
78649
|
-
}
|
|
78650
|
-
const last2 = parts.at(-1);
|
|
78651
|
-
if (last2 && last2 in cursor)
|
|
78652
|
-
cursor[last2] = replacement;
|
|
79903
|
+
function queryClientEvents(events, options) {
|
|
79904
|
+
let rows = events;
|
|
79905
|
+
if (options.eventId)
|
|
79906
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
79907
|
+
if (options.source)
|
|
79908
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
79909
|
+
if (options.type)
|
|
79910
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
79911
|
+
if (options.cursor)
|
|
79912
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
79913
|
+
if (options.limit !== undefined)
|
|
79914
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
79915
|
+
return rows;
|
|
78653
79916
|
}
|
|
78654
79917
|
function normalizeTime(value) {
|
|
78655
79918
|
if (!value)
|
|
@@ -78663,6 +79926,77 @@ function normalizeRetryPolicy(policy) {
|
|
|
78663
79926
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
78664
79927
|
};
|
|
78665
79928
|
}
|
|
79929
|
+
function parseFieldMatchers(values2, label, typed = false) {
|
|
79930
|
+
if (!values2?.length)
|
|
79931
|
+
return;
|
|
79932
|
+
const result2 = {};
|
|
79933
|
+
for (const value of values2) {
|
|
79934
|
+
const parsed = parseMatcherExpression(value, label);
|
|
79935
|
+
const path = parsed.path;
|
|
79936
|
+
if (path in result2)
|
|
79937
|
+
throw new Error(`Duplicate ${label} filter path: ${path}`);
|
|
79938
|
+
const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
|
|
79939
|
+
result2[path] = parsed.negated ? { not: matcherValue } : matcherValue;
|
|
79940
|
+
}
|
|
79941
|
+
return result2;
|
|
79942
|
+
}
|
|
79943
|
+
function parseFilterOptions(options) {
|
|
79944
|
+
const filter2 = {};
|
|
79945
|
+
if (options.source)
|
|
79946
|
+
filter2.source = options.source;
|
|
79947
|
+
if (options.type)
|
|
79948
|
+
filter2.type = options.type;
|
|
79949
|
+
if (options.subject)
|
|
79950
|
+
filter2.subject = options.subject;
|
|
79951
|
+
if (options.severity)
|
|
79952
|
+
filter2.severity = options.severity;
|
|
79953
|
+
const data = mergeMatchers(parseFieldMatchers(options.data, "data"), parseFieldMatchers(options.dataJson, "data-json", true));
|
|
79954
|
+
const metadata = mergeMatchers(parseFieldMatchers(options.metadata, "metadata"), parseFieldMatchers(options.metadataJson, "metadata-json", true));
|
|
79955
|
+
if (Object.keys(data).length > 0)
|
|
79956
|
+
filter2.data = data;
|
|
79957
|
+
if (Object.keys(metadata).length > 0)
|
|
79958
|
+
filter2.metadata = metadata;
|
|
79959
|
+
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
79960
|
+
}
|
|
79961
|
+
function mergeMatchers(...records) {
|
|
79962
|
+
const result2 = {};
|
|
79963
|
+
for (const record of records) {
|
|
79964
|
+
if (!record)
|
|
79965
|
+
continue;
|
|
79966
|
+
for (const [path, value] of Object.entries(record)) {
|
|
79967
|
+
if (path in result2)
|
|
79968
|
+
throw new Error(`Duplicate filter path: ${path}`);
|
|
79969
|
+
result2[path] = value;
|
|
79970
|
+
}
|
|
79971
|
+
}
|
|
79972
|
+
return result2;
|
|
79973
|
+
}
|
|
79974
|
+
function parseTypedMatcherValue(value, label) {
|
|
79975
|
+
const parsed = JSON.parse(value);
|
|
79976
|
+
if (parsed === null || typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean" || Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
79977
|
+
return parsed;
|
|
79978
|
+
}
|
|
79979
|
+
throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
|
|
79980
|
+
}
|
|
79981
|
+
function parseMatcherExpression(value, label) {
|
|
79982
|
+
const negativeSeparator = value.indexOf("!=");
|
|
79983
|
+
if (negativeSeparator > 0) {
|
|
79984
|
+
return {
|
|
79985
|
+
path: value.slice(0, negativeSeparator),
|
|
79986
|
+
rawValue: value.slice(negativeSeparator + 2),
|
|
79987
|
+
negated: true
|
|
79988
|
+
};
|
|
79989
|
+
}
|
|
79990
|
+
const separator = value.indexOf("=");
|
|
79991
|
+
if (separator <= 0)
|
|
79992
|
+
throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
|
|
79993
|
+
return {
|
|
79994
|
+
path: value.slice(0, separator),
|
|
79995
|
+
rawValue: value.slice(separator + 1),
|
|
79996
|
+
negated: false
|
|
79997
|
+
};
|
|
79998
|
+
}
|
|
79999
|
+
var DEFAULT_EVENT_LIST_LIMIT = 100;
|
|
78666
80000
|
function parseJsonObject(value, fallback) {
|
|
78667
80001
|
if (!value)
|
|
78668
80002
|
return fallback;
|
|
@@ -78684,18 +80018,6 @@ function parseHeaders(values2) {
|
|
|
78684
80018
|
}
|
|
78685
80019
|
return headers;
|
|
78686
80020
|
}
|
|
78687
|
-
function parseFilter(options) {
|
|
78688
|
-
const filter2 = {};
|
|
78689
|
-
if (options.source)
|
|
78690
|
-
filter2.source = options.source;
|
|
78691
|
-
if (options.type)
|
|
78692
|
-
filter2.type = options.type;
|
|
78693
|
-
if (options.subject)
|
|
78694
|
-
filter2.subject = options.subject;
|
|
78695
|
-
if (options.severity)
|
|
78696
|
-
filter2.severity = options.severity;
|
|
78697
|
-
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
78698
|
-
}
|
|
78699
80021
|
function createClient(options) {
|
|
78700
80022
|
if (options.createClient)
|
|
78701
80023
|
return options.createClient();
|
|
@@ -78707,22 +80029,30 @@ function print(value, json, text) {
|
|
|
78707
80029
|
else
|
|
78708
80030
|
console.log(text);
|
|
78709
80031
|
}
|
|
80032
|
+
function fail(error, json) {
|
|
80033
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80034
|
+
if (json)
|
|
80035
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
80036
|
+
else
|
|
80037
|
+
console.error(message);
|
|
80038
|
+
process.exitCode = 1;
|
|
80039
|
+
}
|
|
78710
80040
|
function hasJsonOption(options) {
|
|
78711
80041
|
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
78712
80042
|
}
|
|
78713
80043
|
function wantsJson(actionOptions, command) {
|
|
78714
80044
|
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
78715
80045
|
}
|
|
78716
|
-
function
|
|
78717
|
-
const
|
|
78718
|
-
|
|
80046
|
+
function registerChannelCommands(program2, options) {
|
|
80047
|
+
const channels = program2.command(options.channelsCommandName ?? "channels").description("Manage Hasna event channels");
|
|
80048
|
+
channels.command("add").description("Add or replace a channel").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--data <path=value...>", "Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
|
|
78719
80049
|
const timestamp = new Date().toISOString();
|
|
78720
80050
|
const channel = {
|
|
78721
80051
|
id: actionOptions.id,
|
|
78722
80052
|
name: actionOptions.name,
|
|
78723
80053
|
enabled: !actionOptions.disabled,
|
|
78724
80054
|
transport: actionOptions.transport,
|
|
78725
|
-
filters:
|
|
80055
|
+
filters: parseFilterOptions(actionOptions),
|
|
78726
80056
|
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
78727
80057
|
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
78728
80058
|
createdAt: timestamp,
|
|
@@ -78738,35 +80068,61 @@ function registerWebhookCommands(program2, options) {
|
|
|
78738
80068
|
const saved = await createClient(options).addChannel(channel);
|
|
78739
80069
|
print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
|
|
78740
80070
|
});
|
|
78741
|
-
|
|
78742
|
-
const
|
|
80071
|
+
channels.command("list").description("List configured channels").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
80072
|
+
const channels2 = await createClient(options).listChannels();
|
|
78743
80073
|
if (wantsJson(actionOptions, command)) {
|
|
78744
|
-
console.log(JSON.stringify(sanitizeChannelsForOutput(
|
|
80074
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
|
|
78745
80075
|
return;
|
|
78746
80076
|
}
|
|
78747
|
-
if (!
|
|
80077
|
+
if (!channels2.length) {
|
|
78748
80078
|
console.log("No channels configured.");
|
|
78749
80079
|
return;
|
|
78750
80080
|
}
|
|
78751
|
-
for (const channel of
|
|
80081
|
+
for (const channel of channels2) {
|
|
78752
80082
|
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
78753
80083
|
}
|
|
78754
80084
|
});
|
|
78755
|
-
|
|
80085
|
+
channels.command("status").description("Show events channel storage status").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
80086
|
+
const status = await getEventsStatus(options.dataDir);
|
|
80087
|
+
print(status, wantsJson(actionOptions, command), `events dataDir: ${status.dataDir}`);
|
|
80088
|
+
});
|
|
80089
|
+
channels.command("remove").description("Remove a channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
78756
80090
|
const removed = await createClient(options).removeChannel(id);
|
|
78757
80091
|
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
78758
80092
|
});
|
|
78759
|
-
|
|
78760
|
-
const
|
|
78761
|
-
|
|
78762
|
-
|
|
78763
|
-
|
|
78764
|
-
|
|
78765
|
-
|
|
78766
|
-
|
|
78767
|
-
|
|
80093
|
+
channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
80094
|
+
const json = wantsJson(actionOptions, command);
|
|
80095
|
+
try {
|
|
80096
|
+
const result2 = await createClient(options).testChannel(id, {
|
|
80097
|
+
source: actionOptions.source ?? options.source,
|
|
80098
|
+
type: actionOptions.type,
|
|
80099
|
+
subject: actionOptions.subject ?? id,
|
|
80100
|
+
message: actionOptions.message,
|
|
80101
|
+
data: parseJsonObject(actionOptions.data, { test: true }),
|
|
80102
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
80103
|
+
}, { honorFilters: actionOptions.honorFilters });
|
|
80104
|
+
print(result2, json, `${result2.status}: ${result2.channelId}`);
|
|
80105
|
+
} catch (error) {
|
|
80106
|
+
fail(error, json);
|
|
80107
|
+
}
|
|
80108
|
+
});
|
|
80109
|
+
channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
80110
|
+
const json = wantsJson(actionOptions, command);
|
|
80111
|
+
try {
|
|
80112
|
+
const result2 = await createClient(options).matchChannel(id, {
|
|
80113
|
+
source: actionOptions.source ?? options.source,
|
|
80114
|
+
type: actionOptions.type,
|
|
80115
|
+
subject: actionOptions.subject ?? id,
|
|
80116
|
+
message: actionOptions.message,
|
|
80117
|
+
data: parseJsonObject(actionOptions.data, { test: true }),
|
|
80118
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
80119
|
+
});
|
|
80120
|
+
print(result2, json, `${result2.matched ? "matched" : "skipped"}: ${result2.channelId}`);
|
|
80121
|
+
} catch (error) {
|
|
80122
|
+
fail(error, json);
|
|
80123
|
+
}
|
|
78768
80124
|
});
|
|
78769
|
-
return
|
|
80125
|
+
return channels;
|
|
78770
80126
|
}
|
|
78771
80127
|
function registerEventCommands(program2, options) {
|
|
78772
80128
|
const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
@@ -78783,7 +80139,8 @@ function registerEventCommands(program2, options) {
|
|
|
78783
80139
|
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
78784
80140
|
print(result2, wantsJson(actionOptions, command), `${result2.deduped ? "Deduped" : "Emitted"} ${result2.event.id} to ${result2.deliveries.length} channel(s)`);
|
|
78785
80141
|
});
|
|
78786
|
-
|
|
80142
|
+
const defaultListLimit = options.defaultEventListLimit ?? DEFAULT_EVENT_LIST_LIMIT;
|
|
80143
|
+
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", `Limit to the most recent <n> events (default ${defaultListLimit}; use 0 for all)`, parseNumber, defaultListLimit).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
78787
80144
|
let rows = await createClient(options).listEvents();
|
|
78788
80145
|
if (actionOptions.source)
|
|
78789
80146
|
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
@@ -78802,19 +80159,21 @@ function registerEventCommands(program2, options) {
|
|
|
78802
80159
|
for (const event of rows)
|
|
78803
80160
|
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
78804
80161
|
});
|
|
78805
|
-
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
80162
|
+
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--cursor <cursor>", "Opaque replay cursor from a previous page").option("--limit <n>", "Maximum events to replay", parseNumber).option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
78806
80163
|
const result2 = await createClient(options).replay({
|
|
78807
80164
|
eventId: actionOptions.id,
|
|
78808
80165
|
source: actionOptions.source,
|
|
78809
80166
|
type: actionOptions.type,
|
|
80167
|
+
cursor: actionOptions.cursor,
|
|
80168
|
+
limit: actionOptions.limit,
|
|
78810
80169
|
dryRun: actionOptions.dryRun
|
|
78811
80170
|
});
|
|
78812
|
-
print(result2, wantsJson(actionOptions, command),
|
|
80171
|
+
print(result2, wantsJson(actionOptions, command), replaySummary(result2.events.length, result2.deliveries.length, result2.nextCursor));
|
|
78813
80172
|
});
|
|
78814
80173
|
return events;
|
|
78815
80174
|
}
|
|
78816
80175
|
function registerEventsCommands(program2, options) {
|
|
78817
|
-
|
|
80176
|
+
registerChannelCommands(program2, options);
|
|
78818
80177
|
registerEventCommands(program2, options);
|
|
78819
80178
|
}
|
|
78820
80179
|
function parseNumber(value) {
|
|
@@ -78827,6 +80186,10 @@ function collectValues(value, previous) {
|
|
|
78827
80186
|
previous.push(value);
|
|
78828
80187
|
return previous;
|
|
78829
80188
|
}
|
|
80189
|
+
function replaySummary(events, deliveries, nextCursor) {
|
|
80190
|
+
const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
|
|
80191
|
+
return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
|
|
80192
|
+
}
|
|
78830
80193
|
|
|
78831
80194
|
// src/cli/index.tsx
|
|
78832
80195
|
init_source();
|
|
@@ -79986,6 +81349,8 @@ var { registerFeedback: registerFeedback2 } = await Promise.resolve().then(() =>
|
|
|
79986
81349
|
registerFeedback2(program2);
|
|
79987
81350
|
var { registerStorage: registerStorage2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
79988
81351
|
registerStorage2(program2);
|
|
81352
|
+
var { registerRegistryReconcile: registerRegistryReconcile2 } = await Promise.resolve().then(() => (init_registry_reconcile2(), exports_registry_reconcile));
|
|
81353
|
+
registerRegistryReconcile2(program2);
|
|
79989
81354
|
registerEventsCommands(program2, { source: "skills" });
|
|
79990
81355
|
try {
|
|
79991
81356
|
await program2.parseAsync();
|