kodelyth-ecc 2.18.0 → 2.20.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/CHANGELOG.md CHANGED
@@ -2,6 +2,173 @@
2
2
 
3
3
  All notable changes to Kodelyth ECC are documented here.
4
4
 
5
+ ## v2.20.0 — 2.19.0 shipped 21 MB instead of 5; here is the guard (September 2026)
6
+
7
+ **Upgrade from 2.19.0.** That release is 4× the size it should be and carries
8
+ 2,570 files that do not belong to the package.
9
+
10
+ ### What happened
11
+
12
+ Testing all 13 install targets meant running the 9 project-scoped ones, and
13
+ project-scoped targets write into the directory you are standing in — which was
14
+ the repo root. `git add -A` then swept the lot into the release commit:
15
+
16
+ ```
17
+ .roo 488 · .kimi 488 · .aider-ecc 488 · .gemini 388 · .cursor 315
18
+ .agent 194 · .clinerules 189 · .opencode 17 · + 3 stray files
19
+ ```
20
+
21
+ ```
22
+ files unpacked
23
+ 2.18.0 794 5.01 MB
24
+ 2.19.0 3,364 20.89 MB ← 4.2x
25
+ 2.20.0 796 4.80 MB ← 2.18.0 + 2 new source files
26
+ ```
27
+
28
+ Nothing caught it. Tests passed, CI was green, the version published. The only
29
+ signal was the tarball size and nothing was watching it.
30
+
31
+ ### The guards
32
+
33
+ Three, because the first two fail open:
34
+
35
+ 1. **`files` allowlist in package.json** — an allowlist excludes a new stray
36
+ directory by default. `.npmignore` alone is a denylist: it only stops what
37
+ someone thought to name. Note that `files` *overrides* `.npmignore`, so the
38
+ exclusions that file already encoded are restated as negations — adding an
39
+ allowlist without them silently starts shipping content someone had
40
+ deliberately excluded (it re-added 12 files here before that was caught).
41
+ 2. **`.gitignore`** for all 11 install-target outputs.
42
+ 3. **7 packaging tests** that pack the real tarball and assert on it — no
43
+ install-target output, no nested test fixtures, the bin entrypoint present,
44
+ and file count and size inside a band around the 2.18.0 baseline. Verified by
45
+ recreating the exact failure: with the allowlist, 24 planted stray files pack
46
+ as 0; without it, 24 pack and the tests go red.
47
+
48
+ Worth stating plainly: these packaging tests broke Windows CI twice before they
49
+ worked, which is itself the lesson. Spawning npm from Node on Windows has no
50
+ obvious correct form:
51
+
52
+ ```
53
+ execFileSync('npm', [...]) → ENOENT npm is npm.cmd; no PATHEXT without a shell
54
+ execFileSync('npm.cmd', [...]) → EINVAL Node 18.20.2+ refuses .cmd/.bat without shell
55
+ execFileSync(..., shell: true) → works, but DEP0190: args are concatenated, not escaped
56
+ execSync('npm pack ...') → works, no deprecation
57
+ ```
58
+
59
+ The first two were observed on this repo's CI one commit apart — the same class
60
+ of defect the scanner below exists to catch, shipped inside the scanner's own
61
+ companion test. Windows now goes through `execSync` with a single literal
62
+ command string; POSIX still uses an argv array with no shell.
63
+
64
+ Where npm or git genuinely cannot be spawned, the affected tests report why and
65
+ skip instead of going red. A guard that fails for an unrelated reason gets
66
+ disabled, and then it guards nothing. Both paths verified: full environment 7/7
67
+ pass, npm and git hidden 2 pass and 5 skip with a stated reason.
68
+
69
+ ### Added — portability enforcement
70
+
71
+ `npm run portability` scans the shipped markdown for shell constructs that break
72
+ on a platform other than the author's, and the same scan runs as a test, so the
73
+ corpus cannot regress.
74
+
75
+ 11 rules, each carrying the evidence it was derived from — measured on a stock
76
+ macOS shell, not read off a man page. Two were found while writing it:
77
+
78
+ - **macOS `mktemp` ignores `$TMPDIR`.** The process receives the variable and
79
+ Node and Python both honour it; `mktemp` uses `/var/folders/…` regardless. A
80
+ harness that sets `TMPDIR` to isolate itself isolates its Node and Python and
81
+ not its shell.
82
+ - **`zsh` does not word-split an unquoted expansion; `bash` does.** macOS
83
+ defaults to zsh, so `for d in $DIRS` runs **once** on a Mac and N times on
84
+ Linux. This bit during this very release: a cleanup loop reported success for
85
+ work it never did.
86
+
87
+ Also fixed: `grep -c` prints `0` but exits `1` on no matches, so under `set -e`
88
+ a "count the failures" line kills the script on the outcome you wanted.
89
+
90
+ ### Fixed — 21 files across agents, commands and skills
91
+
92
+ `git-rescue` had a real data-loss bug. Its pre-rescue snapshot was:
93
+
94
+ ```bash
95
+ git reflog --all > /tmp/reflog-$(date +%s).txt
96
+ git stash list >> /tmp/reflog-$(date +%s).txt
97
+ ```
98
+
99
+ `$(date +%s)` runs twice. Straddle a second boundary and the append lands in a
100
+ different file from the one the redirect created — so the stash list silently
101
+ goes somewhere other than the snapshot, in the agent whose entire job is not
102
+ losing your work. Reproduced, then replaced with one timestamp, one file,
103
+ written into the repo rather than a temp dir a reboot can clear.
104
+
105
+ Everything else: hardcoded `/tmp` (absent on Windows) across 12 files,
106
+ `/usr/local/bin` (Intel-Homebrew only — Apple Silicon uses `/opt/homebrew`),
107
+ 20 unguarded `rg` invocations in `security-reviewer` that would have reported a
108
+ clean codebase on any machine without ripgrep installed, and PowerShell forms
109
+ plus secret-manager alternatives for `commands/jira.md`, which was telling
110
+ people to paste an API token straight into shell history.
111
+
112
+ ### Corrected
113
+
114
+ `CLAUDE.md` claimed 604 tests across 32 files. The 604 was current; the 32 was
115
+ stale by seven. Now 633 across 41, and the hook count is stated exactly (44
116
+ entries over 8 events, 9 scripts) rather than as "22+".
117
+
118
+ **633 tests passing.**
119
+
120
+ ## v2.19.0 — The install docs were wrong; every command is now tested (September 2026)
121
+
122
+ The install instructions were checked by **running every command they advertise**
123
+ against a throwaway `HOME` and a throwaway working directory. Three of them did
124
+ not work.
125
+
126
+ ### Fixed — commands that failed
127
+
128
+ | Documented | What actually happened |
129
+ |---|---|
130
+ | `--target cursor` | **`Unknown target: cursor`** — the real id is `cursor-project` |
131
+ | `mcp-register --status` | **`unknown subcommand`** — the real command is `mcp-list` |
132
+ | "759 shipped files" | the real dry-run total is **777**, and every row of the table was wrong |
133
+
134
+ Anyone following the Cursor instruction hit a hard error. Anyone following the
135
+ verify step hit another.
136
+
137
+ ### Added — the distinction that was never explained
138
+
139
+ Targets install to one of two places, and the docs never said which:
140
+
141
+ - **Home-scoped** (`claude-code`, `codex-home`, `windsurf-home`, `gemini-home`) —
142
+ run from anywhere, applies machine-wide.
143
+ - **Project-scoped** (`cursor-project`, `antigravity`, `windsurf-project`,
144
+ `gemini-project`, `opencode`, `cline`, `roocode`, `aider`, `kimi`) — writes into
145
+ the directory you are standing in. Run one from your home folder and ECC files
146
+ scatter there instead of into your project.
147
+
148
+ The scope of each was determined by running the installer and counting where the
149
+ files landed, not by reading the source. Two targets that existed but were never
150
+ documented — `windsurf-project` and `gemini-project` — are now listed.
151
+
152
+ ### Changed — the install page
153
+
154
+ Restructured into three numbered steps, with `kodelythecc doctor` as the single
155
+ verification command instead of five separate status calls (the individual ones
156
+ are still there, behind a disclosure). Adds the thing that trips people up most:
157
+ **restart your AI tool** — it reads the config directory at startup, so a running
158
+ session sees none of it. Ends with the support channels, since a failed install
159
+ is exactly when someone needs them.
160
+
161
+ ### Corrected — the uninstall table
162
+
163
+ Every row was stale. Regenerated from a live dry-run on a fresh install:
164
+
165
+ ```
166
+ agents 70 · skills 302 · commands 103 · hooks 12 · rules 106 · scripts 184
167
+ total 777 (was documented as 759)
168
+ ```
169
+
170
+ **604 tests passing.**
171
+
5
172
  ## v2.18.0 — Support channels, shell portability, honest counts (September 2026)
6
173
 
7
174
  ### Added — Support
package/CLAUDE.md CHANGED
@@ -9,7 +9,7 @@ Guidance for Claude Code when working with this repository.
9
9
  - **70 specialist agents** — debug-detective, incident-commander, load-tester, image-architect, kodelyth-memory, security-reviewer, plus 8 adversarial devil-mode agents
10
10
  - **196 skills** — domain knowledge, patterns, testing, security, intent routing, local memory, swarm orchestration, MCP integration
11
11
  - **103 commands** — slash workflows (`/tdd`, `/plan`, `/code-review`, `/team-review`, `/devil-mode`, `/debug-blitz`, `/security-audit`, ...)
12
- - **22+ hooks** — quality gates, memory inject + capture, correction encoding, prompt-injection guard, token-budget enforcer
12
+ - **44 hook entries** across 8 events — quality gates, memory inject + capture, correction encoding, prompt-injection guard, token-budget enforcer
13
13
  - **15 rules** — always-on coding standards + semantic intent routing + memory protocol + self-improvement
14
14
 
15
15
  Works with Claude Code, Windsurf, Cursor, Codex CLI, Antigravity, OpenCode, Cline, Roo Code, Aider, Kimi, and Gemini CLI — **11 platforms** (13 install targets).
@@ -20,13 +20,13 @@ Works with Claude Code, Windsurf, Cursor, Codex CLI, Antigravity, OpenCode, Clin
20
20
  agents/ → 70 specialist subagents (planner, code-reviewer, debug-detective, devil-mode crew, ...)
21
21
  commands/ → 103 slash commands (8 parallel multi-agent, 1 adversarial loop, rest single-agent)
22
22
  skills/ → 196 workflow + domain knowledge files (loadable via slash commands)
23
- hooks/ → 22+ automations (pre-commit, session memory, prompt-injection guard, token-budget)
23
+ hooks/ → 44 hook entries across 8 events, 9 scripts (memory inject/capture, prompt-injection guard, token-budget)
24
24
  rules/ → 15 always-on guidelines (agent-intent-routing, self-improvement, memory-protocol, ...)
25
25
  scripts/ → Node.js utilities: MCP server, dashboard, swarm, replay, router, memory, supply-chain
26
26
  bundles/ → 3 power bundles (indie-hacker, red-team, enterprise)
27
27
  actions/ → GitHub Action (CI/CD integration for PR review)
28
28
  docs/ → Feature docs (arena.md, mcp.md, dashboard.md, swarm.md, replay.md, evolve.md, supply-chain.md)
29
- tests/ → 604 passing tests across 32 test files
29
+ tests/ → 633 passing tests across 41 test files
30
30
  ```
31
31
 
32
32
  ## Running Tests
package/README.md CHANGED
@@ -573,7 +573,7 @@ Type `kodelythecc` alone in a real terminal → arrow-key menu opens.
573
573
 
574
574
  ### Uninstall
575
575
 
576
- The menu's **Uninstall ECC completely** row runs an interactive full cleanup: removes the 759 ECC-installed files from `~/.claude/`, unwires RTK from your AI tool, removes codebase-memory-mcp agent configs, removes ECC's MCP entry from Claude Code + Claude Desktop, and deletes `~/.kodelythecc/` (memory + ledgers). Prompts confirm before anything is deleted; a dry-run mode previews what would be removed without touching anything.
576
+ The menu's **Uninstall ECC completely** row runs an interactive full cleanup: removes the 777 ECC-installed files from `~/.claude/`, unwires RTK from your AI tool, removes codebase-memory-mcp agent configs, removes ECC's MCP entry from Claude Code + Claude Desktop, and deletes `~/.kodelythecc/` (memory + ledgers). Prompts confirm before anything is deleted; a dry-run mode previews what would be removed without touching anything.
577
577
 
578
578
  You can also run it non-interactively:
579
579
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.18.0
1
+ 2.20.0
@@ -119,8 +119,11 @@ stress-ng --vm 4 --vm-bytes 80% --timeout 60s
119
119
  # Hypothesis: every endpoint validates its inputs and never panics/500s on malformed.
120
120
  # Tooling: ffuf, restler, schemathesis
121
121
 
122
+ # The hypothesis database is what makes a re-run reproduce the failing input it
123
+ # found last time. Keep it in the repo, not a temp dir — a reboot that wipes
124
+ # /tmp throws away the shrunk counterexample you were about to debug.
122
125
  schemathesis run https://api.localhost/openapi.json --checks all --hypothesis-deadline 5000 \
123
- --hypothesis-database /tmp/fuzz-state
126
+ --hypothesis-database .hypothesis/fuzz-state
124
127
 
125
128
  # Watch: 500 errors, panics, timeouts, memory leaks
126
129
  ```
@@ -144,10 +147,13 @@ wait
144
147
 
145
148
  ```bash
146
149
  # Hypothesis: app rotates certs 30 days before expiration.
147
- # Tooling: faketime
148
- faketime '+89 days' /usr/local/bin/your-app
150
+ # Tooling: faketime (libfaketime). Resolve the binary rather than hardcoding a
151
+ # prefix /usr/local/bin is Intel-Homebrew only; Apple Silicon uses
152
+ # /opt/homebrew/bin, and a Linux package manager uses /usr/bin.
153
+ APP=$(command -v your-app) || { echo "your-app not on PATH"; exit 1; }
154
+ faketime '+89 days' "$APP"
149
155
  # Watch: rotation event, cert refresh
150
- faketime '+91 days' /usr/local/bin/your-app
156
+ faketime '+91 days' "$APP"
151
157
  # Watch: expiration handling, alert fires
152
158
  ```
153
159
 
@@ -39,13 +39,19 @@ You are Flake Hunter — the engineer who has debugged the test that fails 1 in
39
39
  ### Phase 1 — Get a flake rate
40
40
 
41
41
  ```bash
42
- # Run the suspect test 100 times and count failures
42
+ # Run the suspect test 100 times and count failures.
43
+ # Bare mktemp writes to whatever temp location the platform actually has
44
+ # (/var/folders/... on macOS, /tmp on Linux), so it works everywhere a
45
+ # hardcoded /tmp does not — Windows included. Note: macOS mktemp IGNORES
46
+ # $TMPDIR (measured), so do not try to redirect it that way.
47
+ LOG=$(mktemp)
43
48
  for i in $(seq 1 100); do
44
49
  <test command for this test> --silent || echo "FAIL $i"
45
- done | tee /tmp/flake-runs.log
50
+ done | tee "$LOG"
46
51
 
47
- # Count
48
- grep -c FAIL /tmp/flake-runs.log
52
+ # Count. `grep -c` exits 1 when the count is zero, so guard it or a
53
+ # `set -e` script dies on the good outcome.
54
+ grep -c FAIL "$LOG" || echo 0
49
55
  ```
50
56
 
51
57
  If 0/100 fails locally but it fails on CI: the environment is part of the flake. Move to Phase 2 with that constraint.
@@ -200,10 +200,15 @@ Take screenshots only, analyze visually. Less thorough but works without MCP.
200
200
  For APIs/libraries: run tests, check build, analyze code quality. No browser.
201
201
 
202
202
  ```bash
203
- # Code-only evaluation
204
- npm run build 2>&1 | tee /tmp/build-output.txt
205
- npm test 2>&1 | tee /tmp/test-output.txt
206
- npx eslint . 2>&1 | tee /tmp/lint-output.txt
203
+ # Code-only evaluation. Bare mktemp -d resolves to the platform's real temp
204
+ # location, so this runs unchanged on macOS, Linux and Git Bash; a hardcoded
205
+ # /tmp does not exist on Windows. Echo the path — on macOS it is under
206
+ # /var/folders/... and you will not guess it.
207
+ OUT=$(mktemp -d)
208
+ npm run build 2>&1 | tee "$OUT/build.txt"
209
+ npm test 2>&1 | tee "$OUT/test.txt"
210
+ npx eslint . 2>&1 | tee "$OUT/lint.txt"
211
+ echo "evaluation output: $OUT"
207
212
  ```
208
213
 
209
214
  Score based on: test pass rate, build success, lint issues, code coverage, API response correctness.
@@ -29,11 +29,20 @@ Before any rescue command:
29
29
 
30
30
  ```bash
31
31
  # Make a safety branch from HEAD's current state — costs nothing
32
- git branch backup/rescue-$(date +%s)
33
-
34
- # Capture the full reflog — your map back home
35
- git reflog --all > /tmp/reflog-$(date +%s).txt
36
- git stash list >> /tmp/reflog-$(date +%s).txt
32
+ STAMP=$(date +%Y%m%d-%H%M%S)
33
+ git branch "backup/rescue-$STAMP"
34
+
35
+ # Capture the full reflog — your map back home.
36
+ # Write it INSIDE the repo, not a temp dir: a rescue snapshot that a reboot
37
+ # can delete is not a snapshot. Add the file to .git/info/exclude if the
38
+ # working tree must stay clean.
39
+ SNAPSHOT="git-rescue-$STAMP.txt"
40
+ {
41
+ echo "=== reflog --all ==="; git reflog --all
42
+ echo; echo "=== stash list ==="; git stash list
43
+ echo; echo "=== branches ==="; git branch -avv
44
+ } > "$SNAPSHOT"
45
+ echo "snapshot: $SNAPSHOT"
37
46
  ```
38
47
 
39
48
  ### Phase 1 — Diagnose
@@ -39,16 +39,18 @@ Real-world risks you hunt:
39
39
  ### 1. Inventory all dependency licenses
40
40
 
41
41
  ```bash
42
- # Node
43
- npx license-checker --json --production > /tmp/licenses.json
42
+ # Node. Write the report into the repo, not a temp dir — a compliance
43
+ # artifact you intend to attach to a review should survive a reboot.
44
+ npx license-checker --json --production > licenses.json
44
45
  npx license-checker --summary
45
46
 
46
47
  # Python
47
48
  pip-licenses --format=json
48
49
  pip-licenses --summary
49
50
 
50
- # Go
51
- go-licenses report ./... --template /tmp/template.tpl
51
+ # Go (--template takes a path to a Go text/template you supply; point it at a
52
+ # file in the repo so the command is reproducible for the next person)
53
+ go-licenses report ./... --template ./licenses.tpl
52
54
 
53
55
  # Rust
54
56
  cargo about generate about.hbs
@@ -23,6 +23,17 @@ You are an expert security specialist focused on identifying and remediating vul
23
23
  You are a hunter, not a passive reviewer. On any security task, sweep the codebase with these before reasoning. Each is copy-paste ready (ripgrep; fall back to `grep -rn` if `rg` is absent). Triage every hit — most are real, some are false positives (see that section).
24
24
 
25
25
  ```bash
26
+ # ripgrep is NOT preinstalled on macOS, Linux or Windows. Check first, or every
27
+ # scan below silently reports nothing and you conclude the code is clean.
28
+ command -v rg >/dev/null || {
29
+ echo "ripgrep missing — install it:"
30
+ echo " macOS: brew install ripgrep"
31
+ echo " Debian: apt install ripgrep"
32
+ echo " Windows: winget install BurntSushi.ripgrep.MSVC"
33
+ echo "Or substitute 'grep -rEn' for 'rg -n' below and drop the --glob flags"
34
+ echo "(use --exclude/--exclude-dir instead)."
35
+ }
36
+
26
37
  # ── Dependency + lint baseline ──────────────────────────────────────────────
27
38
  npm audit --audit-level=high 2>/dev/null || pnpm audit || yarn audit
28
39
  npx eslint . --plugin security --quiet 2>/dev/null
package/commands/jira.md CHANGED
@@ -84,10 +84,26 @@ Add `jira` to your `mcpServers` config (see `mcp-configs/mcp-servers.json` for t
84
84
 
85
85
  **Option B — Environment variables:**
86
86
  ```bash
87
+ # macOS/Linux
87
88
  export JIRA_URL="https://yourorg.atlassian.net"
88
89
  export JIRA_EMAIL="your.email@example.com"
89
90
  export JIRA_API_TOKEN="your-api-token"
90
91
  ```
92
+ ```powershell
93
+ # Windows PowerShell
94
+ $env:JIRA_URL = "https://yourorg.atlassian.net"
95
+ $env:JIRA_EMAIL = "your.email@example.com"
96
+ $env:JIRA_API_TOKEN = "your-api-token"
97
+ ```
98
+
99
+ Typing the token directly writes it to your shell history in plaintext, where
100
+ it stays until the history file rolls over. Put it in a `.env` the shell sources
101
+ (and that `.gitignore` covers), or read it from a secret manager:
102
+
103
+ ```bash
104
+ export JIRA_API_TOKEN=$(op read "op://Private/Jira/token") # 1Password
105
+ export JIRA_API_TOKEN=$(security find-generic-password -w -s jira) # macOS Keychain
106
+ ```
91
107
 
92
108
  If credentials are missing, stop and direct the user to set them up.
93
109
 
@@ -80,7 +80,10 @@ command -v gemini >/dev/null 2>&1 && echo "gemini" || true
80
80
 
81
81
  Build the reviewer prompt (identical rubric + instructions as Reviewer A) and write it to a unique temp file:
82
82
  ```bash
83
- PROMPT_FILE=$(mktemp /tmp/santa-reviewer-b-XXXXXX.txt)
83
+ # No path argument: mktemp resolves the platform's own temp location, which
84
+ # exists on macOS, Linux and Git Bash. A literal /tmp template fails outright
85
+ # on Windows.
86
+ PROMPT_FILE=$(mktemp)
84
87
  cat > "$PROMPT_FILE" << 'EOF'
85
88
  ... full rubric + file contents + reviewer instructions ...
86
89
  EOF
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodelyth-ecc",
3
- "version": "2.18.0",
3
+ "version": "2.20.0",
4
4
  "description": "Production-grade AI coding toolkit — 70 agents (incl. devil-mode adversarial crew), 194 skills, 97 commands, parallel multi-agent commands, semantic intent routing, self-learning memory, and a built-in MCP server (16 tools / 6 prompts / 377 resources) that bridges to Claude Desktop, LangGraph, AutoGen, CrewAI, and OpenAI Agents SDK. Works with Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, RooCode, Aider, Kimi, and Gemini CLI.",
5
5
  "author": "Kodelyth <github.com/sifxprime>",
6
6
  "license": "MIT",
@@ -62,12 +62,42 @@
62
62
  },
63
63
  "scripts": {
64
64
  "test": "node tests/run-all.js",
65
- "mcp": "node scripts/mcp/server.js"
65
+ "mcp": "node scripts/mcp/server.js",
66
+ "portability": "node scripts/portability/scan.js agents commands skills hooks rules scripts"
66
67
  },
67
68
  "optionalDependencies": {
68
69
  "@modelcontextprotocol/sdk": "^1.0.4"
69
70
  },
70
71
  "engines": {
71
72
  "node": ">=18.0.0"
72
- }
73
+ },
74
+ "files": [
75
+ "AGENTS.md",
76
+ "CHANGELOG.md",
77
+ "CLAUDE.md",
78
+ "CONTRIBUTING.md",
79
+ "GITHUB_SETUP.md",
80
+ "KODELYTH.md",
81
+ "SECURITY.md",
82
+ "SOUL.md",
83
+ "VERSION",
84
+ "install.ps1",
85
+ "install.sh",
86
+ "actions/",
87
+ "agents/",
88
+ "bin/",
89
+ "bundles/",
90
+ "commands/",
91
+ "contexts/",
92
+ "hooks/",
93
+ "rules/",
94
+ "scripts/",
95
+ "skills/",
96
+ "tasks/",
97
+ "wiki/",
98
+ "!scripts/openclaw-twitter/",
99
+ "!**/tests/",
100
+ "!**/*.log",
101
+ "!**/.DS_Store"
102
+ ]
73
103
  }
@@ -23,6 +23,53 @@ Every row below was checked on a stock macOS shell. These are not theoretical.
23
23
  | `rg` | **any OS** — ripgrep is not preinstalled | `grep -rn` |
24
24
  | `date -d` | **macOS/BSD** | `date -j` on BSD, or do date math in the language |
25
25
  | `find -printf` | **macOS/BSD** | `find ... -exec stat ...` or `-print0 \| xargs` |
26
+ | `TMPDIR=x mktemp` | **macOS** — mktemp ignores TMPDIR entirely | pass a template path, or use the runtime's tempdir |
27
+ | `/tmp/foo` hardcoded | **Windows** — no /tmp | bare `mktemp` / `mktemp -d`, or `os.tmpdir()` |
28
+ | `grep -c` under `set -e` | **any OS** — exits 1 on zero matches | `grep -c x f \|\| echo 0` |
29
+ | `for x in $VAR` | **macOS/zsh** — no word splitting, loops once | list items literally, or `for x in "${ARR[@]}"` |
30
+ | `execFileSync('npm', …)` | **Windows** — ENOENT bare (no PATHEXT), EINVAL on `.cmd` since Node 18.20.2 | `execSync` with one literal command string |
31
+
32
+ ## Measured, not assumed
33
+
34
+ The `TMPDIR` row above surprised us, so here is the evidence. On macOS the
35
+ subprocess receives the variable and every runtime honours it — but `mktemp`
36
+ does not:
37
+
38
+ ```bash
39
+ $ env TMPDIR=/tmp/probe sh -c 'echo $TMPDIR'
40
+ /tmp/probe # the process sees it
41
+ $ env TMPDIR=/tmp/probe sh -c 'mktemp'
42
+ /var/folders/90/.../T/tmp.AlQWd6r4T9 # mktemp ignores it
43
+ $ env TMPDIR=/tmp/probe node -e 'console.log(require("os").tmpdir())'
44
+ /tmp/probe # Node honours it
45
+ $ env TMPDIR=/tmp/probe python3 -c 'import tempfile;print(tempfile.gettempdir())'
46
+ /tmp/probe # Python honours it
47
+ ```
48
+
49
+ The practical consequence: a test harness that sets `TMPDIR` to isolate itself
50
+ will isolate its Node and Python code but **not** its shell `mktemp` calls. If
51
+ isolation matters, pass an explicit template path to `mktemp` rather than
52
+ setting the variable and trusting it.
53
+
54
+ `grep -c` is the other one worth internalising: it prints `0` and exits `1` when
55
+ nothing matches. Under `set -e`, a "count the failures" line kills the script on
56
+ the outcome you were hoping for.
57
+
58
+ ### The one that cost us a release
59
+
60
+ `zsh` does not word-split an unquoted parameter expansion. `bash` does. macOS
61
+ has defaulted to zsh since Catalina, so the same loop does two different things:
62
+
63
+ ```bash
64
+ $ zsh -c 'V="a b c"; for d in $V; do echo "[$d]"; done'
65
+ [a b c] # one iteration
66
+ $ bash -c 'V="a b c"; for d in $V; do echo "[$d]"; done'
67
+ [a] [b] [c] # three iterations
68
+ ```
69
+
70
+ The failure is quiet. A cleanup loop written this way runs once against a
71
+ nonsense path, every command inside it succeeds, and the script reports done
72
+ having removed nothing. Write the list literally, or use a real array.
26
73
 
27
74
  ## Before running a tool that may be absent
28
75
 
@@ -23,6 +23,53 @@ Every row below was checked on a stock macOS shell. These are not theoretical.
23
23
  | `rg` | **any OS** — ripgrep is not preinstalled | `grep -rn` |
24
24
  | `date -d` | **macOS/BSD** | `date -j` on BSD, or do date math in the language |
25
25
  | `find -printf` | **macOS/BSD** | `find ... -exec stat ...` or `-print0 \| xargs` |
26
+ | `TMPDIR=x mktemp` | **macOS** — mktemp ignores TMPDIR entirely | pass a template path, or use the runtime's tempdir |
27
+ | `/tmp/foo` hardcoded | **Windows** — no /tmp | bare `mktemp` / `mktemp -d`, or `os.tmpdir()` |
28
+ | `grep -c` under `set -e` | **any OS** — exits 1 on zero matches | `grep -c x f \|\| echo 0` |
29
+ | `for x in $VAR` | **macOS/zsh** — no word splitting, loops once | list items literally, or `for x in "${ARR[@]}"` |
30
+ | `execFileSync('npm', …)` | **Windows** — ENOENT bare (no PATHEXT), EINVAL on `.cmd` since Node 18.20.2 | `execSync` with one literal command string |
31
+
32
+ ## Measured, not assumed
33
+
34
+ The `TMPDIR` row above surprised us, so here is the evidence. On macOS the
35
+ subprocess receives the variable and every runtime honours it — but `mktemp`
36
+ does not:
37
+
38
+ ```bash
39
+ $ env TMPDIR=/tmp/probe sh -c 'echo $TMPDIR'
40
+ /tmp/probe # the process sees it
41
+ $ env TMPDIR=/tmp/probe sh -c 'mktemp'
42
+ /var/folders/90/.../T/tmp.AlQWd6r4T9 # mktemp ignores it
43
+ $ env TMPDIR=/tmp/probe node -e 'console.log(require("os").tmpdir())'
44
+ /tmp/probe # Node honours it
45
+ $ env TMPDIR=/tmp/probe python3 -c 'import tempfile;print(tempfile.gettempdir())'
46
+ /tmp/probe # Python honours it
47
+ ```
48
+
49
+ The practical consequence: a test harness that sets `TMPDIR` to isolate itself
50
+ will isolate its Node and Python code but **not** its shell `mktemp` calls. If
51
+ isolation matters, pass an explicit template path to `mktemp` rather than
52
+ setting the variable and trusting it.
53
+
54
+ `grep -c` is the other one worth internalising: it prints `0` and exits `1` when
55
+ nothing matches. Under `set -e`, a "count the failures" line kills the script on
56
+ the outcome you were hoping for.
57
+
58
+ ### The one that cost us a release
59
+
60
+ `zsh` does not word-split an unquoted parameter expansion. `bash` does. macOS
61
+ has defaulted to zsh since Catalina, so the same loop does two different things:
62
+
63
+ ```bash
64
+ $ zsh -c 'V="a b c"; for d in $V; do echo "[$d]"; done'
65
+ [a b c] # one iteration
66
+ $ bash -c 'V="a b c"; for d in $V; do echo "[$d]"; done'
67
+ [a] [b] [c] # three iterations
68
+ ```
69
+
70
+ The failure is quiet. A cleanup loop written this way runs once against a
71
+ nonsense path, every command inside it succeeds, and the script reports done
72
+ having removed nothing. Write the list literally, or use a real array.
26
73
 
27
74
  ## Before running a tool that may be absent
28
75
 
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shell-portability rules for the shipped markdown corpus.
5
+ *
6
+ * Every rule below corresponds to a difference that was MEASURED on a stock
7
+ * macOS shell, not inferred from a man page. The `evidence` field records what
8
+ * was actually observed, so a future maintainer can re-verify rather than
9
+ * trust the comment.
10
+ *
11
+ * Scope: these apply to shell text only — fenced code blocks tagged as a shell
12
+ * (or untagged) and inline code spans. Prose is never scanned, because
13
+ * "we set a timeout of 30s" is not a portability defect.
14
+ */
15
+
16
+ const RULES = [
17
+ {
18
+ id: 'gnu-timeout',
19
+ re: /(^|[;|&\s])timeout\s+\d/,
20
+ why: '`timeout` is GNU coreutils and is not installed on macOS',
21
+ fix: 'drop it, or `command -v timeout || gtimeout`',
22
+ evidence: 'macOS 25.5: `command -v timeout` → empty',
23
+ },
24
+ {
25
+ id: 'gnu-stat-c',
26
+ re: /\bstat\s+(?:-[a-zA-Z]+\s+)*-c\b/,
27
+ why: '`stat -c` is GNU; BSD stat uses -f',
28
+ fix: '`wc -c < file` works on both',
29
+ evidence: 'macOS: `stat -c %s f` → "stat: illegal option -- c"',
30
+ },
31
+ {
32
+ id: 'gnu-sed-i',
33
+ re: /\bsed\s+(?:-[a-zA-Z]*\s+)*-i\s+(?!['"])/,
34
+ why: '`sed -i` requires an explicit backup suffix on BSD',
35
+ fix: "`sed -i '' 's/a/b/' f` on BSD, or write to a temp file and mv",
36
+ evidence: 'macOS: `sed -i s/a/b/ f` → "invalid command code"',
37
+ },
38
+ {
39
+ id: 'gnu-grep-p',
40
+ re: /\bgrep\s+(?:-[a-zA-Z]+\s+)*-[a-zA-Z]*P\b/,
41
+ why: '`grep -P` needs PCRE, absent from BSD grep',
42
+ fix: '`grep -E` with a POSIX class',
43
+ evidence: 'macOS: `grep -P` → "this version does not support -P"',
44
+ },
45
+ {
46
+ id: 'gnu-readlink-f',
47
+ re: /\breadlink\s+-f\b/,
48
+ why: '`readlink -f` is absent on older BSD',
49
+ fix: '`cd "$(dirname "$f")" && pwd -P`',
50
+ evidence: 'documented BSD difference; present on macOS 25 but not portable back',
51
+ },
52
+ {
53
+ id: 'gnu-date-d',
54
+ re: /\bdate\s+-d\b/,
55
+ why: '`date -d` is GNU; BSD date uses -j -f',
56
+ fix: 'do date math in the language runtime instead',
57
+ evidence: 'macOS: `date -d yesterday` → "illegal time format"',
58
+ },
59
+ {
60
+ id: 'gnu-find-printf',
61
+ re: /\bfind\b[^\n`]*-printf\b/,
62
+ why: '`-printf` is GNU find only',
63
+ fix: '`-print0 | xargs -0`, or `-exec`',
64
+ evidence: 'macOS: `find . -printf %p` → "unknown primary or operator"',
65
+ },
66
+ {
67
+ id: 'assumes-ripgrep',
68
+ re: /(^|[;|&\s])rg\s+[^\n`]/,
69
+ why: 'ripgrep is not preinstalled on macOS, Linux or Windows',
70
+ fix: 'guard with `command -v rg` and fall back to grep/find',
71
+ guardedBy: 'rg', // one `command -v rg` anywhere in the file clears the file
72
+ evidence: 'not present in any default install image',
73
+ },
74
+ {
75
+ id: 'hardcoded-tmp',
76
+ re: /(^|[\s;|&="'(>])\/tmp\//,
77
+ why: 'there is no /tmp on Windows',
78
+ fix: 'bare `mktemp` / `mktemp -d`, or the runtime tempdir',
79
+ evidence: 'Windows has %TEMP%; /tmp does not exist',
80
+ },
81
+ {
82
+ id: 'homebrew-prefix',
83
+ re: /(^|[\s;|&="'(])\/usr\/local\/bin\//,
84
+ why: '/usr/local/bin is Intel-Homebrew only — Apple Silicon uses /opt/homebrew',
85
+ fix: '`$(command -v tool)`',
86
+ evidence: 'Apple Silicon default prefix is /opt/homebrew',
87
+ },
88
+ {
89
+ // Hit live while writing this file: a cleanup loop silently ran once
90
+ // instead of eleven times, and reported success for work it never did.
91
+ id: 'zsh-word-splitting',
92
+ re: /\bfor\s+\w+\s+in\s+\$[A-Za-z_][A-Za-z0-9_]*\s*;?\s*do\b/,
93
+ why: 'zsh does not word-split an unquoted expansion; bash does — macOS defaults to zsh',
94
+ fix: 'list the items literally, or use an array: `for d in "${ARR[@]}"`',
95
+ evidence: 'measured: `V="a b c"; for d in $V` → 1 iteration in zsh, 3 in bash',
96
+ },
97
+ {
98
+ id: 'grep-c-under-set-e',
99
+ re: /(^|[;|&\s])grep\s+(?:-[a-zA-Z]+\s+)*-[a-zA-Z]*c\b(?![^\n]*\|\|)/,
100
+ why: '`grep -c` exits 1 on a zero count, killing a `set -e` script on the good outcome',
101
+ fix: 'append `|| echo 0`',
102
+ evidence: 'measured: `grep -c X empty` prints 0, exits 1',
103
+ },
104
+ ];
105
+
106
+ module.exports = { RULES };
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { RULES } = require('./rules.js');
6
+
7
+ const SHELL_LANGS = new Set(['', 'bash', 'sh', 'shell', 'zsh', 'console', 'terminal']);
8
+
9
+ /**
10
+ * Extract only the executable-looking regions of a markdown file.
11
+ *
12
+ * Prose is deliberately excluded. A sentence like "we raised the timeout to 30"
13
+ * is not a portability defect, and flagging it trains people to ignore the
14
+ * scanner — which is worse than not having one.
15
+ */
16
+ function shellLines(src) {
17
+ const out = [];
18
+ let m;
19
+
20
+ const fence = /```(\w*)\n([\s\S]*?)```/g;
21
+ while ((m = fence.exec(src)) !== null) {
22
+ if (SHELL_LANGS.has((m[1] || '').toLowerCase())) out.push(...m[2].split('\n'));
23
+ }
24
+
25
+ const inline = /`([^`\n]+)`/g;
26
+ while ((m = inline.exec(src)) !== null) out.push(m[1]);
27
+
28
+ return out;
29
+ }
30
+
31
+ /**
32
+ * A line that is itself teaching the anti-pattern is not a violation.
33
+ * The shell-portability rule documents `timeout 30 npm test` as the WRONG form;
34
+ * flagging its own documentation would make the scanner permanently red.
35
+ */
36
+ function isTeachingExample(line) {
37
+ return /^\s*#/.test(line) || /\bWRONG\b|\bBreaks on\b|not installed|illegal option/i.test(line);
38
+ }
39
+
40
+ /**
41
+ * A line that CHECKS for a tool is the fix, not the defect. Without this the
42
+ * scanner flags its own recommended remedy, which is the fastest way to teach
43
+ * people that the scanner is wrong.
44
+ */
45
+ function isGuard(line) {
46
+ return /command\s+-v\s+\S+|which\s+\S+\s*>\/dev\/null|\|\|\s*(echo|gtimeout|true)/.test(line);
47
+ }
48
+
49
+ /** A bare command name in prose backticks — `grep -c` — is a reference, not a call. */
50
+ function isBareMention(line) {
51
+ return /^[a-z-]+(\s+-{1,2}[a-zA-Z-]+)*$/.test(line.trim());
52
+ }
53
+
54
+ function walk(dir, acc = []) {
55
+ let entries;
56
+ try {
57
+ entries = fs.readdirSync(dir, { withFileTypes: true });
58
+ } catch {
59
+ return acc;
60
+ }
61
+ for (const e of entries) {
62
+ if (e.isSymbolicLink()) continue; // never follow — a link out of the tree escapes scope
63
+ const p = path.join(dir, e.name);
64
+ if (e.isDirectory()) {
65
+ if (e.name !== 'node_modules' && e.name !== '.git') walk(p, acc);
66
+ } else if (e.name.endsWith('.md')) {
67
+ acc.push(p);
68
+ }
69
+ }
70
+ return acc;
71
+ }
72
+
73
+ /**
74
+ * @param {string[]} dirs directories to scan
75
+ * @param {object} [opts]
76
+ * @param {string[]} [opts.exclude] path substrings to skip entirely
77
+ * @returns {{file:string, rule:string, why:string, fix:string, line:string}[]}
78
+ */
79
+ function scan(dirs, opts = {}) {
80
+ const exclude = opts.exclude || [];
81
+ const findings = [];
82
+
83
+ for (const dir of dirs) {
84
+ for (const file of walk(dir)) {
85
+ if (exclude.some((x) => file.includes(x))) continue;
86
+
87
+ let src;
88
+ try {
89
+ src = fs.readFileSync(file, 'utf8');
90
+ } catch {
91
+ continue;
92
+ }
93
+
94
+ const lines = shellLines(src);
95
+
96
+ // File-level guards. A file that checks for a tool once at the top has
97
+ // handled it for every later use; flagging those uses individually would
98
+ // demand a guard on all twenty lines, which nobody would write.
99
+ const guardedTools = new Set();
100
+ for (const l of lines) {
101
+ const m = /command\s+-v\s+([a-z0-9_-]+)/i.exec(l);
102
+ if (m) guardedTools.add(m[1].toLowerCase());
103
+ }
104
+
105
+ for (const rule of RULES) {
106
+ if (rule.guardedBy && guardedTools.has(rule.guardedBy)) continue;
107
+ const hit = lines.find(
108
+ (l) => !isTeachingExample(l) && !isGuard(l) && !isBareMention(l) && rule.re.test(l)
109
+ );
110
+ if (hit) {
111
+ findings.push({
112
+ file,
113
+ rule: rule.id,
114
+ why: rule.why,
115
+ fix: rule.fix,
116
+ line: hit.trim().slice(0, 100),
117
+ });
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ return findings;
124
+ }
125
+
126
+ module.exports = { scan, shellLines, isTeachingExample, isGuard, isBareMention, walk };
127
+
128
+ if (require.main === module) {
129
+ const dirs = process.argv.slice(2);
130
+ if (dirs.length === 0) {
131
+ console.error('usage: node scripts/portability/scan.js <dir> [dir...]');
132
+ process.exit(2);
133
+ }
134
+ const findings = scan(dirs, { exclude: ['shell-portability.md'] });
135
+ if (findings.length === 0) {
136
+ console.log(`portability: clean across ${dirs.join(', ')}`);
137
+ process.exit(0);
138
+ }
139
+ for (const f of findings) {
140
+ console.log(`${f.file}\n [${f.rule}] ${f.why}\n fix: ${f.fix}\n > ${f.line}\n`);
141
+ }
142
+ console.log(`${findings.length} portability finding(s)`);
143
+ process.exit(1);
144
+ }
@@ -62,8 +62,15 @@ Use repo-local evidence before making any classification:
62
62
  Useful commands include:
63
63
 
64
64
  ```bash
65
- rg --files
66
- rg -n "typescript|react|next|supabase|django|spring|flutter|swift"
65
+ # ripgrep is not preinstalled anywhere; fall back to find/grep when absent.
66
+ if command -v rg >/dev/null; then
67
+ rg --files
68
+ rg -n "typescript|react|next|supabase|django|spring|flutter|swift"
69
+ else
70
+ find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*'
71
+ grep -rEn "typescript|react|next|supabase|django|spring|flutter|swift" . \
72
+ --exclude-dir=node_modules --exclude-dir=.git
73
+ fi
67
74
  cat package.json
68
75
  cat pyproject.toml
69
76
  cat Cargo.toml
@@ -514,10 +514,14 @@ evictionContext ─────────────────────
514
514
 
515
515
  ### Worktree Isolation
516
516
 
517
- Every unit runs in an isolated worktree (uses jj/Jujutsu, not git):
517
+ Every unit runs in an isolated worktree (uses jj/Jujutsu, not git), created
518
+ under the platform's temp directory:
518
519
  ```
519
- /tmp/workflow-wt-{unit-id}/
520
+ $(mktemp -d)/workflow-wt-{unit-id}/
520
521
  ```
522
+ Resolve the base once per run and reuse it — hardcoding `/tmp` breaks on
523
+ Windows, and re-resolving it per stage would hand each stage a different
524
+ directory, defeating the shared-state behaviour described below.
521
525
 
522
526
  Pipeline stages for the same unit **share** a worktree, preserving state (context files, plan files, code changes) across research → plan → implement → test → review.
523
527
 
@@ -25,14 +25,19 @@ This skill must be accessible to Claude Code before activation. Two ways to boot
25
25
 
26
26
  ## Step 0: Clone ECC Repository
27
27
 
28
- Before any installation, clone the latest ECC source to `/tmp`:
28
+ Before any installation, clone the latest ECC source to a scratch directory:
29
29
 
30
30
  ```bash
31
- rm -rf /tmp/kodelyth-ecc
32
- git clone https://github.com/sifxprime/kodelyth-ecc.git /tmp/kodelyth-ecc
31
+ # mktemp -d resolves the platform's own temp location, so this line is the same
32
+ # on macOS, Linux and Git Bash. A literal /tmp does not exist on Windows.
33
+ ECC_ROOT="$(mktemp -d)/kodelyth-ecc"
34
+ git clone --depth 1 https://github.com/sifxprime/kodelyth-ecc.git "$ECC_ROOT"
33
35
  ```
34
36
 
35
- Set `ECC_ROOT=/tmp/kodelyth-ecc` as the source for all subsequent copy operations.
37
+ `$ECC_ROOT` is the source for every copy operation below. Because `mktemp -d`
38
+ returns a fresh directory each time, there is no stale clone to `rm -rf` first —
39
+ which also removes the one line in this skill capable of deleting the wrong
40
+ path if the variable were ever empty.
36
41
 
37
42
  If the clone fails (network issues, etc.), use `AskUserQuestion` to ask the user to provide a local path to an existing ECC clone.
38
43
 
@@ -318,10 +323,13 @@ Options:
318
323
 
319
324
  ## Step 6: Installation Summary
320
325
 
321
- Clean up the cloned repository from `/tmp`:
326
+ Clean up the cloned repository:
322
327
 
323
328
  ```bash
324
- rm -rf /tmp/kodelyth-ecc
329
+ # Guard the expansion. An empty $ECC_ROOT makes this `rm -rf ""`, which is a
330
+ # no-op on the rm we tested — but the same unguarded pattern with a path
331
+ # built by concatenation is how people delete the wrong directory.
332
+ [ -n "$ECC_ROOT" ] && [ -d "$ECC_ROOT" ] && rm -rf "$ECC_ROOT"
325
333
  ```
326
334
 
327
335
  Then print a summary report:
@@ -48,7 +48,7 @@ Split research and implementation into parallel tracks:
48
48
  ```
49
49
  Pane 1 (Research): "Research best practices for rate limiting in Node.js.
50
50
  Check current libraries, compare approaches, and write findings to
51
- /tmp/rate-limit-research.md"
51
+ $OUT_DIR/rate-limit-research.md"
52
52
 
53
53
  Pane 2 (Implement): "Implement rate limiting middleware for our Express API.
54
54
  Start with a basic token bucket, we'll refine after research completes."
@@ -60,7 +60,7 @@ origin: community
60
60
 
61
61
  **调用方式**(仅在已安装且已审核时):
62
62
  1. 先将龙虾名字规整为安全片段:仅保留字母、数字和连字符,其余字符统一替换为 `-`
63
- 2. 将提示词写入临时文件 `/tmp/openclaw-<safe-name>-prompt.md`
63
+ 2. 将提示词写入临时文件 `$(mktemp -d)/openclaw-<safe-name>-prompt.md`
64
64
  3. 使用当前环境允许的生图 skill,传入提示词文件和输出路径
65
65
 
66
66
  **接口约定**:
@@ -84,7 +84,7 @@ The key silhouette recognition points at small size are:
84
84
  ### 路径 A:已安装且已审核的生图 skill
85
85
 
86
86
  1. 先将龙虾名字规整为安全片段:仅保留字母、数字和连字符,其余字符替换为 `-`
87
- 2. 用 Write 工具写入:`/tmp/openclaw-<safe-name>-prompt.md`
87
+ 2. 用 Write 工具写入:`$(mktemp -d)/openclaw-<safe-name>-prompt.md`
88
88
  3. 调用当前环境允许的生图 skill 生成图片
89
89
  4. 用 Read 工具展示生成的图片给用户
90
90
  5. 问用户是否满意,不满意可调整变量重新生成
@@ -43,7 +43,7 @@ The `--task` / `--agents` / `--harness` / `--base-ref` flags enrich the bundle's
43
43
  ```bash
44
44
  npx kodelyth-ecc session-import ~/Desktop/oauth-audit.bundle.json
45
45
  # or:
46
- npx kodelyth-ecc session-import oauth-audit.bundle.json --target /tmp/audit-restore --overwrite
46
+ npx kodelyth-ecc session-import oauth-audit.bundle.json --target ./audit-restore --overwrite
47
47
  ```
48
48
 
49
49
  Restores the bundle into a coordination directory. Useful for inspecting handoffs locally before replaying.
@@ -1,5 +1,15 @@
1
1
  # Capture Guide
2
2
 
3
+ All session files live under one directory so a run can be cleaned up in a
4
+ single step, and so nothing assumes `/tmp` exists (it does not on Windows).
5
+ Resolve it once and export it before any command below:
6
+
7
+ ```bash
8
+ export VIDEODB_DIR="${VIDEODB_DIR:-$(mktemp -d)}"
9
+ echo "videodb session: $VIDEODB_DIR"
10
+ ```
11
+
12
+
3
13
  ## Overview
4
14
 
5
15
  VideoDB Capture enables real-time screen and audio recording with AI processing. Desktop capture currently supports **macOS** only.
@@ -10,7 +20,7 @@ For code-level details (SDK methods, event structures, AI pipelines), see [captu
10
20
 
11
21
  1. **Start WebSocket listener**: `python scripts/ws_listener.py --clear &`
12
22
  2. **Run capture code** (see Complete Capture Workflow below)
13
- 3. **Events written to**: `/tmp/videodb_events.jsonl`
23
+ 3. **Events written to**: `$VIDEODB_DIR/events.jsonl`
14
24
 
15
25
  ---
16
26
 
@@ -32,9 +42,9 @@ No webhooks or polling required. WebSocket delivers all events including session
32
42
 
33
43
  6. **Start the session** with selected channels.
34
44
 
35
- 7. **Wait for session active** by reading events until you see `capture_session.active`. This event contains the `rtstreams` array. Save session info (session ID, RTStream IDs) to a file (e.g. `/tmp/videodb_capture_info.json`) so other scripts can read it.
45
+ 7. **Wait for session active** by reading events until you see `capture_session.active`. This event contains the `rtstreams` array. Save session info (session ID, RTStream IDs) to a file (e.g. `$VIDEODB_DIR/capture_info.json`) so other scripts can read it.
36
46
 
37
- 8. **Keep the process alive.** Use `asyncio.Event` with signal handlers for `SIGINT`/`SIGTERM` to block until explicitly stopped. Write a PID file (e.g. `/tmp/videodb_capture_pid`) so the process can be stopped later with `kill $(cat /tmp/videodb_capture_pid)`. The PID file should be overwritten on every run so reruns always have the correct PID.
47
+ 8. **Keep the process alive.** Use `asyncio.Event` with signal handlers for `SIGINT`/`SIGTERM` to block until explicitly stopped. Write a PID file (e.g. `$VIDEODB_DIR/capture.pid`) so the process can be stopped later with `kill $(cat $VIDEODB_DIR/capture.pid)`. The PID file should be overwritten on every run so reruns always have the correct PID.
38
48
 
39
49
  9. **Start AI pipelines** (in a separate command/script) on each RTStream for audio indexing and visual indexing. Read the RTStream IDs from the saved session info file.
40
50
 
@@ -48,7 +58,7 @@ No webhooks or polling required. WebSocket delivers all events including session
48
58
 
49
59
  12. **Wait for export** by reading events until you see `capture_session.exported`. This event contains `exported_video_id`, `stream_url`, and `player_url`. This may take several seconds after stopping capture.
50
60
 
51
- 13. **Stop WebSocket listener** after receiving the export event. Use `kill $(cat /tmp/videodb_ws_pid)` to cleanly terminate it.
61
+ 13. **Stop WebSocket listener** after receiving the export event. Use `kill $(cat $VIDEODB_DIR/ws.pid)` to cleanly terminate it.
52
62
 
53
63
  ---
54
64
 
@@ -57,8 +67,8 @@ No webhooks or polling required. WebSocket delivers all events including session
57
67
  Proper shutdown order is important to ensure all events are captured:
58
68
 
59
69
  1. **Stop the capture session** — `client.stop_capture()` then `client.shutdown()`
60
- 2. **Wait for export event** — poll `/tmp/videodb_events.jsonl` for `capture_session.exported`
61
- 3. **Stop the WebSocket listener** — `kill $(cat /tmp/videodb_ws_pid)`
70
+ 2. **Wait for export event** — poll `$VIDEODB_DIR/events.jsonl` for `capture_session.exported`
71
+ 3. **Stop the WebSocket listener** — `kill $(cat $VIDEODB_DIR/ws.pid)`
62
72
 
63
73
  Do NOT kill the WebSocket listener before receiving the export event, or you will miss the final video URLs.
64
74
 
@@ -83,7 +93,7 @@ python scripts/ws_listener.py --clear &
83
93
  python scripts/ws_listener.py --clear /path/to/events &
84
94
 
85
95
  # Stop the listener
86
- kill $(cat /tmp/videodb_ws_pid)
96
+ kill $(cat $VIDEODB_DIR/ws.pid)
87
97
  ```
88
98
 
89
99
  **Options:**