chat-recall 0.5.4 → 0.5.6

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.
@@ -33,6 +33,32 @@ fi
33
33
  CHAT_RECALL_BIN="${CHAT_RECALL_BIN:-$(command -v chat-recall || true)}"
34
34
  if [[ -z "$CHAT_RECALL_BIN" ]] || [[ ! -x "$CHAT_RECALL_BIN" ]]; then exit 0; fi
35
35
 
36
+ # Run a command under a time limit, on any platform.
37
+ #
38
+ # `timeout` is GNU coreutils and macOS does not ship it. Both hooks that used it
39
+ # swallowed the failure with `|| true`, so on every Mac the command never ran at
40
+ # all — silently, while `chat-recall doctor` reported the hook installed and
41
+ # healthy. Homebrew's coreutils installs it as `gtimeout`, so prefer whichever
42
+ # exists and fall back to a plain shell watchdog when neither does.
43
+ run_limited() {
44
+ local secs="$1"; shift
45
+ if command -v timeout >/dev/null 2>&1; then
46
+ timeout "$secs" "$@"
47
+ elif command -v gtimeout >/dev/null 2>&1; then
48
+ gtimeout "$secs" "$@"
49
+ else
50
+ # Watchdog: run in the background, kill it if it outlives the budget.
51
+ "$@" &
52
+ local cmd_pid=$!
53
+ ( sleep "$secs"; kill -TERM "$cmd_pid" 2>/dev/null || true ) &
54
+ local watch_pid=$!
55
+ wait "$cmd_pid" 2>/dev/null
56
+ local rc=$?
57
+ kill -TERM "$watch_pid" 2>/dev/null || true
58
+ return $rc
59
+ fi
60
+ }
61
+
36
62
  TIMEOUT="${CHAT_RECALL_ESCALATE_TIMEOUT:-60}"
37
63
  DELAY="${CHAT_RECALL_ESCALATE_DELAY:-20}"
38
64
 
@@ -42,9 +68,9 @@ DELAY="${CHAT_RECALL_ESCALATE_DELAY:-20}"
42
68
  sleep "$DELAY"
43
69
  cd "$CWD" 2>/dev/null || true
44
70
  if [[ -n "$SESSION_ID" ]]; then
45
- timeout "$TIMEOUT" "$CHAT_RECALL_BIN" escalate "$SESSION_ID" >/dev/null 2>&1 || true
71
+ run_limited "$TIMEOUT" "$CHAT_RECALL_BIN" escalate "$SESSION_ID" >/dev/null 2>&1 || true
46
72
  else
47
- timeout "$TIMEOUT" "$CHAT_RECALL_BIN" escalate --latest >/dev/null 2>&1 || true
73
+ run_limited "$TIMEOUT" "$CHAT_RECALL_BIN" escalate --latest >/dev/null 2>&1 || true
48
74
  fi
49
75
  ) </dev/null >/dev/null 2>&1 &
50
76
 
@@ -109,7 +109,7 @@ extract_facts() {
109
109
  "tool_uses": $tools,
110
110
  "first_topic": "$(echo "$first_topic" | jq -Rs '.' 2>/dev/null || echo "\"$first_topic\"")",
111
111
  "decisions": "$(echo "$decisions" | jq -Rs '.' 2>/dev/null || echo "\"\"")",
112
- "saved_at": "$(date -Iseconds)"
112
+ "saved_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
113
113
  }
114
114
  EOF
115
115
  }
@@ -176,7 +176,12 @@ do_save() {
176
176
  fi
177
177
 
178
178
  # Track this save
179
- echo "{\"session_id\": \"$SESSION_ID\", \"saved_at\": \"$(date -Iseconds)\", \"precompact\": $precompact}" > "${STATE_FILE}"
179
+ # `date -Iseconds` is GNU-only: BSD date (macOS) rejects -I and prints an
180
+ # empty string, so this field was blank on every Mac. -u +format works on
181
+ # both. saved_at_epoch is written alongside it because reading the ISO value
182
+ # BACK needed `date -d`, which BSD also lacks — storing the number means
183
+ # nothing has to parse a date at all.
184
+ echo "{\"session_id\": \"$SESSION_ID\", \"saved_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"saved_at_epoch\": $(date +%s), \"precompact\": $precompact}" > "${STATE_FILE}"
180
185
 
181
186
  if [[ "$precompact" == "true" ]]; then
182
187
  log "[PRECOMPACT] Emergency save for $SESSION_ID"
@@ -193,17 +198,18 @@ should_save() {
193
198
  return 0
194
199
  fi
195
200
 
196
- # Check last save time
197
- local last_save
198
- last_save=$(jq -r '.saved_at // empty' "${STATE_FILE}" 2>/dev/null || echo "")
199
-
200
- if [[ -z "$last_save" ]]; then
201
+ # Read the epoch we stored. The old code kept only an ISO string and parsed
202
+ # it with `date -d`, which does not exist on BSD date — so on macOS this
203
+ # always resolved to 0, the throttle never engaged, and the hook re-scanned
204
+ # the entire transcript on EVERY Stop event.
205
+ local last_save_epoch
206
+ last_save_epoch=$(jq -r '.saved_at_epoch // empty' "${STATE_FILE}" 2>/dev/null || echo "")
207
+
208
+ if [[ -z "$last_save_epoch" ]]; then
209
+ # A state file written before saved_at_epoch existed. Treat it as due
210
+ # rather than trying to parse its timestamp portably.
201
211
  return 0
202
212
  fi
203
-
204
- # Don't save more than once per 30 seconds
205
- local last_save_epoch
206
- last_save_epoch=$(date -d "$last_save" +%s 2>/dev/null || echo 0)
207
213
  local now_epoch
208
214
  now_epoch=$(date +%s)
209
215
 
@@ -27,7 +27,33 @@ CHAT_RECALL_BIN="${CHAT_RECALL_BIN:-$(command -v chat-recall || true)}"
27
27
  if [[ -z "$CHAT_RECALL_BIN" ]] || [[ ! -x "$CHAT_RECALL_BIN" ]]; then exit 0; fi
28
28
 
29
29
  TIMEOUT="${CHAT_RECALL_WAKEUP_TIMEOUT:-10}"
30
- OUT="$(timeout "$TIMEOUT" "$CHAT_RECALL_BIN" memory wake-up -p "$PROJECT" 2>/dev/null || true)"
30
+ # Run a command under a time limit, on any platform.
31
+ #
32
+ # `timeout` is GNU coreutils and macOS does not ship it. Both hooks that used it
33
+ # swallowed the failure with `|| true`, so on every Mac the command never ran at
34
+ # all — silently, while `chat-recall doctor` reported the hook installed and
35
+ # healthy. Homebrew's coreutils installs it as `gtimeout`, so prefer whichever
36
+ # exists and fall back to a plain shell watchdog when neither does.
37
+ run_limited() {
38
+ local secs="$1"; shift
39
+ if command -v timeout >/dev/null 2>&1; then
40
+ timeout "$secs" "$@"
41
+ elif command -v gtimeout >/dev/null 2>&1; then
42
+ gtimeout "$secs" "$@"
43
+ else
44
+ # Watchdog: run in the background, kill it if it outlives the budget.
45
+ "$@" &
46
+ local cmd_pid=$!
47
+ ( sleep "$secs"; kill -TERM "$cmd_pid" 2>/dev/null || true ) &
48
+ local watch_pid=$!
49
+ wait "$cmd_pid" 2>/dev/null
50
+ local rc=$?
51
+ kill -TERM "$watch_pid" 2>/dev/null || true
52
+ return $rc
53
+ fi
54
+ }
55
+
56
+ OUT="$(run_limited "$TIMEOUT" "$CHAT_RECALL_BIN" memory wake-up -p "$PROJECT" 2>/dev/null || true)"
31
57
  [[ -z "$OUT" ]] && exit 0
32
58
 
33
59
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat-recall",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/munhq/chat-recall.git"
@@ -11,6 +11,9 @@
11
11
  },
12
12
  "description": "Local-first memory for AI coding tools \u2014 indexes Claude Code / Gemini CLI / OpenCode / Codex sessions, exposes them over MCP, optionally syncs redacted history to a chat-recall server",
13
13
  "type": "module",
14
+ "engines": {
15
+ "node": ">=22.5.0"
16
+ },
14
17
  "license": "Elastic-2.0",
15
18
  "bin": {
16
19
  "chat-recall": "./dist/cli.js",
@@ -25,9 +28,10 @@
25
28
  "LICENSE"
26
29
  ],
27
30
  "scripts": {
28
- "build": "tsc -b && node scripts/bundle.mjs && node scripts/postbuild.mjs",
31
+ "build": "tsc -b && tsc -p tsconfig.daemon.json && node scripts/bundle.mjs && node scripts/postbuild.mjs",
29
32
  "build:types": "tsc -b",
30
- "auto-indexer": "tsx auto-indexer/indexer.ts"
33
+ "auto-indexer": "tsx auto-indexer/indexer.ts",
34
+ "typecheck:daemon": "tsc -p tsconfig.daemon.json"
31
35
  },
32
36
  "dependencies": {
33
37
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -31,6 +31,7 @@ read except these, which change stored memory or state — treat each as an edit
31
31
  and apply the test in `chat-recall-memory` before calling one:
32
32
  `recall_kg_add`, `recall_kg_invalidate`, `recall_decision_record`,
33
33
  `recall_diary_write`, `recall_set`, `recall_task_create`, `recall_task_update`,
34
+ `recall_task_comment`,
34
35
  `recall_security_dismiss`, `recall_rename_session`, `recall_regenerate_summary`,
35
36
  `recall_reclassify`, `recall_index`, `recall_code_index`.
36
37
 
@@ -65,9 +66,9 @@ tool is registered and never named here, so adding a tool means placing it.
65
66
  **Findings, ranked** — `recall_claude_suggestions` (every finding that becomes an agent-instruction change — the CLAUDE.md rules and skill installs — merged across account scope and every indexed project) and `recall_improvements` (everything else, most urgent first; `create_tasks: true` opens one team task per item). They split the same engines, so an item never appears in both. Neither needs the codeindex binary: without it you still get the account-level half.
66
67
  **What changed** — `recall_edits_timeline` (cross-tool edits), `recall_diff` (per-session diffs), `recall_commits` (did it actually land), `recall_markers` (where a session went sideways).
67
68
  **Durable memory / KG** — `recall_kg_query` / `recall_kg_add` / `recall_kg_invalidate` / `recall_kg_timeline` / `recall_kg_stats`, `recall_decision_record`, `recall_diary_write` / `recall_diary_read`, `recall_set` / `recall_get`.
68
- **Team** — `recall_tasks` (the shared task board), `recall_task_create`, `recall_task_update` (status, assignee, comment; task ids look like `t_…`), `recall_team_activity` (per-teammate × per-project rollup), `recall_shares` (which projects are shared — private by default, nothing is visible to teammates until shared).
69
+ **Team** — `recall_tasks` (the shared task board), `recall_task_create`, `recall_task_update` (status, assignee, linked session; task ids look like `t_…`; when you start work on a task, set it `in_progress` and pass your session id as `linked_session_id` so the board can verify the work shipped), `recall_task_comment` (leave a progress note on a card), `recall_team_activity` (per-teammate × per-project rollup), `recall_shares` (which projects are shared — private by default, nothing is visible to teammates until shared).
69
70
  **Security** — `recall_security_summary` (leaked secrets that still need action — start here for "did I paste a key somewhere"), `recall_security_session` (findings for one session), `recall_security_dismiss` (mark rotated / false positive — do this only when the user confirms which), `recall_security_rules` (tenant detection rules; also tests a regex).
70
- **Health / maintenance** — `recall_status` (is the index alive, what is synced), `recall_index` (sync now), `recall_heal_audit` (sessions whose rendered text is thinner than the raw archive, i.e. truncated upstream), `recall_regenerate_summary` (stored summary is stale or wrong), `recall_reclassify` (re-run the classifier over old chunks), `recall_rename_session`.
71
+ **Health / maintenance** — `recall_status` (is the index alive, what is synced), `recall_index` (sync now), `recall_help` (names the tools the lean profile leaves unlisted — they all still work by name), `recall_heal_audit` (sessions whose rendered text is thinner than the raw archive, i.e. truncated upstream), `recall_regenerate_summary` (stored summary is stale or wrong), `recall_reclassify` (re-run the classifier over old chunks), `recall_rename_session`.
71
72
 
72
73
  ## If you're unsure where to start
73
74