codehere 1.0.2 → 2.0.0-beta.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/LICENSE +1 -2
- package/README.md +89 -46
- package/bin/codehere.js +90 -0
- package/package.json +20 -64
- package/src/brain.js +186 -0
- package/src/commands.js +445 -0
- package/src/gemma.js +48 -0
- package/src/hooks.js +442 -0
- package/src/signals.js +186 -0
- package/dist/application/use-cases/check-claimed-tests.js +0 -81
- package/dist/application/use-cases/detect-test-weakening.js +0 -237
- package/dist/application/use-cases/rerun-tests.js +0 -329
- package/dist/index.js +0 -111
- package/dist/infrastructure/orchestration/artifacts-extractor.js +0 -66
- package/dist/presentation/cli/commands/check-command.js +0 -251
- package/dist/presentation/cli/commands/help-command.js +0 -77
- package/dist/ui.js +0 -53
- package/dist/utils/typo-guard.js +0 -133
- package/dist/utils/version.js +0 -40
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Muhammad Ega
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
-
|
package/README.md
CHANGED
|
@@ -1,67 +1,110 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Codehere
|
|
2
|
+
|
|
3
|
+
**A team brain for vibe coders.** It stops your AI coding agent from going in circles, checks "done" for real, and shares what your team learns. It works through plain files in git.
|
|
4
|
+
|
|
5
|
+
Codehere is built to remove work, not add it. You keep using Claude Code as usual. Codehere talks to the agent, not to you, and stays silent unless it helps.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
| Without Codehere, you… | With Codehere |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Re-explain your team's rules every session | Team rules load into every session automatically |
|
|
12
|
+
| Type "still broken" and the agent guesses what you mean | Codehere adds the last error, the failing test and the files changed to your short message |
|
|
13
|
+
| Ask for a whole app with no way to check it | The agent asks you one question first: "How will you know it works?" Your answer becomes a test |
|
|
14
|
+
| Watch the agent try the same failing fix again and again | After the same error 3 times, the agent is told to stop and find the root cause first |
|
|
15
|
+
| Trust "Done!" and find it broken later | Codehere runs your tests itself. If they fail, the agent goes back to work. |
|
|
16
|
+
| Miss that a test was skipped or deleted to make it "pass" | The agent is blocked and asked to restore it |
|
|
17
|
+
| Lose a long, messy session and start over from scratch | When a long session keeps hitting the same error, the agent suggests `codehere brief`: one clean spec that the next session loads |
|
|
18
|
+
| Write a long message to ask a friend for help | `codehere help` writes a redacted help card for you |
|
|
19
|
+
| Solve a bug that a teammate already solved last week | Their fix reaches your agent the first time the error appears |
|
|
20
|
+
|
|
21
|
+
## Install (2 minutes)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g codehere@next # the 2.0 beta; codehere@latest is still 1.x
|
|
25
|
+
cd your-project
|
|
26
|
+
codehere init # finds your test command, adds hooks to .claude/settings.json
|
|
27
|
+
git add .codehere .claude/settings.json .gitignore
|
|
28
|
+
git commit -m "Add Codehere" && git push
|
|
29
|
+
```
|
|
2
30
|
|
|
3
|
-
|
|
4
|
-
> `codehere check` on your machine, or the GitHub Action on every pull request. No account.
|
|
31
|
+
Each teammate installs the CLI once (`npm install -g codehere@next`). The hooks and the brain arrive through git.
|
|
5
32
|
|
|
6
|
-
|
|
33
|
+
## The team loop
|
|
7
34
|
|
|
8
|
-
|
|
35
|
+
```bash
|
|
36
|
+
codehere rule "Prices are in cents. Never use floats for money." # every agent follows it
|
|
9
37
|
|
|
10
|
-
|
|
38
|
+
# stuck?
|
|
39
|
+
codehere help "Cart total ignores quantity" # writes .codehere/help/<id>.md (redacted)
|
|
40
|
+
git add .codehere && git commit -m "help: cart" && git push
|
|
11
41
|
|
|
12
|
-
|
|
13
|
-
|
|
42
|
+
# teammate, after git pull (their agent also mentions it at session start)
|
|
43
|
+
codehere inbox
|
|
44
|
+
codehere reply <id> "Use price * qty; same bug was in checkout.js" --solved
|
|
45
|
+
git add .codehere && git commit -m "answer" && git push
|
|
14
46
|
```
|
|
15
47
|
|
|
16
|
-
|
|
48
|
+
`--solved` turns the answer into a **team lesson**. From then on, any teammate's agent that hits a similar error gets that fix automatically. When an agent solves a repeated error by itself, Codehere asks it to write a short lesson too.
|
|
17
49
|
|
|
18
|
-
|
|
50
|
+
Don't want to commit help cards? Copy the printed text into WhatsApp, Discord or Slack instead.
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
19
53
|
|
|
20
54
|
```
|
|
21
|
-
codehere
|
|
22
|
-
codehere
|
|
23
|
-
codehere
|
|
24
|
-
codehere
|
|
55
|
+
codehere init [--test "npm test"] set up (safe to run again)
|
|
56
|
+
codehere rule ["text"] add or list team rules
|
|
57
|
+
codehere lesson ["problem" --fix "..."] add or list lessons
|
|
58
|
+
codehere brief [--gemma] restart brief for a fresh session
|
|
59
|
+
codehere help ["question"] [--gemma] help card for a teammate
|
|
60
|
+
codehere inbox [--all] teammates' help cards
|
|
61
|
+
codehere show <id> read a help card
|
|
62
|
+
codehere reply <id> "answer" [--solved] answer; --solved makes a lesson
|
|
63
|
+
codehere status what Codehere has seen
|
|
64
|
+
codehere stats turns to green, loops stopped, context size
|
|
25
65
|
```
|
|
26
66
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Exit codes:
|
|
30
|
-
|
|
31
|
-
- **0**: the tests ran and passed, no test was weakened, no named test file is missing, and the weakened-test scan finished.
|
|
32
|
-
- **1**: the tests failed; the runner reported no test that ran (0 tests, or every test skipped); the change made a test check nothing (an assertion swapped for `assert True` or `assert.ok(true)`, a skip marker, an assertion commented out, or a `return` above it); or a test file named in `--claim` is not in the repository. Usage errors (an unknown `--base`, a missing folder, an unknown option) also exit 1, with the message on stderr and nothing on stdout, and so does an internal error. Pass the claim as `--claim=<text>`: in `--claim --json` the claim is the text `--json`.
|
|
33
|
-
- **2**: nothing could be verified: the run timed out, no test command was recognized, the runner is not installed, the re-run was turned off, or the weakened-test scan could not run (git is not installed, git could not read the repository, or the diff could not be read).
|
|
34
|
-
|
|
35
|
-
Settings, as environment variables: `CODEHERE_TEST_CMD` replaces the detected test command (split on whitespace, no shell); `CODEHERE_RERUN_TIMEOUT_MS` sets the time limit in whole milliseconds (default 120000, two minutes, also used for a value that is not a whole number from 1 to 2147483647); `CODEHERE_RERUN_TESTS=0` turns the re-run off. Detection covers `npm test` (a `test` script in `package.json`), `go test ./...`, `cargo test`, `pytest` (with `pytest.ini`, `conftest.py` or a pytest section in `pyproject.toml` or `setup.cfg`), `vitest.config.ts`/`.js`, `jest.config.js`/`.ts`, and `node --test` for `*.test.js` files.
|
|
36
|
-
|
|
37
|
-
"No test ran" is read from the runner's own summary for node:test, pytest, jest, vitest and Go's `[no test files]`. A Go package with test files but no tests (`[no tests to run]`), `cargo test` with 0 tests, jest with `--passWithNoTests`, mocha, and a test script that only prints "No tests found" read as a pass. A test script that leaves a background process holding its output open is reported as timed out, even when the tests passed.
|
|
67
|
+
## What lives where
|
|
38
68
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
69
|
+
```
|
|
70
|
+
.codehere/
|
|
71
|
+
rules.md team rules (written by people)
|
|
72
|
+
lessons/ solved errors, one markdown file each
|
|
73
|
+
help/ help cards
|
|
74
|
+
briefs/ restart briefs
|
|
75
|
+
config.json test command, loop threshold
|
|
76
|
+
local/ this machine only, git-ignored
|
|
77
|
+
```
|
|
44
78
|
|
|
45
|
-
|
|
46
|
-
- **`--claim`** finds test files named in the summary in plain text, in quotes or backticks, in markdown bold or italics, in brackets, in `path::test` form and with a `:line` suffix. It ignores absolute paths, Windows backslash paths and Java `*Test.java` names. A file counts as present when any file with the same name exists in the checkout (outside `node_modules`, `dist`, `build`, `vendor`, `target` and virtual environments), so a file in the wrong folder is not reported. The search goes 8 folders deep and stops after 50,000 entries, so a named file deeper than that, or in a larger checkout, is reported as missing.
|
|
47
|
-
- **A passing suite with a broken build, and code the tests do not cover.** A receipt says what the test suite showed, not that the software is correct.
|
|
48
|
-
- **Who controls the tests.** `check` runs the project's own test command, so whoever controls that command controls the result, and whoever controls the environment can point the weakened-test scan at another tree (`GIT_DIR`, `GIT_WORK_TREE`). For a pull request from someone you do not trust, a receipt is not evidence. In the Action, use the `pull_request` trigger, not `pull_request_target`.
|
|
79
|
+
Everything is plain markdown, so you can read, edit, review and delete it like any other file.
|
|
49
80
|
|
|
50
|
-
##
|
|
81
|
+
## Design rules (from the research)
|
|
51
82
|
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
83
|
+
- **No AI in the decision path.** Loops, failed tests and tampering are detected with plain rules, so Codehere doesn't inherit the agent's mistakes.
|
|
84
|
+
- **It talks to the agent, not to you.** If Codehere is wrong, the agent does one extra check. You lose nothing.
|
|
85
|
+
- **Silent on normal turns.** Your prompt gets extra context only after a failure, for a feature with no way to check it, or in a long session stuck on one error. Each at most once, under 300 tokens.
|
|
86
|
+
- **It only reacts to this session's work.** Changes you had before the session are ignored. If the agent changed nothing, Codehere does nothing.
|
|
87
|
+
- **It never traps the agent.** At most 3 "not done yet" messages per session, then it lets go.
|
|
88
|
+
- **It fails open.** If anything breaks, the hook exits quietly and the agent carries on.
|
|
89
|
+
- **Small model, small job.** Gemma 4 (via Ollama) is optional and only writes summaries for briefs and help cards, which you read before using. Turn it on with `--gemma`.
|
|
90
|
+
- **Only human-confirmed context at session start.** Rules are written by people. Lessons appear only when a matching error shows up, because auto-generated context files can make agents worse.
|
|
54
91
|
|
|
55
|
-
##
|
|
92
|
+
## Try the demo (no Claude account needed)
|
|
56
93
|
|
|
94
|
+
```bash
|
|
95
|
+
node demo/demo.mjs # two teammates, one repo, the whole loop
|
|
96
|
+
node demo/demo.mjs --step # press Enter between scenes (good for a live talk)
|
|
97
|
+
npm test # end-to-end checks
|
|
57
98
|
```
|
|
58
|
-
cd agent
|
|
59
|
-
npm ci
|
|
60
|
-
npm test # build, then vitest
|
|
61
|
-
npm run lint
|
|
62
|
-
node eval/false-done.mjs
|
|
63
|
-
```
|
|
64
99
|
|
|
65
|
-
##
|
|
100
|
+
## Known limits (2.0.0, not released yet)
|
|
101
|
+
|
|
102
|
+
- Claude Code only for now. Gemini CLI and Codex hooks are next.
|
|
103
|
+
- Lessons match by error text. Very generic errors can bring up a lesson that doesn't fit, which is why the agent is told it "may apply".
|
|
104
|
+
- Redaction catches common keys, tokens and emails. Always read a help card before sharing it.
|
|
105
|
+
- The "done" check needs a test command. Without one, only the tampering check runs.
|
|
106
|
+
- Sharing is through git. There is no server, so teammates see updates after `git pull`.
|
|
107
|
+
- Prompt checks are plain word lists (English and Indonesian). They can miss a short prompt in another language.
|
|
108
|
+
- The weakened-test scan catches skipped tests, deleted test files and removed assertions. It does not yet catch an edited expected value; see `docs/overlap-with-check.md`.
|
|
66
109
|
|
|
67
|
-
MIT
|
|
110
|
+
MIT licensed.
|
package/bin/codehere.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runHook } from '../src/hooks.js';
|
|
3
|
+
import * as cmd from '../src/commands.js';
|
|
4
|
+
|
|
5
|
+
const argv = process.argv.slice(2);
|
|
6
|
+
const BOOLEAN = new Set(['gemma', 'solved', 'all', 'help', 'h']);
|
|
7
|
+
const flags = {};
|
|
8
|
+
const pos = [];
|
|
9
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10
|
+
const a = argv[i];
|
|
11
|
+
if (a.startsWith('--')) {
|
|
12
|
+
const key = a.slice(2);
|
|
13
|
+
const next = argv[i + 1];
|
|
14
|
+
if (!BOOLEAN.has(key) && next !== undefined && !next.startsWith('--')) {
|
|
15
|
+
flags[key] = next;
|
|
16
|
+
i++;
|
|
17
|
+
} else flags[key] = true;
|
|
18
|
+
} else pos.push(a);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const [command, ...rest] = pos;
|
|
22
|
+
|
|
23
|
+
const USAGE = `codehere · a git-native team brain for vibe coders
|
|
24
|
+
|
|
25
|
+
Setup
|
|
26
|
+
codehere init [--test "npm test"] set up .codehere/ and Claude Code hooks
|
|
27
|
+
|
|
28
|
+
Team brain
|
|
29
|
+
codehere rule ["text"] add a team rule (or list rules)
|
|
30
|
+
codehere lesson ["problem" --fix "what fixed it" --sig "error text"]
|
|
31
|
+
add a lesson (or list lessons)
|
|
32
|
+
|
|
33
|
+
When you're stuck
|
|
34
|
+
codehere brief [--gemma] write a restart brief; the next session loads it
|
|
35
|
+
codehere help ["question"] [--gemma]
|
|
36
|
+
write a redacted help card for a teammate
|
|
37
|
+
codehere inbox [--all] see teammates' help cards
|
|
38
|
+
codehere show <id> read a help card
|
|
39
|
+
codehere reply <id> "answer" [--solved]
|
|
40
|
+
answer; --solved turns it into a team lesson
|
|
41
|
+
|
|
42
|
+
codehere status what Codehere has seen
|
|
43
|
+
codehere stats turns to green, loops stopped, session size
|
|
44
|
+
|
|
45
|
+
Hooks (called by Claude Code, not by you)
|
|
46
|
+
codehere hook session-start | prompt | post-tool | stop
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const run = async () => {
|
|
50
|
+
switch (command) {
|
|
51
|
+
case 'hook':
|
|
52
|
+
return runHook(rest[0]);
|
|
53
|
+
case 'init':
|
|
54
|
+
return cmd.init(flags);
|
|
55
|
+
case 'rule':
|
|
56
|
+
case 'rules':
|
|
57
|
+
return cmd.rule(rest.join(' '));
|
|
58
|
+
case 'lesson':
|
|
59
|
+
case 'lessons':
|
|
60
|
+
return cmd.lesson(rest.join(' '), flags);
|
|
61
|
+
case 'brief':
|
|
62
|
+
return cmd.brief(flags);
|
|
63
|
+
case 'help':
|
|
64
|
+
if (flags.h) return console.log(USAGE);
|
|
65
|
+
return cmd.help(rest.join(' '), flags);
|
|
66
|
+
case 'inbox':
|
|
67
|
+
return cmd.inbox(flags);
|
|
68
|
+
case 'show':
|
|
69
|
+
return cmd.show(rest[0]);
|
|
70
|
+
case 'reply':
|
|
71
|
+
return cmd.reply(rest[0], rest.slice(1).join(' '), flags);
|
|
72
|
+
case 'status':
|
|
73
|
+
return cmd.status();
|
|
74
|
+
case 'stats':
|
|
75
|
+
return cmd.stats();
|
|
76
|
+
default:
|
|
77
|
+
if (!command || command === '-h') return console.log(USAGE);
|
|
78
|
+
// stderr only: a hook's stdout can land in the agent's context (e.g. a codehere 1.x
|
|
79
|
+
// user-level hook such as `codehere scan-tool`). Exit 0 when a hook called us.
|
|
80
|
+
console.error(`Unknown command "${command}". Run codehere --help.`);
|
|
81
|
+
if (process.stdin.isTTY) process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
run().catch((err) => {
|
|
86
|
+
// hooks must never break the agent
|
|
87
|
+
if (command === 'hook') process.exit(0);
|
|
88
|
+
console.error(err.message);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
package/package.json
CHANGED
|
@@ -1,79 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codehere",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
4
|
+
"description": "A git-native team brain for vibe coders. Silent fixes for AI coding agents: breaks doom loops, verifies 'done', blocks test tampering, and shares what your team learned.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/muhammadegaa/codehere.git",
|
|
8
|
+
"directory": "v2"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/muhammadegaa/codehere/tree/v2-team-brain/v2#readme",
|
|
6
11
|
"type": "module",
|
|
7
12
|
"bin": {
|
|
8
|
-
"codehere": "
|
|
9
|
-
"ch": "dist/index.js"
|
|
13
|
+
"codehere": "bin/codehere.js"
|
|
10
14
|
},
|
|
11
15
|
"files": [
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
16
|
+
"bin",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md"
|
|
15
19
|
],
|
|
16
20
|
"engines": {
|
|
17
|
-
"node": ">=
|
|
21
|
+
"node": ">=18"
|
|
18
22
|
},
|
|
19
|
-
"os": [
|
|
20
|
-
"darwin",
|
|
21
|
-
"linux"
|
|
22
|
-
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"
|
|
25
|
-
"prebuild": "rm -rf dist",
|
|
26
|
-
"build": "tsc",
|
|
27
|
-
"postbuild": "chmod +x dist/index.js",
|
|
28
|
-
"prepublishOnly": "npm run build && find dist -name '*.map' -delete && find dist -name '*.d.ts' -delete && find dist -name '*.d.ts.map' -delete",
|
|
29
|
-
"start": "node dist/index.js",
|
|
30
|
-
"dev": "tsx src/index.ts",
|
|
31
|
-
"test": "npm run build && vitest run",
|
|
32
|
-
"test:watch": "vitest",
|
|
33
|
-
"lint": "eslint src/ --ext .ts",
|
|
34
|
-
"knip": "knip",
|
|
35
|
-
"knip:gate": "node scripts/knip-gate.mjs",
|
|
36
|
-
"arch:gate": "tsc --noEmit && eslint src/ --ext .ts && node scripts/knip-gate.mjs",
|
|
37
|
-
"format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
|
|
38
|
-
"format:check": "prettier --check 'src/**/*.ts' 'test/**/*.ts'",
|
|
39
|
-
"sbom": "npm sbom --sbom-format=spdx --omit=dev",
|
|
40
|
-
"pretag:check": "../scripts/pretag-npm-check.sh"
|
|
24
|
+
"test": "node test/e2e.mjs"
|
|
41
25
|
},
|
|
42
26
|
"keywords": [
|
|
43
|
-
"ai-coding-agents",
|
|
44
|
-
"test-verification",
|
|
45
|
-
"ci",
|
|
46
|
-
"github-action",
|
|
47
27
|
"claude-code",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
28
|
+
"ai-agents",
|
|
29
|
+
"vibe-coding",
|
|
30
|
+
"hooks",
|
|
31
|
+
"team",
|
|
32
|
+
"memory"
|
|
51
33
|
],
|
|
52
|
-
"
|
|
53
|
-
"license": "MIT",
|
|
54
|
-
"repository": {
|
|
55
|
-
"type": "git",
|
|
56
|
-
"url": "git+https://github.com/muhammadegaa/codehere.git"
|
|
57
|
-
},
|
|
58
|
-
"bugs": {
|
|
59
|
-
"url": "https://github.com/muhammadegaa/codehere/issues"
|
|
60
|
-
},
|
|
61
|
-
"homepage": "https://github.com/muhammadegaa/codehere",
|
|
62
|
-
"dependencies": {
|
|
63
|
-
"chalk": "^5.3.0",
|
|
64
|
-
"commander": "^11.1.0"
|
|
65
|
-
},
|
|
66
|
-
"devDependencies": {
|
|
67
|
-
"@eslint/js": "^10.0.1",
|
|
68
|
-
"@types/node": "^20.19.25",
|
|
69
|
-
"eslint": "^10.2.0",
|
|
70
|
-
"eslint-import-resolver-typescript": "^4.4.5",
|
|
71
|
-
"eslint-plugin-import-x": "^4.16.2",
|
|
72
|
-
"knip": "^6.16.1",
|
|
73
|
-
"prettier": "^3.8.3",
|
|
74
|
-
"tsx": "^4.21.0",
|
|
75
|
-
"typescript": "^5.9.3",
|
|
76
|
-
"typescript-eslint": "^8.58.2",
|
|
77
|
-
"vitest": "^4.1.4"
|
|
78
|
-
}
|
|
34
|
+
"license": "MIT"
|
|
79
35
|
}
|
package/src/brain.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// The team brain: plain markdown files in .codehere/, committed to git.
|
|
2
|
+
// Local, per-machine state lives in .codehere/local/ (git-ignored).
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
export function findRoot(start = process.cwd()) {
|
|
8
|
+
let dir = path.resolve(start);
|
|
9
|
+
while (true) {
|
|
10
|
+
if (fs.existsSync(path.join(dir, '.codehere'))) return dir;
|
|
11
|
+
const parent = path.dirname(dir);
|
|
12
|
+
if (parent === dir) return null;
|
|
13
|
+
dir = parent;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function paths(root) {
|
|
18
|
+
const base = path.join(root, '.codehere');
|
|
19
|
+
return {
|
|
20
|
+
base,
|
|
21
|
+
config: path.join(base, 'config.json'),
|
|
22
|
+
rules: path.join(base, 'rules.md'),
|
|
23
|
+
lessons: path.join(base, 'lessons'),
|
|
24
|
+
help: path.join(base, 'help'),
|
|
25
|
+
briefs: path.join(base, 'briefs'),
|
|
26
|
+
local: path.join(base, 'local'),
|
|
27
|
+
state: path.join(base, 'local', 'state.json'),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ensureDirs(root) {
|
|
32
|
+
const p = paths(root);
|
|
33
|
+
for (const d of [p.base, p.lessons, p.help, p.briefs, p.local]) {
|
|
34
|
+
fs.mkdirSync(d, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
return p;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_CONFIG = {
|
|
40
|
+
testCommand: null,
|
|
41
|
+
verifyOnStop: true,
|
|
42
|
+
loopThreshold: 3,
|
|
43
|
+
testTimeoutSeconds: 120,
|
|
44
|
+
longSessionTokens: 120000,
|
|
45
|
+
gemmaModel: 'gemma4:e4b',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function readConfig(root) {
|
|
49
|
+
const p = paths(root);
|
|
50
|
+
try {
|
|
51
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(fs.readFileSync(p.config, 'utf8')) };
|
|
52
|
+
} catch {
|
|
53
|
+
return { ...DEFAULT_CONFIG };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function writeConfig(root, cfg) {
|
|
58
|
+
fs.writeFileSync(paths(root).config, JSON.stringify(cfg, null, 2) + '\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------- local session state ----------
|
|
62
|
+
|
|
63
|
+
export function readState(root) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(fs.readFileSync(paths(root).state, 'utf8'));
|
|
66
|
+
} catch {
|
|
67
|
+
return { sessions: {}, lastSession: null, lastGreen: null, pendingBrief: null };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function writeState(root, state) {
|
|
72
|
+
const p = paths(root);
|
|
73
|
+
fs.mkdirSync(p.local, { recursive: true });
|
|
74
|
+
fs.writeFileSync(p.state, JSON.stringify(state, null, 2));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function session(state, id) {
|
|
78
|
+
const key = id || 'default';
|
|
79
|
+
if (!state.sessions[key]) {
|
|
80
|
+
state.sessions[key] = {
|
|
81
|
+
errors: {},
|
|
82
|
+
rawErrors: {},
|
|
83
|
+
shownLessons: [],
|
|
84
|
+
stopBlocks: 0,
|
|
85
|
+
lessonAsked: false,
|
|
86
|
+
startedAt: new Date().toISOString(),
|
|
87
|
+
transcript: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
state.lastSession = key;
|
|
91
|
+
state.sessions[key].lastActive = new Date().toISOString();
|
|
92
|
+
// keep the state file small: drop sessions older than the 20 most recent
|
|
93
|
+
const keys = Object.keys(state.sessions);
|
|
94
|
+
if (keys.length > 20) {
|
|
95
|
+
keys
|
|
96
|
+
.sort((a, b) => (state.sessions[a].startedAt || '').localeCompare(state.sessions[b].startedAt || ''))
|
|
97
|
+
.slice(0, keys.length - 20)
|
|
98
|
+
.forEach((k) => delete state.sessions[k]);
|
|
99
|
+
}
|
|
100
|
+
return state.sessions[key];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------- markdown with simple frontmatter ----------
|
|
104
|
+
|
|
105
|
+
export function parseDoc(text) {
|
|
106
|
+
const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
107
|
+
if (!m) return { meta: {}, body: text };
|
|
108
|
+
const meta = {};
|
|
109
|
+
for (const line of m[1].split('\n')) {
|
|
110
|
+
const i = line.indexOf(':');
|
|
111
|
+
if (i > 0) meta[line.slice(0, i).trim()] = line.slice(i + 1).trim();
|
|
112
|
+
}
|
|
113
|
+
return { meta, body: m[2] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderDoc(meta, body) {
|
|
117
|
+
const head = Object.entries(meta)
|
|
118
|
+
.map(([k, v]) => `${k}: ${String(v).replace(/\n/g, ' ')}`)
|
|
119
|
+
.join('\n');
|
|
120
|
+
return `---\n${head}\n---\n${body.trimEnd()}\n`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function listDocs(dir) {
|
|
124
|
+
if (!fs.existsSync(dir)) return [];
|
|
125
|
+
return fs
|
|
126
|
+
.readdirSync(dir)
|
|
127
|
+
.filter((f) => f.endsWith('.md'))
|
|
128
|
+
.sort()
|
|
129
|
+
.map((f) => {
|
|
130
|
+
const file = path.join(dir, f);
|
|
131
|
+
const { meta, body } = parseDoc(fs.readFileSync(file, 'utf8'));
|
|
132
|
+
return { id: f.replace(/\.md$/, ''), file, meta, body };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function slug(text, max = 40) {
|
|
137
|
+
return (
|
|
138
|
+
String(text)
|
|
139
|
+
.toLowerCase()
|
|
140
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
141
|
+
.replace(/^-+|-+$/g, '')
|
|
142
|
+
.slice(0, max) || 'note'
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function stamp() {
|
|
147
|
+
const d = new Date();
|
|
148
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
149
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function today() {
|
|
153
|
+
return new Date().toISOString().slice(0, 10);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------- rules ----------
|
|
157
|
+
|
|
158
|
+
export function readRules(root) {
|
|
159
|
+
const file = paths(root).rules;
|
|
160
|
+
if (!fs.existsSync(file)) return [];
|
|
161
|
+
return fs
|
|
162
|
+
.readFileSync(file, 'utf8')
|
|
163
|
+
.split('\n')
|
|
164
|
+
.filter((l) => l.startsWith('- '))
|
|
165
|
+
.map((l) => l.slice(2).trim());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------- git helpers (all fail-safe) ----------
|
|
169
|
+
|
|
170
|
+
export function git(root, args) {
|
|
171
|
+
try {
|
|
172
|
+
return execSync(`git ${args}`, { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
173
|
+
.toString()
|
|
174
|
+
.trim();
|
|
175
|
+
} catch {
|
|
176
|
+
return '';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function isGitRepo(root) {
|
|
181
|
+
return git(root, 'rev-parse --is-inside-work-tree') === 'true';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function me(root) {
|
|
185
|
+
return git(root, 'config user.name') || process.env.USER || 'someone';
|
|
186
|
+
}
|