engrm 0.4.42 → 0.4.44

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.
@@ -97,10 +97,11 @@ function readProjectConfigFile(directory) {
97
97
  }
98
98
  }
99
99
  function detectProject(directory) {
100
- const remoteUrl = getGitRemoteUrl(directory);
100
+ const resolvedDirectory = resolve(directory);
101
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
101
102
  if (remoteUrl) {
102
103
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
103
- const repoRoot = getGitTopLevel(directory) ?? directory;
104
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
104
105
  return {
105
106
  canonical_id: canonicalId,
106
107
  name: projectNameFromCanonicalId(canonicalId),
@@ -108,21 +109,22 @@ function detectProject(directory) {
108
109
  local_path: repoRoot
109
110
  };
110
111
  }
111
- const configFile = readProjectConfigFile(directory);
112
+ const configFile = readProjectConfigFile(resolvedDirectory);
112
113
  if (configFile) {
113
114
  return {
114
115
  canonical_id: configFile.project_id,
115
116
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
116
117
  remote_url: null,
117
- local_path: directory
118
+ local_path: resolvedDirectory
118
119
  };
119
120
  }
120
- const dirName = basename(directory);
121
+ const dirName = basename(resolvedDirectory);
122
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
121
123
  return {
122
- canonical_id: `local/${dirName}`,
123
- name: dirName,
124
+ canonical_id: `local/${safeDirName}`,
125
+ name: safeDirName,
124
126
  remote_url: null,
125
- local_path: directory
127
+ local_path: resolvedDirectory
126
128
  };
127
129
  }
128
130
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -1625,7 +1627,19 @@ function parseJsonArray2(value) {
1625
1627
  }
1626
1628
  }
1627
1629
  function compactLine(value) {
1628
- const trimmed = value?.replace(/\s+/g, " ").trim();
1630
+ if (value === null || value === undefined)
1631
+ return null;
1632
+ let text;
1633
+ if (typeof value === "string") {
1634
+ text = value;
1635
+ } else {
1636
+ try {
1637
+ text = JSON.stringify(value);
1638
+ } catch {
1639
+ text = String(value);
1640
+ }
1641
+ }
1642
+ const trimmed = text.replace(/\s+/g, " ").trim();
1629
1643
  if (!trimmed)
1630
1644
  return null;
1631
1645
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
@@ -3266,7 +3280,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3266
3280
  import { join as join3 } from "node:path";
3267
3281
  import { homedir } from "node:os";
3268
3282
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3269
- var CLIENT_VERSION = "0.4.42";
3283
+ var CLIENT_VERSION = "0.4.44";
3270
3284
  function hashFile(filePath) {
3271
3285
  try {
3272
3286
  if (!existsSync3(filePath))
@@ -3480,7 +3494,8 @@ function createDefaultConfig() {
3480
3494
  project_name: "shared-experience",
3481
3495
  namespace: "",
3482
3496
  api_key: ""
3483
- }
3497
+ },
3498
+ tool_profile: "full"
3484
3499
  };
3485
3500
  }
3486
3501
  function loadConfig() {
@@ -3551,7 +3566,8 @@ function loadConfig() {
3551
3566
  project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
3552
3567
  namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
3553
3568
  api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
3554
- }
3569
+ },
3570
+ tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
3555
3571
  };
3556
3572
  }
3557
3573
  function saveConfig(config) {
@@ -3606,12 +3622,107 @@ function asObserverMode(value, fallback) {
3606
3622
  return value;
3607
3623
  return fallback;
3608
3624
  }
3625
+ function asToolProfile(value, fallback) {
3626
+ if (value === "full" || value === "memory")
3627
+ return value;
3628
+ return fallback;
3629
+ }
3609
3630
  function asTeams(value, fallback) {
3610
3631
  if (!Array.isArray(value))
3611
3632
  return fallback;
3612
3633
  return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
3613
3634
  }
3614
3635
 
3636
+ // src/sync/auth.ts
3637
+ import { createHash as createHash3 } from "node:crypto";
3638
+
3639
+ // src/storage/outbox.ts
3640
+ function getPendingEntries(db, limit = 50) {
3641
+ const now = Math.floor(Date.now() / 1000);
3642
+ return db.db.query(`SELECT * FROM sync_outbox
3643
+ WHERE (status = 'pending')
3644
+ OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
3645
+ ORDER BY created_at_epoch ASC
3646
+ LIMIT ?`).all(now, limit);
3647
+ }
3648
+ function markSyncing(db, entryId) {
3649
+ const now = Math.floor(Date.now() / 1000);
3650
+ db.db.query("UPDATE sync_outbox SET status = 'syncing', next_retry_epoch = ? WHERE id = ?").run(now, entryId);
3651
+ }
3652
+ function markSynced(db, entryId) {
3653
+ const now = Math.floor(Date.now() / 1000);
3654
+ db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ?, next_retry_epoch = NULL, last_error = NULL WHERE id = ?").run(now, entryId);
3655
+ }
3656
+ function markFailed(db, entryId, error) {
3657
+ const now = Math.floor(Date.now() / 1000);
3658
+ db.db.query(`UPDATE sync_outbox SET
3659
+ status = 'failed',
3660
+ retry_count = retry_count + 1,
3661
+ last_error = ?,
3662
+ next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
3663
+ WHERE id = ?`).run(error, now, entryId);
3664
+ }
3665
+ function classifyOutboxFailure(error) {
3666
+ const normalized = error.toLowerCase();
3667
+ if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
3668
+ return "auth";
3669
+ }
3670
+ if (normalized.includes("429") || normalized.includes("rate limit")) {
3671
+ return "rate_limit";
3672
+ }
3673
+ if (normalized.includes("timeout") || normalized.includes("abort")) {
3674
+ return "timeout";
3675
+ }
3676
+ if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
3677
+ return "network";
3678
+ }
3679
+ if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
3680
+ return "validation";
3681
+ }
3682
+ return "other";
3683
+ }
3684
+ function resetFailedEntries(db) {
3685
+ const result = db.db.query(`UPDATE sync_outbox
3686
+ SET status = 'pending',
3687
+ retry_count = 0,
3688
+ last_error = NULL,
3689
+ next_retry_epoch = NULL
3690
+ WHERE status = 'failed'`).run();
3691
+ return result.changes;
3692
+ }
3693
+ function resetFailedEntriesMatching(db, predicate) {
3694
+ const rows = db.db.query(`SELECT id, last_error
3695
+ FROM sync_outbox
3696
+ WHERE status = 'failed'`).all();
3697
+ const matchingIds = rows.filter((row) => row.last_error && predicate(row.last_error)).map((row) => row.id);
3698
+ if (matchingIds.length === 0)
3699
+ return 0;
3700
+ const placeholders = matchingIds.map(() => "?").join(", ");
3701
+ const result = db.db.query(`UPDATE sync_outbox
3702
+ SET status = 'pending',
3703
+ retry_count = 0,
3704
+ last_error = NULL,
3705
+ next_retry_epoch = NULL
3706
+ WHERE id IN (${placeholders})`).run(...matchingIds);
3707
+ return result.changes;
3708
+ }
3709
+ function resetSyncingEntries(db) {
3710
+ const result = db.db.query(`UPDATE sync_outbox
3711
+ SET status = 'pending',
3712
+ next_retry_epoch = NULL
3713
+ WHERE status = 'syncing'`).run();
3714
+ return result.changes;
3715
+ }
3716
+ function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
3717
+ const cutoff = Math.floor(Date.now() / 1000) - maxAgeSeconds;
3718
+ const result = db.db.query(`UPDATE sync_outbox
3719
+ SET status = 'pending',
3720
+ next_retry_epoch = NULL
3721
+ WHERE status = 'syncing'
3722
+ AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?)`).run(cutoff);
3723
+ return result.changes;
3724
+ }
3725
+
3615
3726
  // src/sync/auth.ts
3616
3727
  var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
3617
3728
  function normalizeBaseUrl(url) {
@@ -3643,6 +3754,36 @@ function getBaseUrl(config) {
3643
3754
  }
3644
3755
  return null;
3645
3756
  }
3757
+ function getAuthFingerprint(config) {
3758
+ const apiKey = getApiKey(config);
3759
+ const baseUrl = getBaseUrl(config);
3760
+ if (!apiKey || !baseUrl)
3761
+ return null;
3762
+ return createHash3("sha256").update(`${baseUrl}
3763
+ ${apiKey}
3764
+ ${config.namespace}
3765
+ ${config.site_id}`).digest("hex");
3766
+ }
3767
+ function recoverOutboxAfterAuthChange(db, config) {
3768
+ const fingerprint = getAuthFingerprint(config);
3769
+ if (!fingerprint) {
3770
+ const staleSyncingReset2 = resetStaleSyncingEntries(db);
3771
+ return { fingerprintChanged: false, failedReset: 0, authFailedReset: 0, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
3772
+ }
3773
+ const key = "sync_auth_fingerprint";
3774
+ const previous = db.getSyncState(key);
3775
+ const fingerprintChanged = previous !== fingerprint;
3776
+ if (!fingerprintChanged) {
3777
+ const authFailedReset = resetFailedEntriesMatching(db, (error) => classifyOutboxFailure(error) === "auth");
3778
+ const staleSyncingReset2 = resetStaleSyncingEntries(db);
3779
+ return { fingerprintChanged: false, failedReset: 0, authFailedReset, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
3780
+ }
3781
+ const failedReset = resetFailedEntries(db);
3782
+ const syncingReset = resetSyncingEntries(db);
3783
+ const staleSyncingReset = 0;
3784
+ db.setSyncState(key, fingerprint);
3785
+ return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
3786
+ }
3646
3787
  function buildSourceId(config, localId, type = "obs") {
3647
3788
  return `${config.user_id}-${config.device_id}-${type}-${localId}`;
3648
3789
  }
@@ -3905,6 +4046,7 @@ class VectorClient {
3905
4046
  apiKey;
3906
4047
  siteId;
3907
4048
  namespace;
4049
+ timeoutMs;
3908
4050
  constructor(config, overrides = {}) {
3909
4051
  const baseUrl = getBaseUrl(config);
3910
4052
  const apiKey = overrides.apiKey ?? getApiKey(config);
@@ -3915,6 +4057,7 @@ class VectorClient {
3915
4057
  this.apiKey = apiKey;
3916
4058
  this.siteId = overrides.siteId ?? config.site_id;
3917
4059
  this.namespace = overrides.namespace ?? config.namespace;
4060
+ this.timeoutMs = overrides.timeoutMs ?? 1e4;
3918
4061
  }
3919
4062
  static isConfigured(config) {
3920
4063
  return getApiKey(config) !== null && getBaseUrl(config) !== null;
@@ -3977,6 +4120,7 @@ class VectorClient {
3977
4120
  if (body && method !== "GET") {
3978
4121
  init.body = JSON.stringify(body);
3979
4122
  }
4123
+ init.signal = AbortSignal.timeout(this.timeoutMs);
3980
4124
  const response = await fetch(url, init);
3981
4125
  if (!response.ok) {
3982
4126
  const text = await response.text().catch(() => "");
@@ -4629,6 +4773,7 @@ function ensureObservationTypes(db) {
4629
4773
  DROP TABLE observations;
4630
4774
  ALTER TABLE observations_repair RENAME TO observations;
4631
4775
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
4776
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
4632
4777
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
4633
4778
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
4634
4779
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -4761,7 +4906,7 @@ function getSchemaVersion(db) {
4761
4906
  var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
4762
4907
 
4763
4908
  // src/storage/sqlite.ts
4764
- import { createHash as createHash3 } from "node:crypto";
4909
+ import { createHash as createHash4 } from "node:crypto";
4765
4910
  var IS_BUN = typeof globalThis.Bun !== "undefined";
4766
4911
  function openDatabase(dbPath) {
4767
4912
  if (IS_BUN) {
@@ -4829,6 +4974,7 @@ class MemDatabase {
4829
4974
  this.db = openDatabase(dbPath);
4830
4975
  this.db.exec("PRAGMA journal_mode = WAL");
4831
4976
  this.db.exec("PRAGMA foreign_keys = ON");
4977
+ this.db.exec("PRAGMA busy_timeout = 5000");
4832
4978
  this.vecAvailable = this.loadVecExtension();
4833
4979
  runMigrations(this.db);
4834
4980
  ensureObservationTypes(this.db);
@@ -4850,8 +4996,16 @@ class MemDatabase {
4850
4996
  this.db.close();
4851
4997
  }
4852
4998
  upsertProject(project) {
4999
+ const canonicalId = project.canonical_id?.trim();
5000
+ const name = project.name?.trim();
5001
+ if (!canonicalId) {
5002
+ throw new Error("Project canonical_id is required");
5003
+ }
5004
+ if (!name) {
5005
+ throw new Error("Project name is required");
5006
+ }
4853
5007
  const now = Math.floor(Date.now() / 1000);
4854
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
5008
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
4855
5009
  if (existing) {
4856
5010
  this.db.query(`UPDATE projects SET
4857
5011
  local_path = COALESCE(?, local_path),
@@ -4866,7 +5020,7 @@ class MemDatabase {
4866
5020
  };
4867
5021
  }
4868
5022
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
4869
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
5023
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
4870
5024
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
4871
5025
  }
4872
5026
  getProjectByCanonicalId(canonicalId) {
@@ -5542,7 +5696,7 @@ class MemDatabase {
5542
5696
  }
5543
5697
  }
5544
5698
  function hashPrompt(prompt) {
5545
- return createHash3("sha256").update(prompt).digest("hex");
5699
+ return createHash4("sha256").update(prompt).digest("hex");
5546
5700
  }
5547
5701
 
5548
5702
  // src/hooks/common.ts