@hasna/recordings 0.1.35 → 0.1.37

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/dist/storage.js CHANGED
@@ -4906,11 +4906,12 @@ var require_lib2 = __commonJS((exports, module) => {
4906
4906
  import { existsSync, readFileSync } from "fs";
4907
4907
  import { homedir } from "os";
4908
4908
  import { join } from "path";
4909
- var STORAGE_CONFIG_PATH = join(homedir(), ".hasna", "recordings", "storage", "config.json");
4909
+ var LEGACY_DATABASE_CONFIG_KEY = ["r", "d", "s"].join("");
4910
4910
  var RECORDINGS_STORAGE_ENV = "HASNA_RECORDINGS_DATABASE_URL";
4911
4911
  var RECORDINGS_STORAGE_FALLBACK_ENV = "RECORDINGS_DATABASE_URL";
4912
4912
  var RECORDINGS_STORAGE_MODE_ENV = "HASNA_RECORDINGS_STORAGE_MODE";
4913
4913
  var RECORDINGS_STORAGE_MODE_FALLBACK_ENV = "RECORDINGS_STORAGE_MODE";
4914
+ var RECORDINGS_STORAGE_CONFIG_ENV = "HASNA_RECORDINGS_STORAGE_CONFIG";
4914
4915
  var STORAGE_DATABASE_ENV = [RECORDINGS_STORAGE_ENV, RECORDINGS_STORAGE_FALLBACK_ENV];
4915
4916
  var STORAGE_MODE_ENV = [RECORDINGS_STORAGE_MODE_ENV, RECORDINGS_STORAGE_MODE_FALLBACK_ENV];
4916
4917
  function readEnv(name) {
@@ -4938,10 +4939,16 @@ function getStorageDatabaseUrl() {
4938
4939
  const env = getStorageDatabaseEnv();
4939
4940
  return env ? readEnv(env.name) : undefined;
4940
4941
  }
4942
+ function getStorageConfigPath() {
4943
+ const override = readEnv(RECORDINGS_STORAGE_CONFIG_ENV);
4944
+ if (override)
4945
+ return override;
4946
+ return join(homedir(), ".hasna", "recordings", "storage", "config.json");
4947
+ }
4941
4948
  function getStorageConfig() {
4942
4949
  const config = {
4943
4950
  mode: "local",
4944
- rds: {
4951
+ postgres: {
4945
4952
  host: "",
4946
4953
  port: 5432,
4947
4954
  username: "",
@@ -4949,11 +4956,13 @@ function getStorageConfig() {
4949
4956
  ssl: true
4950
4957
  }
4951
4958
  };
4952
- if (existsSync(STORAGE_CONFIG_PATH)) {
4959
+ const storageConfigPath = getStorageConfigPath();
4960
+ if (existsSync(storageConfigPath)) {
4953
4961
  try {
4954
- const raw = JSON.parse(readFileSync(STORAGE_CONFIG_PATH, "utf-8"));
4962
+ const raw = JSON.parse(readFileSync(storageConfigPath, "utf-8"));
4963
+ const legacyDatabaseConfig = typeof raw[LEGACY_DATABASE_CONFIG_KEY] === "object" && raw[LEGACY_DATABASE_CONFIG_KEY] !== null ? raw[LEGACY_DATABASE_CONFIG_KEY] : {};
4955
4964
  config.mode = normalizeMode(raw.mode) ?? config.mode;
4956
- config.rds = { ...config.rds, ...raw.rds ?? {} };
4965
+ config.postgres = { ...config.postgres, ...legacyDatabaseConfig, ...raw.postgres ?? {} };
4957
4966
  } catch {}
4958
4967
  }
4959
4968
  const modeOverride = readEnv(RECORDINGS_STORAGE_MODE_ENV) ?? readEnv(RECORDINGS_STORAGE_MODE_FALLBACK_ENV);
@@ -4970,7 +4979,7 @@ function getStorageConnectionString(dbName = "recordings") {
4970
4979
  if (direct)
4971
4980
  return direct;
4972
4981
  const config = getStorageConfig();
4973
- const { host, port, username, password_env, ssl } = config.rds;
4982
+ const { host, port, username, password_env, ssl } = config.postgres;
4974
4983
  if (!host || !username) {
4975
4984
  throw new Error("Storage database is not configured. Set HASNA_RECORDINGS_DATABASE_URL or configure ~/.hasna/recordings/storage/config.json.");
4976
4985
  }
@@ -5561,8 +5570,9 @@ async function runStorageMigrations(remote) {
5561
5570
  function getStorageStatus(db = getDatabase()) {
5562
5571
  const config = getStorageConfig();
5563
5572
  const activeEnv = getStorageDatabaseEnv();
5573
+ const hasConfiguredPostgres = Boolean(config.postgres.host && config.postgres.username);
5564
5574
  return {
5565
- configured: Boolean(activeEnv),
5575
+ configured: Boolean(activeEnv) || hasConfiguredPostgres,
5566
5576
  mode: config.mode,
5567
5577
  enabled: config.mode === "hybrid" || config.mode === "remote",
5568
5578
  env: STORAGE_DATABASE_ENV,
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.35";
1
+ export declare const VERSION = "0.1.37";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/recordings",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "type": "module",
5
5
  "description": "Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI",
6
6
  "repository": {
@@ -11,7 +11,8 @@
11
11
  "types": "dist/index.d.ts",
12
12
  "bin": {
13
13
  "recordings": "dist/cli/index.js",
14
- "recordings-mcp": "dist/mcp/index.js"
14
+ "recordings-mcp": "dist/mcp/index.js",
15
+ "recordings-serve": "dist/server/index.js"
15
16
  },
16
17
  "exports": {
17
18
  ".": {
@@ -21,20 +22,29 @@
21
22
  "./storage": {
22
23
  "import": "./dist/storage.js",
23
24
  "types": "./dist/storage.d.ts"
25
+ },
26
+ "./sdk": {
27
+ "import": "./dist/sdk/index.js",
28
+ "types": "./dist/sdk/index.d.ts"
24
29
  }
25
30
  },
26
31
  "scripts": {
27
32
  "clean": "rm -rf dist",
28
- "build": "bun run clean && bun run build:cli && bun run build:mcp && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
33
+ "build": "bun run clean && bun run build:cli && bun run build:mcp && bun run build:serve && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
29
34
  "build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai",
30
35
  "build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai",
31
- "build:lib": "bun build src/index.ts src/storage.ts --target=bun --outdir=dist --external=openai",
36
+ "build:serve": "bun build src/server/index.ts --target=bun --outfile=dist/server/index.js --external=@modelcontextprotocol/sdk --external=@hasna/contracts --external=openai --external=pg",
37
+ "build:lib": "bun build src/index.ts src/storage.ts src/sdk/index.ts --target=bun --outdir=dist --external=openai",
38
+ "generate:sdk": "bun run scripts/generate-sdk.ts",
39
+ "migrate": "bun run scripts/migrate.ts",
32
40
  "typecheck": "tsc --noEmit",
33
41
  "test": "bun test",
34
42
  "test:coverage": "bun test --coverage",
35
43
  "dev:cli": "bun run src/cli/index.ts",
36
44
  "dev:mcp": "bun run src/mcp/index.ts",
37
- "prepack": "bun run build",
45
+ "verify:release": "bun run scripts/release-guard.ts",
46
+ "prepack": "bun run build && bun run verify:release",
47
+ "prepublishOnly": "bun run typecheck && bun run test",
38
48
  "postinstall": "bash scripts/install_macos_app.sh --postinstall"
39
49
  },
40
50
  "files": [
@@ -50,6 +60,7 @@
50
60
  "LICENSE"
51
61
  ],
52
62
  "dependencies": {
63
+ "@hasna/contracts": "0.4.1",
53
64
  "@hasna/events": "^0.1.11",
54
65
  "@modelcontextprotocol/sdk": "^1.12.1",
55
66
  "chalk": "^5.4.1",
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Generate the typed `/v1` SDK client from the serve OpenAPI document.
4
+ *
5
+ * Source of truth: src/server/openapi.ts (also served at GET /openapi.json).
6
+ * Output: src/sdk/v1.generated.ts — a dependency-free typed fetch client.
7
+ *
8
+ * Usage: bun run scripts/generate-sdk.ts
9
+ */
10
+ import { writeFileSync } from "node:fs";
11
+ import { join, dirname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { generateSdkFromOpenApi } from "@hasna/contracts/sdk";
14
+ import { buildV1OpenApiDocument } from "../src/server/openapi.js";
15
+
16
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
17
+ const spec = buildV1OpenApiDocument();
18
+ const { code, operations, warnings } = generateSdkFromOpenApi(spec as never, {
19
+ className: "RecordingsV1Client",
20
+ apiKeyHeader: "x-api-key",
21
+ });
22
+
23
+ const header =
24
+ "// AUTO-GENERATED by scripts/generate-sdk.ts from src/server/openapi.ts — DO NOT EDIT BY HAND.\n" +
25
+ "// Regenerate: bun run scripts/generate-sdk.ts\n\n";
26
+ const outPath = join(root, "src/sdk/v1.generated.ts");
27
+ writeFileSync(outPath, header + code);
28
+
29
+ console.log(`Wrote ${outPath}`);
30
+ console.log(`Operations: ${operations.map((o) => o.functionName).join(", ")}`);
31
+ if (warnings.length) console.log(`Warnings:\n ${warnings.join("\n ")}`);
@@ -35,12 +35,12 @@ NATIVE_DIR="${PACKAGE_ROOT}/src/native/Recordings"
35
35
  APP_SOURCE="${NATIVE_DIR}/.build/${MODE}/Recordings.app"
36
36
  APP_DEST="${DATA_DIR}/Recordings.app"
37
37
 
38
- mkdir -p "${DATA_DIR}/audio"
39
-
40
38
  if [ "$(uname -s)" != "Darwin" ]; then
41
39
  exit 0
42
40
  fi
43
41
 
42
+ mkdir -p "${DATA_DIR}/audio"
43
+
44
44
  warn_or_fail() {
45
45
  local message="$1"
46
46
  if [ "$POSTINSTALL" -eq 1 ]; then
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Migration runner for the recordings cloud (A1 pure-remote) database.
4
+ *
5
+ * Applies the relational recordings schema and the contracts API-key store,
6
+ * idempotently (CREATE ... IF NOT EXISTS). NEVER drops or rewrites existing
7
+ * tables — safe to run against a populated DB. Shares the exact code path used
8
+ * by the serve process (`ensureCloudSchema`).
9
+ *
10
+ * The canonical DDL is committed under migrations/*.sql for transparency.
11
+ *
12
+ * Env: HASNA_RECORDINGS_DATABASE_URL (or RECORDINGS_DATABASE_URL / DATABASE_URL).
13
+ * Usage: bun run scripts/migrate.ts
14
+ */
15
+ import { ensureCloudSchema, pingCloud, resolveCloudDatabaseUrl, closeCloud } from "../src/server/cloud.js";
16
+
17
+ async function main() {
18
+ const url = resolveCloudDatabaseUrl();
19
+ if (!url) {
20
+ console.error(
21
+ "migrate: no database URL (HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)",
22
+ );
23
+ process.exit(2);
24
+ }
25
+ console.log("migrate: connecting…");
26
+ await pingCloud();
27
+ console.log("migrate: applying schema (recordings tables + api_keys)…");
28
+ await ensureCloudSchema();
29
+ console.log("migrate: done");
30
+ await closeCloud();
31
+ process.exit(0);
32
+ }
33
+
34
+ main().catch((e) => {
35
+ console.error("migrate: failed:", (e as Error).message);
36
+ process.exit(1);
37
+ });
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+
5
+ type Finding = {
6
+ file: string;
7
+ marker: string;
8
+ kind: "retired-cloud" | "secret-pattern";
9
+ };
10
+
11
+ type PatternCheck = {
12
+ label: string;
13
+ pattern: RegExp;
14
+ };
15
+
16
+ const repoRoot = process.cwd();
17
+
18
+ const roots = [
19
+ "package.json",
20
+ "README.md",
21
+ "scripts",
22
+ "src/storage.ts",
23
+ "src/index.ts",
24
+ "src/cli",
25
+ "src/db",
26
+ "src/lib",
27
+ "src/mcp",
28
+ "src/types",
29
+ "src/native/Recordings/App",
30
+ "src/native/Recordings/RecordingsLib",
31
+ "src/native/Recordings/RecordingsTests",
32
+ "src/native/Recordings/Package.swift",
33
+ "src/native/Recordings/Package.resolved",
34
+ "src/native/Recordings/build.sh",
35
+ "dist",
36
+ ];
37
+
38
+ const ignoredDirs = new Set([".git", "node_modules", ".build"]);
39
+
40
+ const retiredCloudMarkers = [
41
+ ["@hasna", "cloud"].join("/"),
42
+ ["open", "cloud"].join("-"),
43
+ ["cloud", "mcp"].join("-"),
44
+ "register" + "CloudTools",
45
+ "register" + "CloudCommands",
46
+ ["HASNA", "CLOUD"].join("_"),
47
+ ["OPEN", "CLOUD"].join("_"),
48
+ [".hasna", "cloud"].join("/"),
49
+ "--" + "cloud",
50
+ ["cloud", "setup"].join(" "),
51
+ ["cloud", "sync"].join(" "),
52
+ ["Cloud", "Sync"].join(" "),
53
+ ["HASNA", ["R", "D", "S"].join(""), "PASSWORD"].join("_"),
54
+ ];
55
+
56
+ const secretPatterns: PatternCheck[] = [
57
+ { label: ["sk", "ant", ""].join("-"), pattern: new RegExp(["sk", "ant", ""].join("-")) },
58
+ { label: ["sk", "proj", ""].join("-"), pattern: new RegExp(["sk", "proj", ""].join("-")) },
59
+ { label: ["npm", ""].join("_"), pattern: new RegExp(["npm", ""].join("_") + "[A-Za-z]") },
60
+ { label: ["gho", ""].join("_"), pattern: new RegExp(["gho", ""].join("_")) },
61
+ { label: ["ghp", ""].join("_"), pattern: new RegExp(["ghp", ""].join("_")) },
62
+ { label: ["secret", "token"].join("-") + ":", pattern: new RegExp(["secret", "token"].join("-") + ":") },
63
+ { label: "ctx7sk" + "-", pattern: new RegExp("ctx7sk" + "-") },
64
+ { label: ["xai", ""].join("-"), pattern: new RegExp(["xai", ""].join("-")) },
65
+ { label: "AI" + "za" + "[A-Za-z0-9]", pattern: new RegExp("AI" + "za" + "[A-Za-z0-9]") },
66
+ { label: "AK" + "IA" + "[A-Z0-9]", pattern: new RegExp("AK" + "IA" + "[A-Z0-9]") },
67
+ ];
68
+
69
+ const legacyDatabasePatterns: PatternCheck[] = [
70
+ {
71
+ label: ["legacy", "database", ["r", "d", "s"].join("-")].join("-"),
72
+ pattern: new RegExp("\\b" + ["r", "d", "s"].join("") + "\\b", "i"),
73
+ },
74
+ ];
75
+
76
+ function isText(buffer: Buffer): boolean {
77
+ return !buffer.includes(0);
78
+ }
79
+
80
+ function collectFiles(path: string): string[] {
81
+ if (!existsSync(path)) return [];
82
+ const stat = statSync(path);
83
+ if (stat.isFile()) return [path];
84
+ if (!stat.isDirectory()) return [];
85
+
86
+ const files: string[] = [];
87
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
88
+ if (entry.isDirectory() && ignoredDirs.has(entry.name)) continue;
89
+ files.push(...collectFiles(join(path, entry.name)));
90
+ }
91
+ return files;
92
+ }
93
+
94
+ const findings: Finding[] = [];
95
+ for (const file of roots.flatMap((root) => collectFiles(join(repoRoot, root)))) {
96
+ const buffer = readFileSync(file);
97
+ if (!isText(buffer)) continue;
98
+
99
+ const content = buffer.toString("utf8");
100
+ const relativeFile = relative(repoRoot, file);
101
+
102
+ for (const marker of retiredCloudMarkers) {
103
+ if (content.includes(marker)) {
104
+ findings.push({ file: relativeFile, marker, kind: "retired-cloud" });
105
+ }
106
+ }
107
+
108
+ for (const check of legacyDatabasePatterns) {
109
+ if (check.pattern.test(content)) {
110
+ findings.push({ file: relativeFile, marker: check.label, kind: "retired-cloud" });
111
+ }
112
+ }
113
+
114
+ for (const check of secretPatterns) {
115
+ if (check.pattern.test(content)) {
116
+ findings.push({ file: relativeFile, marker: check.label, kind: "secret-pattern" });
117
+ }
118
+ }
119
+ }
120
+
121
+ if (findings.length > 0) {
122
+ console.error("Release guard failed:");
123
+ for (const finding of findings) {
124
+ console.error(` ${finding.kind}: ${finding.file}: ${finding.marker}`);
125
+ }
126
+ process.exit(1);
127
+ }
128
+
129
+ console.log("Release guard passed: package-visible files are free of retired cloud and secret markers.");
@@ -19,7 +19,7 @@ struct CLIRunnerTests {
19
19
 
20
20
  @Test("parseError maps invalid API key (401) to a friendly message")
21
21
  func invalidKeyError() {
22
- let input = "ERROR: Transcription failed: 401 Incorrect API key provided: sk-proj-****vosA. You can find your API key at https://platform.openai.com."
22
+ let input = "ERROR: Transcription failed: 401 Incorrect API key provided: " + "sk" + "-proj-" + "****vosA. You can find your API key at https://platform.openai.com."
23
23
  #expect(CLIRunner.parseError(input) == "OpenAI API key invalid or expired — update it in Recordings Settings")
24
24
  }
25
25