datanotes 1.2.1 → 1.3.0

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/SECURITY.md CHANGED
@@ -47,7 +47,10 @@ Nothing leaves the machine unless you turn it on:
47
47
  - **Semantic search** sends note text to the embedding provider you configure (`semanticEnabled`,
48
48
  off by default; provider `semanticProvider` / `semanticBaseUrl`). A local provider (Ollama,
49
49
  LM Studio) keeps it on the machine.
50
- - **Telemetry** sends logs to the OTLP endpoint you configure (`telemetryEnabled`, off by default).
50
+ - **Telemetry** sends logs to the OTLP endpoint you configure (`telemetryEnabled`, off by default):
51
+ operations, routes, note paths, actors, tool names and error messages (which may quote a value),
52
+ not note bodies. `telemetryHeaders` (e.g. an
53
+ ingestion key) is stored in the settings file; `OTEL_EXPORTER_OTLP_HEADERS` keeps it out.
51
54
  - **Webhooks** (created by the owner) post change events to the URLs they name.
52
55
  - **Apps' MCP endpoints** (registered by the owner): the engine sends each app's `mcp_token`, and
53
56
  the arguments and caller of every call of its tools, to the app's `mcp_url`, which may be remote.
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  DatanotesError: () => DatanotesError,
27
27
  OPERATIONS: () => OPERATIONS,
28
28
  OperationInputError: () => OperationInputError,
29
+ OtlpLogExporter: () => OtlpLogExporter,
29
30
  ROUTE_PREFIX: () => ROUTE_PREFIX,
30
31
  describeBatch: () => describeBatch,
31
32
  describeRefusal: () => describeRefusal,
@@ -33,6 +34,8 @@ __export(index_exports, {
33
34
  fetchTransport: () => fetchTransport,
34
35
  operationRequest: () => operationRequest,
35
36
  operationRoute: () => operationRoute,
37
+ otlpFromEnv: () => otlpFromEnv,
38
+ parseOtlpHeaders: () => parseOtlpHeaders,
36
39
  resultOf: () => resultOf
37
40
  });
38
41
  module.exports = __toCommonJS(index_exports);
@@ -3062,6 +3065,110 @@ function estimateTokens(text) {
3062
3065
  return n;
3063
3066
  }
3064
3067
 
3068
+ // src/api/otlp.ts
3069
+ var SEVERITY_NUMBER = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
3070
+ function attr(key, v) {
3071
+ if (v === void 0 || v === null) return null;
3072
+ if (typeof v === "boolean") return { key, value: { boolValue: v } };
3073
+ if (typeof v === "number") return Number.isInteger(v) ? { key, value: { intValue: String(v) } } : { key, value: { doubleValue: v } };
3074
+ return { key, value: { stringValue: v } };
3075
+ }
3076
+ var attrs = (a) => Object.entries(a).map(([k, v]) => attr(k, v)).filter((x) => x !== null);
3077
+ var fetchSend = async (url, body, headers) => {
3078
+ const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body });
3079
+ if (!res.ok) throw new Error(`OTLP HTTP ${res.status}`);
3080
+ };
3081
+ var OtlpLogExporter = class {
3082
+ constructor(o) {
3083
+ this.o = o;
3084
+ this.buffer = [];
3085
+ this.inflight = Promise.resolve();
3086
+ this.timer = null;
3087
+ /** Records sent, and records lost because the collector did not take them. */
3088
+ this.sent = 0;
3089
+ this.dropped = 0;
3090
+ const every = o.flushMs ?? 5e3;
3091
+ if (every > 0) {
3092
+ this.timer = setInterval(() => void this.flush(), every);
3093
+ this.timer.unref?.();
3094
+ }
3095
+ }
3096
+ log(severity, body, attributes = {}) {
3097
+ const ms = (this.o.now ?? Date.now)();
3098
+ this.buffer.push({ timeUnixNano: `${Math.round(ms)}000000`, severity, body, attributes });
3099
+ if (this.buffer.length >= (this.o.maxBatch ?? 50)) void this.flush();
3100
+ }
3101
+ /** Send what is buffered. Never rejects. */
3102
+ flush() {
3103
+ if (this.buffer.length === 0) return this.inflight;
3104
+ const batch = this.buffer;
3105
+ this.buffer = [];
3106
+ const body = JSON.stringify({
3107
+ resourceLogs: [
3108
+ {
3109
+ resource: { attributes: attrs({ "service.name": this.o.serviceName, "service.version": this.o.serviceVersion, ...this.o.resource }) },
3110
+ scopeLogs: [
3111
+ {
3112
+ scope: { name: this.o.serviceName },
3113
+ logRecords: batch.map((r) => ({
3114
+ timeUnixNano: r.timeUnixNano,
3115
+ observedTimeUnixNano: r.timeUnixNano,
3116
+ severityNumber: SEVERITY_NUMBER[r.severity],
3117
+ severityText: r.severity,
3118
+ body: { stringValue: r.body },
3119
+ attributes: attrs(r.attributes)
3120
+ }))
3121
+ }
3122
+ ]
3123
+ }
3124
+ ]
3125
+ });
3126
+ const url = `${this.o.endpoint.replace(/\/+$/, "")}/v1/logs`;
3127
+ const send = this.o.send ?? fetchSend;
3128
+ this.inflight = this.inflight.then(
3129
+ () => send(url, body, this.o.headers ?? {}).then(
3130
+ () => void (this.sent += batch.length),
3131
+ () => void (this.dropped += batch.length)
3132
+ )
3133
+ );
3134
+ return this.inflight;
3135
+ }
3136
+ /** Stop the timer and send what is left. */
3137
+ async close() {
3138
+ if (this.timer) clearInterval(this.timer);
3139
+ this.timer = null;
3140
+ await this.flush();
3141
+ }
3142
+ };
3143
+ function parseOtlpHeaders(text) {
3144
+ const out = {};
3145
+ for (const part of (text ?? "").split(",")) {
3146
+ const i = part.indexOf("=");
3147
+ if (i <= 0) continue;
3148
+ const key = part.slice(0, i).trim();
3149
+ const value = part.slice(i + 1).trim();
3150
+ if (!key) continue;
3151
+ try {
3152
+ out[key] = decodeURIComponent(value);
3153
+ } catch {
3154
+ out[key] = value;
3155
+ }
3156
+ }
3157
+ return out;
3158
+ }
3159
+ function otlpFromEnv(env, defaults) {
3160
+ const logs = env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT?.trim();
3161
+ const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
3162
+ const endpoint = logs ? logs.replace(/\/v1\/logs\/?$/, "") : base;
3163
+ if (!endpoint) return null;
3164
+ return {
3165
+ endpoint,
3166
+ serviceName: env.OTEL_SERVICE_NAME?.trim() || defaults.serviceName,
3167
+ ...defaults.serviceVersion ? { serviceVersion: defaults.serviceVersion } : {},
3168
+ headers: { ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS), ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_LOGS_HEADERS) }
3169
+ };
3170
+ }
3171
+
3065
3172
  // src/api/client.ts
3066
3173
  var ACTOR_HEADER = "X-Datanotes-Actor";
3067
3174
  var DatanotesError = class extends Error {
@@ -6,4 +6,5 @@ export { OPERATIONS, type OperationDef } from "./catalog";
6
6
  export { operationRequest, operationRoute, CALLABLE_OPERATIONS, OperationInputError, ROUTE_PREFIX, type OperationRequest } from "./requests";
7
7
  export { resultOf, describeRefusal, describeBatch, type OperationResult } from "./results";
8
8
  export { estimateTokens } from "./tokens";
9
+ export { OtlpLogExporter, otlpFromEnv, parseOtlpHeaders, type OtlpOptions, type Severity, type AttrValue } from "./otlp";
9
10
  export { DatanotesClient, DatanotesError, fetchTransport, ACTOR_HEADER, type ActorInfo, type AppTool, type ClientOptions, type HttpRequest, type HttpResponse, type ToolCallRecord, type Transport, } from "./client";
package/dist/api/index.js CHANGED
@@ -3023,6 +3023,110 @@ function estimateTokens(text) {
3023
3023
  return n;
3024
3024
  }
3025
3025
 
3026
+ // src/api/otlp.ts
3027
+ var SEVERITY_NUMBER = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
3028
+ function attr(key, v) {
3029
+ if (v === void 0 || v === null) return null;
3030
+ if (typeof v === "boolean") return { key, value: { boolValue: v } };
3031
+ if (typeof v === "number") return Number.isInteger(v) ? { key, value: { intValue: String(v) } } : { key, value: { doubleValue: v } };
3032
+ return { key, value: { stringValue: v } };
3033
+ }
3034
+ var attrs = (a) => Object.entries(a).map(([k, v]) => attr(k, v)).filter((x) => x !== null);
3035
+ var fetchSend = async (url, body, headers) => {
3036
+ const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body });
3037
+ if (!res.ok) throw new Error(`OTLP HTTP ${res.status}`);
3038
+ };
3039
+ var OtlpLogExporter = class {
3040
+ constructor(o) {
3041
+ this.o = o;
3042
+ this.buffer = [];
3043
+ this.inflight = Promise.resolve();
3044
+ this.timer = null;
3045
+ /** Records sent, and records lost because the collector did not take them. */
3046
+ this.sent = 0;
3047
+ this.dropped = 0;
3048
+ const every = o.flushMs ?? 5e3;
3049
+ if (every > 0) {
3050
+ this.timer = setInterval(() => void this.flush(), every);
3051
+ this.timer.unref?.();
3052
+ }
3053
+ }
3054
+ log(severity, body, attributes = {}) {
3055
+ const ms = (this.o.now ?? Date.now)();
3056
+ this.buffer.push({ timeUnixNano: `${Math.round(ms)}000000`, severity, body, attributes });
3057
+ if (this.buffer.length >= (this.o.maxBatch ?? 50)) void this.flush();
3058
+ }
3059
+ /** Send what is buffered. Never rejects. */
3060
+ flush() {
3061
+ if (this.buffer.length === 0) return this.inflight;
3062
+ const batch = this.buffer;
3063
+ this.buffer = [];
3064
+ const body = JSON.stringify({
3065
+ resourceLogs: [
3066
+ {
3067
+ resource: { attributes: attrs({ "service.name": this.o.serviceName, "service.version": this.o.serviceVersion, ...this.o.resource }) },
3068
+ scopeLogs: [
3069
+ {
3070
+ scope: { name: this.o.serviceName },
3071
+ logRecords: batch.map((r) => ({
3072
+ timeUnixNano: r.timeUnixNano,
3073
+ observedTimeUnixNano: r.timeUnixNano,
3074
+ severityNumber: SEVERITY_NUMBER[r.severity],
3075
+ severityText: r.severity,
3076
+ body: { stringValue: r.body },
3077
+ attributes: attrs(r.attributes)
3078
+ }))
3079
+ }
3080
+ ]
3081
+ }
3082
+ ]
3083
+ });
3084
+ const url = `${this.o.endpoint.replace(/\/+$/, "")}/v1/logs`;
3085
+ const send = this.o.send ?? fetchSend;
3086
+ this.inflight = this.inflight.then(
3087
+ () => send(url, body, this.o.headers ?? {}).then(
3088
+ () => void (this.sent += batch.length),
3089
+ () => void (this.dropped += batch.length)
3090
+ )
3091
+ );
3092
+ return this.inflight;
3093
+ }
3094
+ /** Stop the timer and send what is left. */
3095
+ async close() {
3096
+ if (this.timer) clearInterval(this.timer);
3097
+ this.timer = null;
3098
+ await this.flush();
3099
+ }
3100
+ };
3101
+ function parseOtlpHeaders(text) {
3102
+ const out = {};
3103
+ for (const part of (text ?? "").split(",")) {
3104
+ const i = part.indexOf("=");
3105
+ if (i <= 0) continue;
3106
+ const key = part.slice(0, i).trim();
3107
+ const value = part.slice(i + 1).trim();
3108
+ if (!key) continue;
3109
+ try {
3110
+ out[key] = decodeURIComponent(value);
3111
+ } catch {
3112
+ out[key] = value;
3113
+ }
3114
+ }
3115
+ return out;
3116
+ }
3117
+ function otlpFromEnv(env, defaults) {
3118
+ const logs = env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT?.trim();
3119
+ const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
3120
+ const endpoint = logs ? logs.replace(/\/v1\/logs\/?$/, "") : base;
3121
+ if (!endpoint) return null;
3122
+ return {
3123
+ endpoint,
3124
+ serviceName: env.OTEL_SERVICE_NAME?.trim() || defaults.serviceName,
3125
+ ...defaults.serviceVersion ? { serviceVersion: defaults.serviceVersion } : {},
3126
+ headers: { ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS), ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_LOGS_HEADERS) }
3127
+ };
3128
+ }
3129
+
3026
3130
  // src/api/client.ts
3027
3131
  var ACTOR_HEADER = "X-Datanotes-Actor";
3028
3132
  var DatanotesError = class extends Error {
@@ -3130,6 +3234,7 @@ export {
3130
3234
  DatanotesError,
3131
3235
  OPERATIONS,
3132
3236
  OperationInputError,
3237
+ OtlpLogExporter,
3133
3238
  ROUTE_PREFIX,
3134
3239
  describeBatch,
3135
3240
  describeRefusal,
@@ -3137,5 +3242,7 @@ export {
3137
3242
  fetchTransport,
3138
3243
  operationRequest,
3139
3244
  operationRoute,
3245
+ otlpFromEnv,
3246
+ parseOtlpHeaders,
3140
3247
  resultOf
3141
3248
  };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * OTLP/HTTP log export for programs around the engine (MCP servers, apps): records are buffered
3
+ * and POSTed as OTLP JSON to `<endpoint>/v1/logs`, the format SigNoz and any OpenTelemetry
4
+ * collector read. No dependency, no SDK. Export failures are counted, never thrown: logging must
5
+ * not break the program.
6
+ *
7
+ * Configure it in code, or from the standard environment variables with `otlpFromEnv`.
8
+ */
9
+ export type Severity = "DEBUG" | "INFO" | "WARN" | "ERROR";
10
+ export type AttrValue = string | number | boolean | null | undefined;
11
+ export interface OtlpOptions {
12
+ /** Collector base URL, e.g. `http://127.0.0.1:4318` (`/v1/logs` is added). */
13
+ endpoint: string;
14
+ /** `service.name` of the records. */
15
+ serviceName: string;
16
+ serviceVersion?: string;
17
+ /** Extra headers, e.g. `{"signoz-ingestion-key": "…"}`. */
18
+ headers?: Record<string, string>;
19
+ /** More resource attributes (e.g. `host.name`). */
20
+ resource?: Record<string, AttrValue>;
21
+ /** Send at most this many records per request (default 50); a full buffer is sent at once. */
22
+ maxBatch?: number;
23
+ /** Send the buffer every this many ms (default 5000; 0: only on `flush`). */
24
+ flushMs?: number;
25
+ /** POST a body; resolves on 2xx (default: fetch). */
26
+ send?: (url: string, body: string, headers: Record<string, string>) => Promise<void>;
27
+ now?: () => number;
28
+ }
29
+ export declare class OtlpLogExporter {
30
+ private readonly o;
31
+ private buffer;
32
+ private inflight;
33
+ private timer;
34
+ /** Records sent, and records lost because the collector did not take them. */
35
+ sent: number;
36
+ dropped: number;
37
+ constructor(o: OtlpOptions);
38
+ log(severity: Severity, body: string, attributes?: Record<string, AttrValue>): void;
39
+ /** Send what is buffered. Never rejects. */
40
+ flush(): Promise<void>;
41
+ /** Stop the timer and send what is left. */
42
+ close(): Promise<void>;
43
+ }
44
+ /** `k1=v1,k2=v2` (values URL-encoded), the format of `OTEL_EXPORTER_OTLP_HEADERS`. */
45
+ export declare function parseOtlpHeaders(text: string | undefined): Record<string, string>;
46
+ /**
47
+ * Exporter options from the standard OpenTelemetry variables, or null when no endpoint is set:
48
+ * `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` (full URL of /v1/logs) or `OTEL_EXPORTER_OTLP_ENDPOINT`,
49
+ * `OTEL_EXPORTER_OTLP_HEADERS` / `OTEL_EXPORTER_OTLP_LOGS_HEADERS`, `OTEL_SERVICE_NAME`.
50
+ */
51
+ export declare function otlpFromEnv(env: Record<string, string | undefined>, defaults: {
52
+ serviceName: string;
53
+ serviceVersion?: string;
54
+ }): OtlpOptions | null;
@@ -7425,6 +7425,7 @@ var DEFAULT_SETTINGS = {
7425
7425
  telemetryEnabled: false,
7426
7426
  telemetryEndpoint: "http://127.0.0.1:4318",
7427
7427
  telemetryServiceName: "datanotes",
7428
+ telemetryHeaders: {},
7428
7429
  autoRecompute: true,
7429
7430
  viewNotes: true,
7430
7431
  semanticEnabled: false,
@@ -7450,6 +7451,7 @@ var KEYS = [
7450
7451
  "telemetryEnabled",
7451
7452
  "telemetryEndpoint",
7452
7453
  "telemetryServiceName",
7454
+ "telemetryHeaders",
7453
7455
  "autoRecompute",
7454
7456
  "viewNotes",
7455
7457
  "semanticEnabled",
@@ -7500,6 +7502,14 @@ function mergeWithDefaults(loaded) {
7500
7502
  }
7501
7503
  break;
7502
7504
  }
7505
+ case "telemetryHeaders": {
7506
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
7507
+ out.telemetryHeaders = Object.fromEntries(
7508
+ Object.entries(raw).filter(([k, v]) => /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(k) && typeof v === "string")
7509
+ );
7510
+ }
7511
+ break;
7512
+ }
7503
7513
  case "telemetryServiceName": {
7504
7514
  if (typeof raw === "string" && raw.trim().length > 0) {
7505
7515
  out.telemetryServiceName = raw.trim();
@@ -7659,7 +7669,7 @@ function otlpLogsPayload(serviceName, records, resource = {}) {
7659
7669
  resource: { attributes: attrs({ "service.name": serviceName, ...resource }) },
7660
7670
  scopeLogs: [
7661
7671
  {
7662
- scope: { name: "obsidian-datanotes" },
7672
+ scope: { name: "datanotes" },
7663
7673
  logRecords: records.map((r) => ({
7664
7674
  timeUnixNano: r.timeUnixNano,
7665
7675
  observedTimeUnixNano: r.timeUnixNano,
@@ -7705,7 +7715,7 @@ var OtlpLogger = class {
7705
7715
  const url = `${settings.telemetryEndpoint.replace(/\/+$/, "")}/v1/logs`;
7706
7716
  const body = otlpLogsPayload(settings.telemetryServiceName, batch, this.opts.resource);
7707
7717
  this.inflight = this.inflight.then(
7708
- () => this.opts.transport(url, body).then(
7718
+ () => this.opts.transport(url, body, { ...settings.telemetryHeaders ?? {} }).then(
7709
7719
  () => {
7710
7720
  this.sent += batch.length;
7711
7721
  },
@@ -7840,6 +7850,24 @@ var LockHolder = class {
7840
7850
  }
7841
7851
  };
7842
7852
 
7853
+ // src/api/otlp.ts
7854
+ function parseOtlpHeaders(text2) {
7855
+ const out = {};
7856
+ for (const part of (text2 ?? "").split(",")) {
7857
+ const i = part.indexOf("=");
7858
+ if (i <= 0) continue;
7859
+ const key = part.slice(0, i).trim();
7860
+ const value = part.slice(i + 1).trim();
7861
+ if (!key) continue;
7862
+ try {
7863
+ out[key] = decodeURIComponent(value);
7864
+ } catch {
7865
+ out[key] = value;
7866
+ }
7867
+ }
7868
+ return out;
7869
+ }
7870
+
7843
7871
  // src/core/filename-values.ts
7844
7872
  var PLACEHOLDER = /\{([A-Za-z][A-Za-z0-9_-]*)(?::([^}]+))?\}/g;
7845
7873
  var FORMAT_TOKEN = /\[([^\]]*)\]|YYYY|MM|DD|HH|mm|ss/g;
@@ -20762,7 +20790,7 @@ var EngineRuntime = class {
20762
20790
  const s = this.o.getSettings;
20763
20791
  const logger = new OtlpLogger({
20764
20792
  getSettings: s,
20765
- transport: async (url, body) => this.o.postLogs(url, body),
20793
+ transport: async (url, body, headers) => this.o.postLogs(url, body, headers),
20766
20794
  resource: { "service.version": this.o.version, "host.name": this.o.hostName }
20767
20795
  });
20768
20796
  const source = this.o.hostName;
@@ -22069,7 +22097,7 @@ var NodeWatcher = class {
22069
22097
  };
22070
22098
 
22071
22099
  // src/node/service.ts
22072
- var VERSION = true ? "1.2.1" : "dev";
22100
+ var VERSION = true ? "1.3.0" : "dev";
22073
22101
  function parseArgs(argv, env = process.env) {
22074
22102
  const get = (name) => {
22075
22103
  const i = argv.indexOf(`--${name}`);
@@ -22137,8 +22165,9 @@ async function startService(o, env = process.env) {
22137
22165
  const res = await fetch(req.url, { method: req.method, headers: req.headers, body: req.body });
22138
22166
  return { status: res.status, text: await res.text() };
22139
22167
  },
22140
- postLogs: async (url, body) => {
22141
- const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body });
22168
+ postLogs: async (url, body, headers) => {
22169
+ const all = { ...headers, ...parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS) };
22170
+ const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...all }, body });
22142
22171
  if (!res.ok) throw new Error(`OTLP HTTP ${res.status}`);
22143
22172
  },
22144
22173
  embeddingKey: () => env.DATANOTES_EMBEDDING_KEY || keyFromFile(),
@@ -22247,7 +22276,7 @@ Docs: https://datanotes.dev`;
22247
22276
  }
22248
22277
 
22249
22278
  // src/node/cli.ts
22250
- var VERSION2 = true ? "1.2.1" : "dev";
22279
+ var VERSION2 = true ? "1.3.0" : "dev";
22251
22280
  async function main2(argv = process.argv.slice(2)) {
22252
22281
  const [command, ...rest] = argv;
22253
22282
  if (command === "--version" || command === "-v" || command === "version") return void process.stdout.write(`${VERSION2}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datanotes",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "The engine of your second brain: Markdown notes as database tables, for you and your agents.",
5
5
  "keywords": [
6
6
  "markdown",