@hasna/recordings 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +191 -170
  2. package/README.md +16 -7
  3. package/dist/cli/index.js +114 -64
  4. package/dist/cli/storage.d.ts +3 -0
  5. package/dist/cli/storage.d.ts.map +1 -0
  6. package/dist/db/pg-migrations.d.ts +1 -1
  7. package/dist/db/storage-config.d.ts +27 -0
  8. package/dist/db/storage-config.d.ts.map +1 -0
  9. package/dist/db/storage-sync.d.ts +36 -0
  10. package/dist/db/storage-sync.d.ts.map +1 -0
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +110 -50
  14. package/dist/lib/enhancer.d.ts.map +1 -1
  15. package/dist/lib/transcriber.d.ts +1 -0
  16. package/dist/lib/transcriber.d.ts.map +1 -1
  17. package/dist/mcp/http.d.ts +13 -0
  18. package/dist/mcp/http.d.ts.map +1 -0
  19. package/dist/mcp/index.d.ts +2 -1
  20. package/dist/mcp/index.d.ts.map +1 -1
  21. package/dist/mcp/index.js +519 -408
  22. package/dist/mcp/storage-tools.d.ts +3 -0
  23. package/dist/mcp/storage-tools.d.ts.map +1 -0
  24. package/dist/storage.d.ts +7 -0
  25. package/dist/storage.d.ts.map +1 -0
  26. package/dist/storage.js +5656 -0
  27. package/dist/version.d.ts +1 -1
  28. package/package.json +8 -3
  29. package/scripts/install_macos_app.sh +17 -0
  30. package/src/native/Recordings/App/RecordingsApp.swift +3 -2
  31. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +101 -80
  32. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +26 -0
  33. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +13 -1
  34. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +92 -31
  35. package/src/native/Recordings/RecordingsLib/SettingsView.swift +5 -1
  36. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +12 -0
  37. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +49 -0
  38. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +54 -0
  39. package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +58 -0
  40. package/dist/cli/cloud.d.ts +0 -3
  41. package/dist/cli/cloud.d.ts.map +0 -1
  42. package/dist/db/cloud-config.d.ts +0 -14
  43. package/dist/db/cloud-config.d.ts.map +0 -1
  44. package/dist/db/cloud-sync.d.ts +0 -29
  45. package/dist/db/cloud-sync.d.ts.map +0 -1
  46. package/dist/mcp/cloud-tools.d.ts +0 -3
  47. package/dist/mcp/cloud-tools.d.ts.map +0 -1
package/dist/mcp/index.js CHANGED
@@ -4920,7 +4920,7 @@ var require_lib2 = __commonJS((exports, module) => {
4920
4920
  var require_package = __commonJS((exports, module) => {
4921
4921
  module.exports = {
4922
4922
  name: "@hasna/recordings",
4923
- version: "0.1.23",
4923
+ version: "0.1.24",
4924
4924
  type: "module",
4925
4925
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
4926
4926
  repository: {
@@ -4937,13 +4937,18 @@ var require_package = __commonJS((exports, module) => {
4937
4937
  ".": {
4938
4938
  import: "./dist/index.js",
4939
4939
  types: "./dist/index.d.ts"
4940
+ },
4941
+ "./storage": {
4942
+ import: "./dist/storage.js",
4943
+ types: "./dist/storage.d.ts"
4940
4944
  }
4941
4945
  },
4942
4946
  scripts: {
4943
- build: "bun run build:cli && bun run build:mcp && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
4947
+ clean: "rm -rf dist",
4948
+ build: "bun run clean && bun run build:cli && bun run build:mcp && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
4944
4949
  "build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai",
4945
4950
  "build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai",
4946
- "build:lib": "bun build src/index.ts --target=bun --outfile=dist/index.js --external=openai",
4951
+ "build:lib": "bun build src/index.ts src/storage.ts --target=bun --outdir=dist --external=openai",
4947
4952
  typecheck: "tsc --noEmit",
4948
4953
  test: "bun test",
4949
4954
  "test:coverage": "bun test --coverage",
@@ -4990,6 +4995,51 @@ var require_package = __commonJS((exports, module) => {
4990
4995
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4991
4996
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4992
4997
 
4998
+ // src/mcp/http.ts
4999
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
5000
+ var DEFAULT_MCP_HTTP_PORT = 8873;
5001
+ var MCP_HTTP_HOST = "127.0.0.1";
5002
+ function isStdioMode(args) {
5003
+ return args.includes("--stdio") || process.env.MCP_STDIO === "1";
5004
+ }
5005
+ function resolveMcpHttpPort(args) {
5006
+ const portIdx = args.indexOf("--port");
5007
+ if (portIdx >= 0 && args[portIdx + 1]) {
5008
+ return Number(args[portIdx + 1]);
5009
+ }
5010
+ const envPort = process.env.MCP_HTTP_PORT;
5011
+ if (envPort)
5012
+ return Number(envPort);
5013
+ return DEFAULT_MCP_HTTP_PORT;
5014
+ }
5015
+ async function handleMcpRequest(req, buildServer) {
5016
+ const transport = new WebStandardStreamableHTTPServerTransport({
5017
+ sessionIdGenerator: undefined
5018
+ });
5019
+ const server = buildServer();
5020
+ await server.connect(transport);
5021
+ return transport.handleRequest(req);
5022
+ }
5023
+ function startMcpHttpServer(options) {
5024
+ const { name, port, buildServer } = options;
5025
+ const server = Bun.serve({
5026
+ hostname: MCP_HTTP_HOST,
5027
+ port,
5028
+ async fetch(req) {
5029
+ const url = new URL(req.url);
5030
+ if (url.pathname === "/health" && req.method === "GET") {
5031
+ return Response.json({ status: "ok", name });
5032
+ }
5033
+ if (url.pathname === "/mcp") {
5034
+ return handleMcpRequest(req, buildServer);
5035
+ }
5036
+ return new Response("Not Found", { status: 404 });
5037
+ }
5038
+ });
5039
+ console.error(`${name}-mcp HTTP listening on http://${MCP_HTTP_HOST}:${port}/mcp`);
5040
+ return server;
5041
+ }
5042
+
4993
5043
  // node_modules/zod/v3/external.js
4994
5044
  var exports_external = {};
4995
5045
  __export(exports_external, {
@@ -9598,9 +9648,18 @@ async function transcribeAudio(audioPath, config, options = {}) {
9598
9648
  };
9599
9649
  } catch (error) {
9600
9650
  const msg = error instanceof Error ? error.message : String(error);
9601
- throw new TranscriptionError(`Transcription failed: ${msg}`);
9651
+ throw new TranscriptionError(`Transcription failed: ${describeTranscriptionFailure(msg)}`);
9602
9652
  }
9603
9653
  }
9654
+ function describeTranscriptionFailure(message) {
9655
+ if (/401|incorrect api key|invalid_api_key/i.test(message)) {
9656
+ return "OpenAI API key invalid or expired (401). Update it in ~/.hasna/recordings/config.json, the OPENAI_API_KEY env var, or the Recordings app Settings.";
9657
+ }
9658
+ if (/429|exceeded your current quota|insufficient_quota/i.test(message)) {
9659
+ return "OpenAI quota exceeded (429). Check the OpenAI account plan and billing.";
9660
+ }
9661
+ return message;
9662
+ }
9604
9663
  function buildVerbatimPrompt(context) {
9605
9664
  const base = "Transcribe the speaker's words verbatim. Output only words that were spoken. Do not summarize, paraphrase, rewrite, clean up grammar, add explanations, or infer missing words. Preserve names, acronyms, technical terms, punctuation, and casing when audible.";
9606
9665
  const trimmed = context?.trim();
@@ -9718,7 +9777,7 @@ ${systemPrompt}` : basePrompt;
9718
9777
  };
9719
9778
  } catch (error) {
9720
9779
  const msg = error instanceof Error ? error.message : String(error);
9721
- throw new EnhancementError(`Enhancement failed: ${msg}`);
9780
+ throw new EnhancementError(`Enhancement failed: ${describeTranscriptionFailure(msg)}`);
9722
9781
  }
9723
9782
  }
9724
9783
  async function processText(rawText, config, systemPrompt) {
@@ -9738,57 +9797,82 @@ async function processText(rawText, config, systemPrompt) {
9738
9797
  }
9739
9798
 
9740
9799
  // src/version.ts
9741
- var VERSION = "0.1.20";
9800
+ var VERSION = "0.1.24";
9742
9801
 
9743
- // src/db/cloud-config.ts
9802
+ // src/db/storage-config.ts
9744
9803
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
9745
9804
  import { homedir as homedir2 } from "os";
9746
9805
  import { join as join2 } from "path";
9747
- var CONFIG_PATH = join2(homedir2(), ".hasna", "recordings", "cloud", "config.json");
9748
- function isMode(value) {
9749
- return value === "local" || value === "hybrid" || value === "cloud";
9806
+ var STORAGE_CONFIG_PATH = join2(homedir2(), ".hasna", "recordings", "storage", "config.json");
9807
+ var RECORDINGS_STORAGE_ENV = "HASNA_RECORDINGS_DATABASE_URL";
9808
+ var RECORDINGS_STORAGE_FALLBACK_ENV = "RECORDINGS_DATABASE_URL";
9809
+ var RECORDINGS_STORAGE_MODE_ENV = "HASNA_RECORDINGS_STORAGE_MODE";
9810
+ var RECORDINGS_STORAGE_MODE_FALLBACK_ENV = "RECORDINGS_STORAGE_MODE";
9811
+ var STORAGE_DATABASE_ENV = [RECORDINGS_STORAGE_ENV, RECORDINGS_STORAGE_FALLBACK_ENV];
9812
+ function readEnv(name) {
9813
+ const value = process.env[name]?.trim();
9814
+ return value || undefined;
9815
+ }
9816
+ function normalizeMode(value) {
9817
+ const normalized = value?.trim().toLowerCase();
9818
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
9819
+ return normalized;
9820
+ return;
9821
+ }
9822
+ function getStorageDatabaseEnvName() {
9823
+ for (const name of STORAGE_DATABASE_ENV) {
9824
+ if (readEnv(name))
9825
+ return name;
9826
+ }
9827
+ return null;
9750
9828
  }
9751
- function envConnectionString() {
9752
- return process.env["HASNA_RECORDINGS_CLOUD_DATABASE_URL"] ?? process.env["OPEN_RECORDINGS_CLOUD_DATABASE_URL"] ?? process.env["RECORDINGS_CLOUD_DATABASE_URL"];
9829
+ function getStorageDatabaseEnv() {
9830
+ const name = getStorageDatabaseEnvName();
9831
+ return name ? { name } : null;
9753
9832
  }
9754
- function getCloudConfig() {
9833
+ function getStorageDatabaseUrl() {
9834
+ const env = getStorageDatabaseEnv();
9835
+ return env ? readEnv(env.name) : undefined;
9836
+ }
9837
+ function getStorageConfig() {
9755
9838
  const config = {
9756
9839
  mode: "local",
9757
9840
  rds: {
9758
9841
  host: "",
9759
9842
  port: 5432,
9760
9843
  username: "",
9761
- password_env: "RECORDINGS_CLOUD_DATABASE_PASSWORD",
9844
+ password_env: "RECORDINGS_DATABASE_PASSWORD",
9762
9845
  ssl: true
9763
9846
  }
9764
9847
  };
9765
- if (existsSync2(CONFIG_PATH)) {
9848
+ if (existsSync2(STORAGE_CONFIG_PATH)) {
9766
9849
  try {
9767
- const raw = JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
9768
- config.mode = raw.mode ?? config.mode;
9850
+ const raw = JSON.parse(readFileSync2(STORAGE_CONFIG_PATH, "utf-8"));
9851
+ config.mode = normalizeMode(raw.mode) ?? config.mode;
9769
9852
  config.rds = { ...config.rds, ...raw.rds ?? {} };
9770
9853
  } catch {}
9771
9854
  }
9772
- const modeOverride = process.env["HASNA_RECORDINGS_CLOUD_MODE"] ?? process.env["OPEN_RECORDINGS_CLOUD_MODE"] ?? process.env["RECORDINGS_CLOUD_MODE"];
9773
- if (isMode(modeOverride)) {
9774
- config.mode = modeOverride;
9775
- } else if (envConnectionString() && config.mode === "local") {
9855
+ const modeOverride = readEnv(RECORDINGS_STORAGE_MODE_ENV) ?? readEnv(RECORDINGS_STORAGE_MODE_FALLBACK_ENV);
9856
+ const normalizedMode = normalizeMode(modeOverride);
9857
+ if (normalizedMode) {
9858
+ config.mode = normalizedMode;
9859
+ } else if (getStorageDatabaseUrl() && config.mode === "local") {
9776
9860
  config.mode = "hybrid";
9777
9861
  }
9778
9862
  return config;
9779
9863
  }
9780
- function getConnectionString(dbName = "recordings") {
9781
- const direct = envConnectionString();
9864
+ function getStorageConnectionString(dbName = "recordings") {
9865
+ const direct = getStorageDatabaseUrl();
9782
9866
  if (direct)
9783
9867
  return direct;
9784
- const config = getCloudConfig();
9868
+ const config = getStorageConfig();
9785
9869
  const { host, port, username, password_env, ssl } = config.rds;
9786
9870
  if (!host || !username) {
9787
- throw new Error("Cloud database is not configured. Set HASNA_RECORDINGS_CLOUD_DATABASE_URL or configure ~/.hasna/recordings/cloud/config.json.");
9871
+ throw new Error("Storage database is not configured. Set HASNA_RECORDINGS_DATABASE_URL or configure ~/.hasna/recordings/storage/config.json.");
9788
9872
  }
9789
9873
  const password = process.env[password_env];
9790
9874
  if (!password) {
9791
- throw new Error(`Cloud database password is not set. Export ${password_env}.`);
9875
+ throw new Error(`Storage database password is not set. Export ${password_env}.`);
9792
9876
  }
9793
9877
  const sslParam = ssl ? "?sslmode=require" : "";
9794
9878
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
@@ -9915,8 +9999,8 @@ var PG_MIGRATIONS = [
9915
9999
  `ALTER TABLE agents ADD COLUMN IF NOT EXISTS active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`
9916
10000
  ];
9917
10001
 
9918
- // src/db/cloud-sync.ts
9919
- var CLOUD_TABLES = [
10002
+ // src/db/storage-sync.ts
10003
+ var STORAGE_TABLES = [
9920
10004
  "projects",
9921
10005
  "agents",
9922
10006
  "recordings",
@@ -9975,21 +10059,26 @@ function upsertSqlite(db, table, rows) {
9975
10059
  }
9976
10060
  return written;
9977
10061
  }
9978
- async function getCloudPg() {
9979
- return new PgAdapterAsync(getConnectionString("recordings"));
10062
+ async function getStoragePg() {
10063
+ return new PgAdapterAsync(getStorageConnectionString("recordings"));
9980
10064
  }
9981
- async function runCloudMigrations(remote) {
10065
+ async function runStorageMigrations(remote) {
9982
10066
  for (const migration of PG_MIGRATIONS) {
9983
10067
  await remote.exec(migration);
9984
10068
  }
9985
10069
  }
9986
- function getCloudStatus(db = getDatabase()) {
9987
- const config = getCloudConfig();
10070
+ function getStorageStatus(db = getDatabase()) {
10071
+ const config = getStorageConfig();
10072
+ const activeEnv = getStorageDatabaseEnv();
9988
10073
  return {
10074
+ configured: Boolean(activeEnv),
9989
10075
  mode: config.mode,
9990
- enabled: config.mode === "hybrid" || config.mode === "cloud",
10076
+ enabled: config.mode === "hybrid" || config.mode === "remote",
10077
+ env: STORAGE_DATABASE_ENV,
10078
+ activeEnv: activeEnv?.name ?? null,
10079
+ service: "recordings",
9991
10080
  db_path: getDbPath(),
9992
- tables: CLOUD_TABLES.map((table) => {
10081
+ tables: STORAGE_TABLES.map((table) => {
9993
10082
  try {
9994
10083
  const row = db.query(`SELECT COUNT(*) as count FROM ${quoteId(table)}`).get();
9995
10084
  return { table, rows: row.count };
@@ -9999,12 +10088,12 @@ function getCloudStatus(db = getDatabase()) {
9999
10088
  })
10000
10089
  };
10001
10090
  }
10002
- async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
10091
+ async function pushStorageChanges(tables = [...STORAGE_TABLES]) {
10003
10092
  const db = getDatabase();
10004
- const remote = await getCloudPg();
10093
+ const remote = await getStoragePg();
10005
10094
  const results = [];
10006
10095
  try {
10007
- await runCloudMigrations(remote);
10096
+ await runStorageMigrations(remote);
10008
10097
  for (const table of tables) {
10009
10098
  const result = { table, direction: "push", rows_read: 0, rows_written: 0, errors: [] };
10010
10099
  try {
@@ -10021,12 +10110,12 @@ async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
10021
10110
  }
10022
10111
  return results;
10023
10112
  }
10024
- async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
10113
+ async function pullStorageChanges(tables = [...STORAGE_TABLES]) {
10025
10114
  const db = getDatabase();
10026
- const remote = await getCloudPg();
10115
+ const remote = await getStoragePg();
10027
10116
  const results = [];
10028
10117
  try {
10029
- await runCloudMigrations(remote);
10118
+ await runStorageMigrations(remote);
10030
10119
  for (const table of tables) {
10031
10120
  const result = { table, direction: "pull", rows_read: 0, rows_written: 0, errors: [] };
10032
10121
  try {
@@ -10043,20 +10132,26 @@ async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
10043
10132
  }
10044
10133
  return results;
10045
10134
  }
10046
- async function syncCloudChanges(tables = [...CLOUD_TABLES]) {
10135
+ async function syncStorageChanges(tables = [...STORAGE_TABLES]) {
10047
10136
  return {
10048
- push: await pushCloudChanges(tables),
10049
- pull: await pullCloudChanges(tables)
10137
+ push: await pushStorageChanges(tables),
10138
+ pull: await pullStorageChanges(tables)
10050
10139
  };
10051
10140
  }
10052
- function parseCloudTables(raw) {
10141
+ function parseStorageTables(raw) {
10053
10142
  if (!raw)
10054
- return [...CLOUD_TABLES];
10143
+ return [...STORAGE_TABLES];
10055
10144
  const requested = raw.split(",").map((table) => table.trim()).filter(Boolean);
10056
- return requested.length > 0 ? requested : [...CLOUD_TABLES];
10145
+ if (requested.length === 0)
10146
+ return [...STORAGE_TABLES];
10147
+ const allowed = new Set(STORAGE_TABLES);
10148
+ const invalid = requested.filter((table) => !allowed.has(table));
10149
+ if (invalid.length > 0)
10150
+ throw new Error(`Unknown recordings sync table(s): ${invalid.join(", ")}`);
10151
+ return requested;
10057
10152
  }
10058
10153
 
10059
- // src/mcp/cloud-tools.ts
10154
+ // src/mcp/storage-tools.ts
10060
10155
  function text(value) {
10061
10156
  return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
10062
10157
  }
@@ -10066,42 +10161,42 @@ function errorText(error) {
10066
10161
  isError: true
10067
10162
  };
10068
10163
  }
10069
- function registerRecordingsCloudTools(server) {
10070
- server.tool("recordings_cloud_status", "Show recordings local database and cloud sync status", {}, async () => {
10164
+ function registerRecordingsStorageTools(server) {
10165
+ server.tool("recordings_storage_status", "Show recordings local database and storage sync status", {}, async () => {
10071
10166
  try {
10072
- return text(getCloudStatus());
10167
+ return text(getStorageStatus());
10073
10168
  } catch (error) {
10074
10169
  return errorText(error);
10075
10170
  }
10076
10171
  });
10077
- server.tool("recordings_cloud_push", "Push local recordings data to PostgreSQL", {
10172
+ server.tool("recordings_storage_push", "Push local recordings data to PostgreSQL", {
10078
10173
  tables: exports_external.string().optional().describe("Comma-separated table names")
10079
10174
  }, async ({ tables }) => {
10080
10175
  try {
10081
- return text(await pushCloudChanges(parseCloudTables(tables)));
10176
+ return text(await pushStorageChanges(parseStorageTables(tables)));
10082
10177
  } catch (error) {
10083
10178
  return errorText(error);
10084
10179
  }
10085
10180
  });
10086
- server.tool("recordings_cloud_pull", "Pull PostgreSQL recordings data into the local database", {
10181
+ server.tool("recordings_storage_pull", "Pull PostgreSQL recordings data into the local database", {
10087
10182
  tables: exports_external.string().optional().describe("Comma-separated table names")
10088
10183
  }, async ({ tables }) => {
10089
10184
  try {
10090
- return text(await pullCloudChanges(parseCloudTables(tables)));
10185
+ return text(await pullStorageChanges(parseStorageTables(tables)));
10091
10186
  } catch (error) {
10092
10187
  return errorText(error);
10093
10188
  }
10094
10189
  });
10095
- server.tool("recordings_cloud_sync", "Push local changes, then pull remote changes", {
10190
+ server.tool("recordings_storage_sync", "Push local changes, then pull remote changes", {
10096
10191
  tables: exports_external.string().optional().describe("Comma-separated table names")
10097
10192
  }, async ({ tables }) => {
10098
10193
  try {
10099
- return text(await syncCloudChanges(parseCloudTables(tables)));
10194
+ return text(await syncStorageChanges(parseStorageTables(tables)));
10100
10195
  } catch (error) {
10101
10196
  return errorText(error);
10102
10197
  }
10103
10198
  });
10104
- server.tool("recordings_cloud_feedback", "Save feedback for recordings", {
10199
+ server.tool("recordings_storage_feedback", "Save feedback for recordings", {
10105
10200
  message: exports_external.string(),
10106
10201
  email: exports_external.string().optional(),
10107
10202
  category: exports_external.enum(["bug", "feature", "general"]).optional()
@@ -10120,382 +10215,398 @@ function registerRecordingsCloudTools(server) {
10120
10215
  var config = loadConfig();
10121
10216
  ensureDataDir(config);
10122
10217
  getDatabase(config.db_path);
10123
- var server = new McpServer({
10124
- name: "recordings",
10125
- version: VERSION
10126
- });
10127
- var registerTool = server.tool.bind(server);
10128
- function text2(content) {
10129
- return { content: [{ type: "text", text: content }] };
10130
- }
10131
- function errorResult(e) {
10132
- const msg = e instanceof Error ? e.message : String(e);
10133
- return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
10134
- }
10135
- function compact(r) {
10136
- const t = (r.processed_text || r.raw_text).slice(0, 80);
10137
- return `${r.id.slice(0, 8)} | ${r.processing_mode} | ${r.created_at.slice(0, 16)} | ${t}${t.length >= 80 ? "..." : ""}`;
10138
- }
10139
- function full(r) {
10140
- const lines = [`ID: ${r.id}`, `Mode: ${r.processing_mode}`, `Model: ${r.model_used}`];
10141
- if (r.enhancement_model)
10142
- lines.push(`Enhanced by: ${r.enhancement_model}`);
10143
- if (r.duration_ms)
10144
- lines.push(`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
10145
- if (r.language)
10146
- lines.push(`Language: ${r.language}`);
10147
- if (r.tags.length > 0)
10148
- lines.push(`Tags: ${r.tags.join(", ")}`);
10149
- if (r.agent_id)
10150
- lines.push(`Agent: ${r.agent_id}`);
10151
- if (r.project_id)
10152
- lines.push(`Project: ${r.project_id}`);
10153
- if (r.session_id)
10154
- lines.push(`Session: ${r.session_id}`);
10155
- lines.push(`Created: ${r.created_at}`);
10156
- lines.push(`Text: ${r.raw_text}`);
10157
- if (r.processed_text && r.processed_text !== r.raw_text) {
10158
- lines.push(`Enhanced: ${r.processed_text}`);
10159
- }
10160
- return lines.join(`
10218
+ function buildServer() {
10219
+ const server = new McpServer({
10220
+ name: "recordings",
10221
+ version: VERSION
10222
+ });
10223
+ const registerTool = server.tool.bind(server);
10224
+ function text2(content) {
10225
+ return { content: [{ type: "text", text: content }] };
10226
+ }
10227
+ function errorResult(e) {
10228
+ const msg = e instanceof Error ? e.message : String(e);
10229
+ return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
10230
+ }
10231
+ function compact(r) {
10232
+ const t = (r.processed_text || r.raw_text).slice(0, 80);
10233
+ return `${r.id.slice(0, 8)} | ${r.processing_mode} | ${r.created_at.slice(0, 16)} | ${t}${t.length >= 80 ? "..." : ""}`;
10234
+ }
10235
+ function full(r) {
10236
+ const lines = [`ID: ${r.id}`, `Mode: ${r.processing_mode}`, `Model: ${r.model_used}`];
10237
+ if (r.enhancement_model)
10238
+ lines.push(`Enhanced by: ${r.enhancement_model}`);
10239
+ if (r.duration_ms)
10240
+ lines.push(`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
10241
+ if (r.language)
10242
+ lines.push(`Language: ${r.language}`);
10243
+ if (r.tags.length > 0)
10244
+ lines.push(`Tags: ${r.tags.join(", ")}`);
10245
+ if (r.agent_id)
10246
+ lines.push(`Agent: ${r.agent_id}`);
10247
+ if (r.project_id)
10248
+ lines.push(`Project: ${r.project_id}`);
10249
+ if (r.session_id)
10250
+ lines.push(`Session: ${r.session_id}`);
10251
+ lines.push(`Created: ${r.created_at}`);
10252
+ lines.push(`Text: ${r.raw_text}`);
10253
+ if (r.processed_text && r.processed_text !== r.raw_text) {
10254
+ lines.push(`Enhanced: ${r.processed_text}`);
10255
+ }
10256
+ return lines.join(`
10161
10257
  `);
10162
- }
10163
- async function saveRecordingMemento(args) {
10164
- try {
10165
- const proc = Bun.spawn([
10166
- "mementos",
10167
- "save",
10168
- "--scope",
10169
- "shared",
10170
- "--category",
10171
- "history",
10172
- "--importance",
10173
- "5",
10174
- "--tags",
10175
- "recording,transcription",
10176
- "--summary",
10177
- args.summary,
10178
- args.key,
10179
- args.value
10180
- ], {
10181
- stdout: "ignore",
10182
- stderr: "ignore"
10183
- });
10184
- await proc.exited;
10185
- } catch {}
10186
- }
10187
- var toolDocs = {
10188
- transcribe_audio: `Transcribe audio file. Auto-enhances if needed.
10258
+ }
10259
+ async function saveRecordingMemento(args) {
10260
+ try {
10261
+ const proc = Bun.spawn([
10262
+ "mementos",
10263
+ "save",
10264
+ "--scope",
10265
+ "shared",
10266
+ "--category",
10267
+ "history",
10268
+ "--importance",
10269
+ "5",
10270
+ "--tags",
10271
+ "recording,transcription",
10272
+ "--summary",
10273
+ args.summary,
10274
+ args.key,
10275
+ args.value
10276
+ ], {
10277
+ stdout: "ignore",
10278
+ stderr: "ignore"
10279
+ });
10280
+ await proc.exited;
10281
+ } catch {}
10282
+ }
10283
+ const toolDocs = {
10284
+ transcribe_audio: `Transcribe audio file. Auto-enhances if needed.
10189
10285
  Params: audio_path (string, required): path to wav/mp3/m4a/webm | language (string): ISO code e.g. en/es/fr | no_enhance (bool): skip AI enhancement | tags (string[]): tags | agent_id (string) | project_id (string) | session_id (string)`,
10190
- save_recording: `Save text as recording. Auto-enhances if needed.
10286
+ save_recording: `Save text as recording. Auto-enhances if needed.
10191
10287
  Params: text (string, required): text to save | enhance (bool): force enhancement | tags (string[]) | agent_id (string) | project_id (string) | session_id (string) | metadata (object)`,
10192
- get_recording: `Get recording by ID or prefix.
10288
+ get_recording: `Get recording by ID or prefix.
10193
10289
  Params: id (string, required): recording ID or prefix`,
10194
- list_recordings: `List recordings, compact by default, most recent first.
10290
+ list_recordings: `List recordings, compact by default, most recent first.
10195
10291
  Params: limit (number, default 10) | offset (number) | processing_mode ('raw'|'enhanced') | tags (string[]) | search (string): text search | since/until (ISO date) | agent_id | project_id | session_id | full (bool): verbose output`,
10196
- search_recordings: `Search recordings by text content.
10292
+ search_recordings: `Search recordings by text content.
10197
10293
  Params: query (string, required) | limit (number, default 10) | agent_id | project_id | full (bool): verbose output`,
10198
- delete_recording: `Delete recording by ID.
10294
+ delete_recording: `Delete recording by ID.
10199
10295
  Params: id (string, required)`,
10200
- recording_stats: `Recording count, mode breakdown, duration.
10296
+ recording_stats: `Recording count, mode breakdown, duration.
10201
10297
  Params: none`,
10202
- detect_enhancement: `Check if text needs AI enhancement.
10298
+ detect_enhancement: `Check if text needs AI enhancement.
10203
10299
  Params: text (string, required)`,
10204
- register_agent: `Register agent (idempotent). Auto-updates last_seen_at on re-register.
10300
+ register_agent: `Register agent (idempotent). Auto-updates last_seen_at on re-register.
10205
10301
  Params: name (string, required) | description (string) | role (string)`,
10206
- list_agents: `List registered agents.
10302
+ list_agents: `List registered agents.
10207
10303
  Params: none`,
10208
- get_agent: `Get agent by ID or name.
10304
+ get_agent: `Get agent by ID or name.
10209
10305
  Params: id (string, required)`,
10210
- heartbeat: `Update last_seen_at to signal agent is active.
10306
+ heartbeat: `Update last_seen_at to signal agent is active.
10211
10307
  Params: agent_id (string, required): agent ID or name`,
10212
- set_focus: `Set active project context for this agent session.
10308
+ set_focus: `Set active project context for this agent session.
10213
10309
  Params: agent_id (string, required) | project_id (string, nullable): project ID or null to clear`,
10214
- register_project: `Register project (idempotent).
10310
+ register_project: `Register project (idempotent).
10215
10311
  Params: name (string, required) | path (string, required): absolute path | description (string)`,
10216
- list_projects: `List registered projects.
10312
+ list_projects: `List registered projects.
10217
10313
  Params: none`
10218
- };
10219
- registerTool("describe_tool", "Get full param docs for any tool.", { name: exports_external.string() }, async (args) => {
10220
- const doc = toolDocs[args.name];
10221
- return doc ? text2(doc) : text2(`Unknown tool: ${args.name}. Available: ${Object.keys(toolDocs).join(", ")}`);
10222
- });
10223
- registerTool("transcribe_audio", "Transcribe audio file. Auto-enhances if needed.", {
10224
- audio_path: exports_external.string(),
10225
- language: exports_external.string().optional(),
10226
- no_enhance: exports_external.boolean().optional(),
10227
- tags: exports_external.array(exports_external.string()).optional(),
10228
- agent_id: exports_external.string().optional(),
10229
- project_id: exports_external.string().optional(),
10230
- session_id: exports_external.string().optional()
10231
- }, async (args) => {
10232
- try {
10233
- const cfg = { ...config };
10234
- if (args.language)
10235
- cfg.language = args.language;
10236
- if (args.no_enhance)
10237
- cfg.auto_enhance = false;
10238
- const transcription = await transcribeAudio(args.audio_path, cfg);
10239
- const processed = await processText(transcription.text, cfg);
10240
- const recording = createRecording({
10241
- audio_path: args.audio_path,
10242
- raw_text: transcription.text,
10243
- processed_text: processed.mode === "enhanced" ? processed.text : undefined,
10244
- processing_mode: processed.mode,
10245
- model_used: transcription.model,
10246
- enhancement_model: processed.enhancement_model || undefined,
10247
- duration_ms: transcription.duration_ms,
10248
- language: transcription.language || undefined,
10249
- tags: args.tags,
10250
- agent_id: args.agent_id,
10251
- project_id: args.project_id,
10252
- session_id: args.session_id
10253
- });
10254
- if (args.agent_id) {
10255
- await saveRecordingMemento({
10256
- key: `recording-${recording.id}`,
10257
- value: JSON.stringify({
10258
- recording_id: recording.id,
10259
- text: processed.mode === "enhanced" ? processed.text : transcription.text,
10260
- agent_id: args.agent_id,
10261
- project_id: args.project_id,
10262
- session_id: args.session_id,
10263
- created_at: recording.created_at
10264
- }),
10265
- summary: `Recording ${recording.id.slice(0, 8)} for ${args.agent_id}`
10314
+ };
10315
+ registerTool("describe_tool", "Get full param docs for any tool.", { name: exports_external.string() }, async (args) => {
10316
+ const doc = toolDocs[args.name];
10317
+ return doc ? text2(doc) : text2(`Unknown tool: ${args.name}. Available: ${Object.keys(toolDocs).join(", ")}`);
10318
+ });
10319
+ registerTool("transcribe_audio", "Transcribe audio file. Auto-enhances if needed.", {
10320
+ audio_path: exports_external.string(),
10321
+ language: exports_external.string().optional(),
10322
+ no_enhance: exports_external.boolean().optional(),
10323
+ tags: exports_external.array(exports_external.string()).optional(),
10324
+ agent_id: exports_external.string().optional(),
10325
+ project_id: exports_external.string().optional(),
10326
+ session_id: exports_external.string().optional()
10327
+ }, async (args) => {
10328
+ try {
10329
+ const cfg = { ...config };
10330
+ if (args.language)
10331
+ cfg.language = args.language;
10332
+ if (args.no_enhance)
10333
+ cfg.auto_enhance = false;
10334
+ const transcription = await transcribeAudio(args.audio_path, cfg);
10335
+ const processed = await processText(transcription.text, cfg);
10336
+ const recording = createRecording({
10337
+ audio_path: args.audio_path,
10338
+ raw_text: transcription.text,
10339
+ processed_text: processed.mode === "enhanced" ? processed.text : undefined,
10340
+ processing_mode: processed.mode,
10341
+ model_used: transcription.model,
10342
+ enhancement_model: processed.enhancement_model || undefined,
10343
+ duration_ms: transcription.duration_ms,
10344
+ language: transcription.language || undefined,
10345
+ tags: args.tags,
10346
+ agent_id: args.agent_id,
10347
+ project_id: args.project_id,
10348
+ session_id: args.session_id
10266
10349
  });
10350
+ if (args.agent_id) {
10351
+ await saveRecordingMemento({
10352
+ key: `recording-${recording.id}`,
10353
+ value: JSON.stringify({
10354
+ recording_id: recording.id,
10355
+ text: processed.mode === "enhanced" ? processed.text : transcription.text,
10356
+ agent_id: args.agent_id,
10357
+ project_id: args.project_id,
10358
+ session_id: args.session_id,
10359
+ created_at: recording.created_at
10360
+ }),
10361
+ summary: `Recording ${recording.id.slice(0, 8)} for ${args.agent_id}`
10362
+ });
10363
+ }
10364
+ const output = processed.mode === "enhanced" ? processed.text : transcription.text;
10365
+ return text2(`${recording.id.slice(0, 8)} | ${processed.mode} | ${output}`);
10366
+ } catch (e) {
10367
+ return errorResult(e);
10267
10368
  }
10268
- const output = processed.mode === "enhanced" ? processed.text : transcription.text;
10269
- return text2(`${recording.id.slice(0, 8)} | ${processed.mode} | ${output}`);
10270
- } catch (e) {
10271
- return errorResult(e);
10272
- }
10273
- });
10274
- registerTool("save_recording", "Save text as recording. Auto-enhances if needed.", {
10275
- text: exports_external.string(),
10276
- enhance: exports_external.boolean().optional(),
10277
- tags: exports_external.array(exports_external.string()).optional(),
10278
- agent_id: exports_external.string().optional(),
10279
- project_id: exports_external.string().optional(),
10280
- session_id: exports_external.string().optional(),
10281
- goal: exports_external.string().optional().describe("Goal or purpose of this recording session (e.g. 'code review for PR #123')"),
10282
- role: exports_external.string().optional().describe("Agent role for this session (e.g. 'dev agent for connectdev')"),
10283
- task_list_id: exports_external.string().optional().describe("Task list ID to bind this recording to"),
10284
- metadata: exports_external.record(exports_external.unknown()).optional()
10285
- }, async (args) => {
10286
- try {
10287
- let processedText;
10288
- let mode = "raw";
10289
- let enhModel;
10290
- if (args.enhance !== false) {
10291
- const processed = await processText(args.text, config);
10292
- if (processed.mode === "enhanced") {
10293
- processedText = processed.text;
10294
- mode = "enhanced";
10295
- enhModel = processed.enhancement_model || undefined;
10296
- }
10297
- }
10298
- const recording = createRecording({
10299
- raw_text: args.text,
10300
- processed_text: processedText,
10301
- processing_mode: mode,
10302
- model_used: "direct-input",
10303
- enhancement_model: enhModel,
10304
- tags: args.tags,
10305
- agent_id: args.agent_id,
10306
- project_id: args.project_id,
10307
- session_id: args.session_id,
10308
- goal: args.goal,
10309
- role: args.role,
10310
- task_list_id: args.task_list_id,
10311
- metadata: args.metadata
10312
- });
10313
- const output = processedText || args.text;
10314
- return text2(`${recording.id.slice(0, 8)} | ${mode} | ${output}`);
10315
- } catch (e) {
10316
- return errorResult(e);
10317
- }
10318
- });
10319
- registerTool("get_recording", "Get recording by ID or prefix.", { id: exports_external.string() }, async (args) => {
10320
- try {
10321
- const r = getRecording(args.id);
10322
- if (!r)
10323
- return text2(`Not found: ${args.id}`);
10324
- return text2(full(r));
10325
- } catch (e) {
10326
- return errorResult(e);
10327
- }
10328
- });
10329
- registerTool("list_recordings", "List recordings. Compact default, recent first.", {
10330
- limit: exports_external.number().optional(),
10331
- offset: exports_external.number().optional(),
10332
- processing_mode: exports_external.enum(["raw", "enhanced"]).optional(),
10333
- tags: exports_external.array(exports_external.string()).optional(),
10334
- search: exports_external.string().optional(),
10335
- since: exports_external.string().optional(),
10336
- until: exports_external.string().optional(),
10337
- agent_id: exports_external.string().optional(),
10338
- project_id: exports_external.string().optional(),
10339
- session_id: exports_external.string().optional(),
10340
- full: exports_external.boolean().optional()
10341
- }, async (args) => {
10342
- try {
10343
- const filter = {
10344
- limit: args.limit || 10,
10345
- offset: args.offset,
10346
- processing_mode: args.processing_mode,
10347
- tags: args.tags,
10348
- search: args.search,
10349
- since: args.since,
10350
- until: args.until,
10351
- agent_id: args.agent_id,
10352
- project_id: args.project_id,
10353
- session_id: args.session_id
10354
- };
10355
- const recordings = listRecordings(filter);
10356
- if (recordings.length === 0)
10357
- return text2("No recordings found.");
10358
- const fmt = args.full ? full : compact;
10359
- const sep = args.full ? `
10369
+ });
10370
+ registerTool("save_recording", "Save text as recording. Auto-enhances if needed.", {
10371
+ text: exports_external.string(),
10372
+ enhance: exports_external.boolean().optional(),
10373
+ tags: exports_external.array(exports_external.string()).optional(),
10374
+ agent_id: exports_external.string().optional(),
10375
+ project_id: exports_external.string().optional(),
10376
+ session_id: exports_external.string().optional(),
10377
+ goal: exports_external.string().optional().describe("Goal or purpose of this recording session (e.g. 'code review for PR #123')"),
10378
+ role: exports_external.string().optional().describe("Agent role for this session (e.g. 'dev agent for connectdev')"),
10379
+ task_list_id: exports_external.string().optional().describe("Task list ID to bind this recording to"),
10380
+ metadata: exports_external.record(exports_external.unknown()).optional()
10381
+ }, async (args) => {
10382
+ try {
10383
+ let processedText;
10384
+ let mode = "raw";
10385
+ let enhModel;
10386
+ if (args.enhance !== false) {
10387
+ const processed = await processText(args.text, config);
10388
+ if (processed.mode === "enhanced") {
10389
+ processedText = processed.text;
10390
+ mode = "enhanced";
10391
+ enhModel = processed.enhancement_model || undefined;
10392
+ }
10393
+ }
10394
+ const recording = createRecording({
10395
+ raw_text: args.text,
10396
+ processed_text: processedText,
10397
+ processing_mode: mode,
10398
+ model_used: "direct-input",
10399
+ enhancement_model: enhModel,
10400
+ tags: args.tags,
10401
+ agent_id: args.agent_id,
10402
+ project_id: args.project_id,
10403
+ session_id: args.session_id,
10404
+ goal: args.goal,
10405
+ role: args.role,
10406
+ task_list_id: args.task_list_id,
10407
+ metadata: args.metadata
10408
+ });
10409
+ const output = processedText || args.text;
10410
+ return text2(`${recording.id.slice(0, 8)} | ${mode} | ${output}`);
10411
+ } catch (e) {
10412
+ return errorResult(e);
10413
+ }
10414
+ });
10415
+ registerTool("get_recording", "Get recording by ID or prefix.", { id: exports_external.string() }, async (args) => {
10416
+ try {
10417
+ const r = getRecording(args.id);
10418
+ if (!r)
10419
+ return text2(`Not found: ${args.id}`);
10420
+ return text2(full(r));
10421
+ } catch (e) {
10422
+ return errorResult(e);
10423
+ }
10424
+ });
10425
+ registerTool("list_recordings", "List recordings. Compact default, recent first.", {
10426
+ limit: exports_external.number().optional(),
10427
+ offset: exports_external.number().optional(),
10428
+ processing_mode: exports_external.enum(["raw", "enhanced"]).optional(),
10429
+ tags: exports_external.array(exports_external.string()).optional(),
10430
+ search: exports_external.string().optional(),
10431
+ since: exports_external.string().optional(),
10432
+ until: exports_external.string().optional(),
10433
+ agent_id: exports_external.string().optional(),
10434
+ project_id: exports_external.string().optional(),
10435
+ session_id: exports_external.string().optional(),
10436
+ full: exports_external.boolean().optional()
10437
+ }, async (args) => {
10438
+ try {
10439
+ const filter = {
10440
+ limit: args.limit || 10,
10441
+ offset: args.offset,
10442
+ processing_mode: args.processing_mode,
10443
+ tags: args.tags,
10444
+ search: args.search,
10445
+ since: args.since,
10446
+ until: args.until,
10447
+ agent_id: args.agent_id,
10448
+ project_id: args.project_id,
10449
+ session_id: args.session_id
10450
+ };
10451
+ const recordings = listRecordings(filter);
10452
+ if (recordings.length === 0)
10453
+ return text2("No recordings found.");
10454
+ const fmt = args.full ? full : compact;
10455
+ const sep = args.full ? `
10360
10456
  ---
10361
10457
  ` : `
10362
10458
  `;
10363
- return text2(`${recordings.length} recording(s):${sep}${recordings.map(fmt).join(sep)}`);
10364
- } catch (e) {
10365
- return errorResult(e);
10366
- }
10367
- });
10368
- registerTool("search_recordings", "Search recordings by text.", {
10369
- query: exports_external.string(),
10370
- limit: exports_external.number().optional(),
10371
- agent_id: exports_external.string().optional(),
10372
- project_id: exports_external.string().optional(),
10373
- full: exports_external.boolean().optional()
10374
- }, async (args) => {
10375
- try {
10376
- const results = searchRecordings(args.query, {
10377
- limit: args.limit || 10,
10378
- agent_id: args.agent_id,
10379
- project_id: args.project_id
10380
- });
10381
- if (results.length === 0)
10382
- return text2("No results.");
10383
- const fmt = args.full ? full : compact;
10384
- const sep = args.full ? `
10459
+ return text2(`${recordings.length} recording(s):${sep}${recordings.map(fmt).join(sep)}`);
10460
+ } catch (e) {
10461
+ return errorResult(e);
10462
+ }
10463
+ });
10464
+ registerTool("search_recordings", "Search recordings by text.", {
10465
+ query: exports_external.string(),
10466
+ limit: exports_external.number().optional(),
10467
+ agent_id: exports_external.string().optional(),
10468
+ project_id: exports_external.string().optional(),
10469
+ full: exports_external.boolean().optional()
10470
+ }, async (args) => {
10471
+ try {
10472
+ const results = searchRecordings(args.query, {
10473
+ limit: args.limit || 10,
10474
+ agent_id: args.agent_id,
10475
+ project_id: args.project_id
10476
+ });
10477
+ if (results.length === 0)
10478
+ return text2("No results.");
10479
+ const fmt = args.full ? full : compact;
10480
+ const sep = args.full ? `
10385
10481
  ---
10386
10482
  ` : `
10387
10483
  `;
10388
- return text2(`${results.length} result(s):${sep}${results.map(fmt).join(sep)}`);
10389
- } catch (e) {
10390
- return errorResult(e);
10391
- }
10392
- });
10393
- registerTool("delete_recording", "Delete recording by ID.", { id: exports_external.string() }, async (args) => {
10394
- try {
10395
- return text2(deleteRecording(args.id) ? `Deleted ${args.id}` : `Not found: ${args.id}`);
10396
- } catch (e) {
10397
- return errorResult(e);
10398
- }
10399
- });
10400
- registerTool("recording_stats", "Recording stats: count, modes, duration.", {}, async () => {
10401
- try {
10402
- const s = getRecordingStats();
10403
- let out = `Total: ${s.total} | Raw: ${s.raw} | Enhanced: ${s.enhanced} | Duration: ${(s.total_duration_ms / 1000).toFixed(1)}s`;
10404
- if (Object.keys(s.by_model).length > 0) {
10405
- out += `
10484
+ return text2(`${results.length} result(s):${sep}${results.map(fmt).join(sep)}`);
10485
+ } catch (e) {
10486
+ return errorResult(e);
10487
+ }
10488
+ });
10489
+ registerTool("delete_recording", "Delete recording by ID.", { id: exports_external.string() }, async (args) => {
10490
+ try {
10491
+ return text2(deleteRecording(args.id) ? `Deleted ${args.id}` : `Not found: ${args.id}`);
10492
+ } catch (e) {
10493
+ return errorResult(e);
10494
+ }
10495
+ });
10496
+ registerTool("recording_stats", "Recording stats: count, modes, duration.", {}, async () => {
10497
+ try {
10498
+ const s = getRecordingStats();
10499
+ let out = `Total: ${s.total} | Raw: ${s.raw} | Enhanced: ${s.enhanced} | Duration: ${(s.total_duration_ms / 1000).toFixed(1)}s`;
10500
+ if (Object.keys(s.by_model).length > 0) {
10501
+ out += `
10406
10502
  ` + Object.entries(s.by_model).map(([m, c]) => `${m}: ${c}`).join(", ");
10503
+ }
10504
+ return text2(out);
10505
+ } catch (e) {
10506
+ return errorResult(e);
10407
10507
  }
10408
- return text2(out);
10409
- } catch (e) {
10410
- return errorResult(e);
10411
- }
10412
- });
10413
- registerTool("detect_enhancement", "Check if text needs AI enhancement.", { text: exports_external.string() }, async (args) => {
10414
- try {
10415
- const r = needsEnhancement(args.text, config);
10416
- return text2(`${r.needs ? "Yes" : "No"}: ${r.reason}`);
10417
- } catch (e) {
10418
- return errorResult(e);
10419
- }
10420
- });
10421
- registerTool("register_agent", "Register agent (idempotent).", { name: exports_external.string(), description: exports_external.string().optional(), role: exports_external.string().optional() }, async (args) => {
10422
- try {
10423
- const a = registerAgent(args.name, args.description, args.role);
10424
- return text2(`${a.id} | ${a.name} | ${a.role}`);
10425
- } catch (e) {
10426
- return errorResult(e);
10427
- }
10428
- });
10429
- registerTool("list_agents", "List registered agents.", {}, async () => {
10430
- try {
10431
- const agents = listAgents();
10432
- if (agents.length === 0)
10433
- return text2("None.");
10434
- return text2(agents.map((a) => `${a.id} | ${a.name} | ${a.role}`).join(`
10508
+ });
10509
+ registerTool("detect_enhancement", "Check if text needs AI enhancement.", { text: exports_external.string() }, async (args) => {
10510
+ try {
10511
+ const r = needsEnhancement(args.text, config);
10512
+ return text2(`${r.needs ? "Yes" : "No"}: ${r.reason}`);
10513
+ } catch (e) {
10514
+ return errorResult(e);
10515
+ }
10516
+ });
10517
+ registerTool("register_agent", "Register agent (idempotent).", { name: exports_external.string(), description: exports_external.string().optional(), role: exports_external.string().optional() }, async (args) => {
10518
+ try {
10519
+ const a = registerAgent(args.name, args.description, args.role);
10520
+ return text2(`${a.id} | ${a.name} | ${a.role}`);
10521
+ } catch (e) {
10522
+ return errorResult(e);
10523
+ }
10524
+ });
10525
+ registerTool("list_agents", "List registered agents.", {}, async () => {
10526
+ try {
10527
+ const agents = listAgents();
10528
+ if (agents.length === 0)
10529
+ return text2("None.");
10530
+ return text2(agents.map((a) => `${a.id} | ${a.name} | ${a.role}`).join(`
10435
10531
  `));
10436
- } catch (e) {
10437
- return errorResult(e);
10438
- }
10439
- });
10440
- registerTool("get_agent", "Get agent by ID or name.", { id: exports_external.string() }, async (args) => {
10441
- try {
10442
- const a = getAgent(args.id);
10443
- if (!a)
10444
- return text2(`Not found: ${args.id}`);
10445
- return text2(`${a.id} | ${a.name} | ${a.role} | ${a.last_seen_at}`);
10446
- } catch (e) {
10447
- return errorResult(e);
10448
- }
10449
- });
10450
- registerTool("register_project", "Register project (idempotent).", { name: exports_external.string(), path: exports_external.string(), description: exports_external.string().optional() }, async (args) => {
10451
- try {
10452
- const p = registerProject(args.name, args.path, args.description);
10453
- return text2(`${p.id.slice(0, 8)} | ${p.name} | ${p.path}`);
10454
- } catch (e) {
10455
- return errorResult(e);
10456
- }
10457
- });
10458
- registerTool("list_projects", "List registered projects.", {}, async () => {
10459
- try {
10460
- const projects = listProjects();
10461
- if (projects.length === 0)
10462
- return text2("None.");
10463
- return text2(projects.map((p) => `${p.id.slice(0, 8)} | ${p.name} | ${p.path}`).join(`
10532
+ } catch (e) {
10533
+ return errorResult(e);
10534
+ }
10535
+ });
10536
+ registerTool("get_agent", "Get agent by ID or name.", { id: exports_external.string() }, async (args) => {
10537
+ try {
10538
+ const a = getAgent(args.id);
10539
+ if (!a)
10540
+ return text2(`Not found: ${args.id}`);
10541
+ return text2(`${a.id} | ${a.name} | ${a.role} | ${a.last_seen_at}`);
10542
+ } catch (e) {
10543
+ return errorResult(e);
10544
+ }
10545
+ });
10546
+ registerTool("register_project", "Register project (idempotent).", { name: exports_external.string(), path: exports_external.string(), description: exports_external.string().optional() }, async (args) => {
10547
+ try {
10548
+ const p = registerProject(args.name, args.path, args.description);
10549
+ return text2(`${p.id.slice(0, 8)} | ${p.name} | ${p.path}`);
10550
+ } catch (e) {
10551
+ return errorResult(e);
10552
+ }
10553
+ });
10554
+ registerTool("list_projects", "List registered projects.", {}, async () => {
10555
+ try {
10556
+ const projects = listProjects();
10557
+ if (projects.length === 0)
10558
+ return text2("None.");
10559
+ return text2(projects.map((p) => `${p.id.slice(0, 8)} | ${p.name} | ${p.path}`).join(`
10464
10560
  `));
10465
- } catch (e) {
10466
- return errorResult(e);
10467
- }
10468
- });
10469
- registerTool("heartbeat", "Update last_seen_at to signal agent is active. Call periodically during long tasks.", { agent_id: exports_external.string().describe("Agent ID or name") }, async (args) => {
10470
- try {
10471
- const agent = heartbeatAgent(args.agent_id);
10472
- if (!agent)
10473
- return text2(`Agent not found: ${args.agent_id}`);
10474
- return text2(`${agent.id} | ${agent.name} | last_seen: ${agent.last_seen_at}`);
10475
- } catch (e) {
10476
- return errorResult(e);
10477
- }
10478
- });
10479
- registerTool("set_focus", "Set active project context for this agent session.", { agent_id: exports_external.string().describe("Agent ID or name"), project_id: exports_external.string().nullable().optional().describe("Project ID to focus on, or null to clear") }, async (args) => {
10480
- try {
10481
- const agent = setAgentFocus(args.agent_id, args.project_id ?? null);
10482
- if (!agent)
10483
- return text2(`Agent not found: ${args.agent_id}`);
10484
- return text2(args.project_id ? `Focus set: ${args.project_id}` : "Focus cleared");
10485
- } catch (e) {
10486
- return errorResult(e);
10561
+ } catch (e) {
10562
+ return errorResult(e);
10563
+ }
10564
+ });
10565
+ registerTool("heartbeat", "Update last_seen_at to signal agent is active. Call periodically during long tasks.", { agent_id: exports_external.string().describe("Agent ID or name") }, async (args) => {
10566
+ try {
10567
+ const agent = heartbeatAgent(args.agent_id);
10568
+ if (!agent)
10569
+ return text2(`Agent not found: ${args.agent_id}`);
10570
+ return text2(`${agent.id} | ${agent.name} | last_seen: ${agent.last_seen_at}`);
10571
+ } catch (e) {
10572
+ return errorResult(e);
10573
+ }
10574
+ });
10575
+ registerTool("set_focus", "Set active project context for this agent session.", { agent_id: exports_external.string().describe("Agent ID or name"), project_id: exports_external.string().nullable().optional().describe("Project ID to focus on, or null to clear") }, async (args) => {
10576
+ try {
10577
+ const agent = setAgentFocus(args.agent_id, args.project_id ?? null);
10578
+ if (!agent)
10579
+ return text2(`Agent not found: ${args.agent_id}`);
10580
+ return text2(args.project_id ? `Focus set: ${args.project_id}` : "Focus cleared");
10581
+ } catch (e) {
10582
+ return errorResult(e);
10583
+ }
10584
+ });
10585
+ registerTool("send_feedback", "Send feedback about this service", {
10586
+ message: exports_external.string().describe("Feedback message"),
10587
+ email: exports_external.string().optional().describe("Contact email (optional)"),
10588
+ category: exports_external.enum(["bug", "feature", "general"]).optional().describe("Feedback category")
10589
+ }, async (params) => {
10590
+ const adapter = getAdapter();
10591
+ const pkg = require_package();
10592
+ adapter.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", params.message, params.email || null, params.category || "general", pkg.version);
10593
+ return text2("Feedback saved. Thank you!");
10594
+ });
10595
+ registerRecordingsStorageTools(server);
10596
+ return server;
10597
+ }
10598
+ async function main() {
10599
+ const args = process.argv.slice(2);
10600
+ if (isStdioMode(args)) {
10601
+ const transport = new StdioServerTransport;
10602
+ await buildServer().connect(transport);
10603
+ return;
10487
10604
  }
10488
- });
10489
- registerTool("send_feedback", "Send feedback about this service", {
10490
- message: exports_external.string().describe("Feedback message"),
10491
- email: exports_external.string().optional().describe("Contact email (optional)"),
10492
- category: exports_external.enum(["bug", "feature", "general"]).optional().describe("Feedback category")
10493
- }, async (params) => {
10494
- const adapter = getAdapter();
10495
- const pkg = require_package();
10496
- adapter.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", params.message, params.email || null, params.category || "general", pkg.version);
10497
- return text2("Feedback saved. Thank you!");
10498
- });
10499
- var transport = new StdioServerTransport;
10500
- registerRecordingsCloudTools(server);
10501
- await server.connect(transport);
10605
+ startMcpHttpServer({ name: "recordings", port: resolveMcpHttpPort(args), buildServer });
10606
+ }
10607
+ if (import.meta.main) {
10608
+ await main();
10609
+ }
10610
+ export {
10611
+ buildServer
10612
+ };