@hasna/skills 0.1.71 → 0.1.72
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/bin/index.js +639 -858
- package/bin/mcp.js +342 -232
- package/bin/migrate.js +149 -40
- package/bin/server.js +212 -100
- package/bin/worker.js +151 -45
- package/dist/index.js +461 -331
- package/dist/lib/app-home.d.ts +85 -0
- package/dist/lib/config.d.ts +9 -10
- package/dist/lib/station-hydrate.d.ts +2 -0
- package/dist/lib/station-snapshot.d.ts +1 -1
- package/dist/sdk/index.js +518 -630
- package/dist/storage.js +155 -43
- package/package.json +2 -1
package/bin/migrate.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/server/migrate.ts
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { join as
|
|
5
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync3 } from "fs";
|
|
6
|
+
import { join as join7 } from "path";
|
|
7
7
|
|
|
8
8
|
// src/lib/retired-settings.ts
|
|
9
9
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
@@ -81,65 +81,174 @@ function normalizePrefix(value) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// src/server/database-url.ts
|
|
84
|
-
import { isAbsolute, join as
|
|
84
|
+
import { isAbsolute, join as join4 } from "path";
|
|
85
85
|
import { fileURLToPath } from "url";
|
|
86
86
|
|
|
87
87
|
// src/lib/config.ts
|
|
88
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
89
|
-
import { join, dirname } from "path";
|
|
88
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
89
|
+
import { join as join3, dirname } from "path";
|
|
90
|
+
|
|
91
|
+
// src/lib/app-home.ts
|
|
92
|
+
import { existsSync } from "fs";
|
|
93
|
+
import { homedir as homedir2 } from "os";
|
|
94
|
+
import { join as join2, resolve } from "path";
|
|
95
|
+
|
|
96
|
+
// ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
|
|
90
97
|
import { homedir } from "os";
|
|
98
|
+
import { join } from "path";
|
|
99
|
+
var KIND_ENV = {
|
|
100
|
+
config: "HASNA_CONFIG_HOME",
|
|
101
|
+
data: "HASNA_DATA_HOME",
|
|
102
|
+
state: "HASNA_STATE_HOME",
|
|
103
|
+
cache: "HASNA_CACHE_HOME"
|
|
104
|
+
};
|
|
105
|
+
var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
106
|
+
function assertApp(app) {
|
|
107
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
108
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
109
|
+
}
|
|
110
|
+
if (!APP_SLUG_RE.test(app)) {
|
|
111
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function envOf(options) {
|
|
115
|
+
return options.env ?? process.env;
|
|
116
|
+
}
|
|
117
|
+
function envValue(options, kind) {
|
|
118
|
+
const value = envOf(options)[KIND_ENV[kind]];
|
|
119
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
120
|
+
}
|
|
121
|
+
function isMacOS(platform) {
|
|
122
|
+
return platform === "darwin";
|
|
123
|
+
}
|
|
124
|
+
function baseDir(kind, options) {
|
|
125
|
+
const override = envValue(options, kind);
|
|
126
|
+
if (override)
|
|
127
|
+
return override;
|
|
128
|
+
const home = options.home ?? homedir();
|
|
129
|
+
const platform = options.platform ?? process.platform;
|
|
130
|
+
if (isMacOS(platform)) {
|
|
131
|
+
switch (kind) {
|
|
132
|
+
case "config":
|
|
133
|
+
case "data":
|
|
134
|
+
return join(home, "Library", "Application Support", "Hasna");
|
|
135
|
+
case "cache":
|
|
136
|
+
return join(home, "Library", "Caches", "Hasna");
|
|
137
|
+
case "state":
|
|
138
|
+
return join(home, "Library", "Logs", "Hasna");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
switch (kind) {
|
|
142
|
+
case "config":
|
|
143
|
+
return join(home, ".config", "hasna");
|
|
144
|
+
case "data":
|
|
145
|
+
return join(home, ".local", "share", "hasna");
|
|
146
|
+
case "state":
|
|
147
|
+
return join(home, ".local", "state", "hasna");
|
|
148
|
+
case "cache":
|
|
149
|
+
return join(home, ".cache", "hasna");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function resolvePath(kind, options) {
|
|
153
|
+
assertApp(options.app);
|
|
154
|
+
const appSegment = options.internal === true ? join("internal", options.app) : options.app;
|
|
155
|
+
return join(baseDir(kind, options), appSegment);
|
|
156
|
+
}
|
|
157
|
+
function dataDir(options) {
|
|
158
|
+
return resolvePath("data", options);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/lib/app-home.ts
|
|
162
|
+
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
163
|
+
var HASNA_SKILLS_HOME_ENV = "HASNA_SKILLS_HOME";
|
|
164
|
+
var SKILLS_HOME_ENV = "SKILLS_HOME";
|
|
165
|
+
var DEFAULT_SQLITE_FILENAME = "server.db";
|
|
166
|
+
var GLOBAL_CONFIG_FILENAME = "config.json";
|
|
167
|
+
function effectiveHome() {
|
|
168
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2() || "/tmp";
|
|
169
|
+
}
|
|
170
|
+
function legacyDataRoot() {
|
|
171
|
+
return join2(effectiveHome(), ".hasna", "skills");
|
|
172
|
+
}
|
|
173
|
+
function resolverDataRoot(home = effectiveHome()) {
|
|
174
|
+
return dataDir({ app: "skills", home });
|
|
175
|
+
}
|
|
176
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
177
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
178
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
179
|
+
return true;
|
|
180
|
+
return existsSync(join2(resolved, DEFAULT_SQLITE_FILENAME)) || existsSync(join2(resolved, GLOBAL_CONFIG_FILENAME));
|
|
181
|
+
}
|
|
182
|
+
function exactDataRoot() {
|
|
183
|
+
for (const key of [DATA_DIR_ENV, HASNA_SKILLS_HOME_ENV, SKILLS_HOME_ENV]) {
|
|
184
|
+
const dir = process.env[key]?.trim();
|
|
185
|
+
if (dir)
|
|
186
|
+
return resolve(dir);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
function hasExactOverride(env = process.env) {
|
|
191
|
+
return Boolean(env[DATA_DIR_ENV]?.trim()) || Boolean(env[HASNA_SKILLS_HOME_ENV]?.trim()) || Boolean(env[SKILLS_HOME_ENV]?.trim());
|
|
192
|
+
}
|
|
193
|
+
function hasOperatorOverride(env = process.env) {
|
|
194
|
+
return hasExactOverride(env) || Boolean(env.HASNA_DATA_HOME?.trim());
|
|
195
|
+
}
|
|
196
|
+
function getDataRoot() {
|
|
197
|
+
const exact = exactDataRoot();
|
|
198
|
+
if (exact)
|
|
199
|
+
return exact;
|
|
200
|
+
const resolved = resolverDataRoot();
|
|
201
|
+
return adoptResolverDataRoot(resolved) ? resolve(resolved) : resolve(legacyDataRoot());
|
|
202
|
+
}
|
|
203
|
+
// src/lib/config.ts
|
|
91
204
|
function mergeDirectoryContents(sourceDir, targetDir) {
|
|
92
|
-
if (!
|
|
205
|
+
if (!existsSync2(sourceDir))
|
|
93
206
|
return;
|
|
94
207
|
mkdirSync(targetDir, { recursive: true });
|
|
95
208
|
for (const entry of readdirSync(sourceDir)) {
|
|
96
|
-
const sourcePath =
|
|
97
|
-
const targetPath =
|
|
209
|
+
const sourcePath = join3(sourceDir, entry);
|
|
210
|
+
const targetPath = join3(targetDir, entry);
|
|
98
211
|
try {
|
|
99
212
|
const sourceStat = statSync(sourcePath);
|
|
100
213
|
if (sourceStat.isDirectory()) {
|
|
101
214
|
mergeDirectoryContents(sourcePath, targetPath);
|
|
102
215
|
continue;
|
|
103
216
|
}
|
|
104
|
-
if (!
|
|
217
|
+
if (!existsSync2(targetPath))
|
|
105
218
|
copyFileSync(sourcePath, targetPath);
|
|
106
219
|
} catch {}
|
|
107
220
|
}
|
|
108
221
|
}
|
|
109
|
-
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
110
222
|
function getDataDir() {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const oldDir = join(home, ".skills");
|
|
121
|
-
const oldConfigFile = join(home, ".skillsrc");
|
|
122
|
-
mkdirSync(newDir, { recursive: true });
|
|
223
|
+
const root = getDataRoot();
|
|
224
|
+
try {
|
|
225
|
+
mkdirSync(root, { recursive: true });
|
|
226
|
+
} catch {}
|
|
227
|
+
if (hasOperatorOverride())
|
|
228
|
+
return root;
|
|
229
|
+
const home = effectiveHome();
|
|
230
|
+
const oldDir = join3(home, ".skills");
|
|
231
|
+
const oldConfigFile = join3(home, ".skillsrc");
|
|
123
232
|
try {
|
|
124
|
-
mergeDirectoryContents(oldDir,
|
|
233
|
+
mergeDirectoryContents(oldDir, root);
|
|
125
234
|
} catch {}
|
|
126
|
-
if (
|
|
235
|
+
if (existsSync2(oldConfigFile) && !existsSync2(join3(root, "config.json"))) {
|
|
127
236
|
try {
|
|
128
|
-
copyFileSync(oldConfigFile,
|
|
237
|
+
copyFileSync(oldConfigFile, join3(root, "config.json"));
|
|
129
238
|
} catch {}
|
|
130
239
|
}
|
|
131
|
-
return
|
|
240
|
+
return root;
|
|
132
241
|
}
|
|
133
242
|
|
|
134
243
|
// src/server/database-url.ts
|
|
135
|
-
var
|
|
244
|
+
var DEFAULT_SQLITE_FILENAME2 = "server.db";
|
|
136
245
|
var SQLITE_MEMORY_PATH = ":memory:";
|
|
137
246
|
var POSTGRES_SCHEMES = new Set(["postgres", "postgresql"]);
|
|
138
247
|
var SQLITE_SCHEMES = new Set(["sqlite", "sqlite3", "file"]);
|
|
139
248
|
var MEMORY_SCHEMES = new Set(["memory"]);
|
|
140
249
|
var SQLITE_EXTENSIONS = [".db", ".sqlite", ".sqlite3", ".db3"];
|
|
141
250
|
function defaultSqlitePath() {
|
|
142
|
-
return
|
|
251
|
+
return join4(getDataDir(), DEFAULT_SQLITE_FILENAME2);
|
|
143
252
|
}
|
|
144
253
|
function resolveDatabaseTarget(raw) {
|
|
145
254
|
const value = raw?.trim();
|
|
@@ -166,7 +275,7 @@ function resolveDatabaseTarget(raw) {
|
|
|
166
275
|
throw new Error(`unsupported database scheme "${scheme}:". Supported: postgres://, postgresql://, sqlite:, file:, ` + `an absolute or relative path to a .db/.sqlite file, ":memory:", or "memory:" (non-durable, tests only). ` + `Leave the setting empty to use the default SQLite database at ${defaultSqlitePath()}.`);
|
|
167
276
|
}
|
|
168
277
|
if (looksLikeSqlitePath(value)) {
|
|
169
|
-
const path = isAbsolute(value) ? value :
|
|
278
|
+
const path = isAbsolute(value) ? value : join4(process.cwd(), value);
|
|
170
279
|
return { kind: "sqlite", path, durable: true, label: `sqlite (${path})` };
|
|
171
280
|
}
|
|
172
281
|
throw new Error(`could not resolve a database backend from "${value}". Use postgres://\u2026, sqlite:\u2026, an absolute or ` + `relative path ending in ${SQLITE_EXTENSIONS.join("/")}, ":memory:", or leave it empty for the ` + `default SQLite database at ${defaultSqlitePath()}.`);
|
|
@@ -190,7 +299,7 @@ function sqlitePathFromUrl(value, scheme) {
|
|
|
190
299
|
}
|
|
191
300
|
return rest.slice(2).replace(/^\/\/+/, "/");
|
|
192
301
|
}
|
|
193
|
-
return isAbsolute(rest) ? rest :
|
|
302
|
+
return isAbsolute(rest) ? rest : join4(process.cwd(), rest);
|
|
194
303
|
}
|
|
195
304
|
function looksLikeSqlitePath(value) {
|
|
196
305
|
if (value.includes("/"))
|
|
@@ -199,16 +308,16 @@ function looksLikeSqlitePath(value) {
|
|
|
199
308
|
}
|
|
200
309
|
|
|
201
310
|
// src/server/migrations-dir.ts
|
|
202
|
-
import { existsSync as
|
|
203
|
-
import { dirname as dirname2, join as
|
|
311
|
+
import { existsSync as existsSync3 } from "fs";
|
|
312
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
204
313
|
var MIGRATION_DIALECTS = ["postgres", "sqlite"];
|
|
205
314
|
var MAX_WALK_UP = 6;
|
|
206
315
|
function findMigrationsRoot(startDirs = defaultStartDirs()) {
|
|
207
316
|
for (const start of startDirs) {
|
|
208
317
|
let dir = start;
|
|
209
318
|
for (let level = 0;level < MAX_WALK_UP; level += 1) {
|
|
210
|
-
const candidate =
|
|
211
|
-
if (MIGRATION_DIALECTS.some((dialect) =>
|
|
319
|
+
const candidate = join5(dir, "migrations");
|
|
320
|
+
if (MIGRATION_DIALECTS.some((dialect) => existsSync3(join5(candidate, dialect))))
|
|
212
321
|
return candidate;
|
|
213
322
|
const parent = dirname2(dir);
|
|
214
323
|
if (parent === dir)
|
|
@@ -222,8 +331,8 @@ function resolveMigrationsDir(dialect, root = findMigrationsRoot()) {
|
|
|
222
331
|
if (!root) {
|
|
223
332
|
throw new Error(`could not locate the migrations directory for dialect "${dialect}". ` + `Expected a migrations/${dialect}/ folder alongside the package root.`);
|
|
224
333
|
}
|
|
225
|
-
const dir =
|
|
226
|
-
if (!
|
|
334
|
+
const dir = join5(root, dialect);
|
|
335
|
+
if (!existsSync3(dir)) {
|
|
227
336
|
throw new Error(`migrations directory not found: ${dir}`);
|
|
228
337
|
}
|
|
229
338
|
return dir;
|
|
@@ -240,7 +349,7 @@ function defaultStartDirs() {
|
|
|
240
349
|
import { Database } from "bun:sqlite";
|
|
241
350
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
242
351
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
243
|
-
import { dirname as dirname3, join as
|
|
352
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
244
353
|
|
|
245
354
|
// src/server/auth.ts
|
|
246
355
|
import { createHash } from "crypto";
|
|
@@ -1119,7 +1228,7 @@ function applySqliteMigrations(db, migrationsDir = resolveMigrationsDir("sqlite"
|
|
|
1119
1228
|
const appliedNow = [];
|
|
1120
1229
|
for (const file of files) {
|
|
1121
1230
|
const version = file.replace(/\.sql$/, "");
|
|
1122
|
-
const text = readFileSync2(
|
|
1231
|
+
const text = readFileSync2(join6(migrationsDir, file), "utf8");
|
|
1123
1232
|
const apply = db.transaction(() => {
|
|
1124
1233
|
const already = db.query("SELECT 1 AS present FROM schema_migrations WHERE version = ? LIMIT 1").get(version);
|
|
1125
1234
|
if (already)
|
|
@@ -1192,7 +1301,7 @@ async function runMigrations(databaseUrl, migrationsDir) {
|
|
|
1192
1301
|
return { backend: "postgres", applied: await runPostgresMigrations(target.url, migrationsDir ?? resolveMigrationsDir("postgres")) };
|
|
1193
1302
|
}
|
|
1194
1303
|
async function runPostgresMigrations(databaseUrl, migrationsDir) {
|
|
1195
|
-
if (!
|
|
1304
|
+
if (!existsSync4(migrationsDir))
|
|
1196
1305
|
throw new Error(`migrations directory not found: ${migrationsDir}`);
|
|
1197
1306
|
const bunWithSql = Bun;
|
|
1198
1307
|
const sql = new bunWithSql.SQL(databaseUrl, { max: 1 });
|
|
@@ -1208,7 +1317,7 @@ async function runPostgresMigrations(databaseUrl, migrationsDir) {
|
|
|
1208
1317
|
const version = file.replace(/\.sql$/, "");
|
|
1209
1318
|
if (applied.has(version))
|
|
1210
1319
|
continue;
|
|
1211
|
-
const sqlText = readFileSync3(
|
|
1320
|
+
const sqlText = readFileSync3(join7(migrationsDir, file), "utf8");
|
|
1212
1321
|
await sql.unsafe("BEGIN");
|
|
1213
1322
|
try {
|
|
1214
1323
|
await sql.unsafe(sqlText);
|